{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,KAAK,cAAc,EAA4B,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAiBzG;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACtC,cAAc,EAAE,cAAc,EAC9B,UAAU,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,MAAM,GACrB,MAAM,CAgGR;AAoCD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,eAAe,CAE9E","sourcesContent":["/**\n * Context management for mom.\n *\n * Mom uses two files per channel:\n * - context.jsonl: Structured API messages for LLM context (same format as coding-agent sessions)\n * - log.jsonl: Human-readable channel history for grep (no tool results)\n *\n * This module provides:\n * - syncLogToSessionManager: Syncs messages from log.jsonl to SessionManager\n * - createMomSettingsManager: Creates a SettingsManager backed by workspace settings.json\n */\n\nimport type { UserMessage } from \"@apholdings/jensen-ai\";\nimport { type SessionManager, type SessionMessageEntry, SettingsManager } from \"@apholdings/jensen-code\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\n// ============================================================================\n// Sync log.jsonl to SessionManager\n// ============================================================================\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tisBot?: boolean;\n}\n\n/**\n * Sync user messages from log.jsonl to SessionManager.\n *\n * This ensures that messages logged while mom wasn't running (channel chatter,\n * backfilled messages, messages while busy) are added to the LLM context.\n *\n * @param sessionManager - The SessionManager to sync to\n * @param channelDir - Path to channel directory containing log.jsonl\n * @param excludeSlackTs - Slack timestamp of current message (will be added via prompt(), not sync)\n * @returns Number of messages synced\n */\nexport function syncLogToSessionManager(\n\tsessionManager: SessionManager,\n\tchannelDir: string,\n\texcludeSlackTs?: string,\n): number {\n\tconst logFile = join(channelDir, \"log.jsonl\");\n\n\tif (!existsSync(logFile)) return 0;\n\n\t// Build set of existing message content from session\n\tconst existingMessages = new Set<string>();\n\tfor (const entry of sessionManager.getEntries()) {\n\t\tif (entry.type === \"message\") {\n\t\t\tconst msgEntry = entry as SessionMessageEntry;\n\t\t\tconst msg = msgEntry.message as { role: string; content?: unknown };\n\t\t\tif (msg.role === \"user\" && msg.content !== undefined) {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (typeof content === \"string\") {\n\t\t\t\t\t// Strip timestamp prefix for comparison (live messages have it, synced don't)\n\t\t\t\t\t// Format: [YYYY-MM-DD HH:MM:SS+HH:MM] [username]: text\n\t\t\t\t\tlet normalized = content.replace(/^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}\\] /, \"\");\n\t\t\t\t\t// Strip attachments section\n\t\t\t\t\tconst attachmentsIdx = normalized.indexOf(\"\\n\\n<slack_attachments>\\n\");\n\t\t\t\t\tif (attachmentsIdx !== -1) {\n\t\t\t\t\t\tnormalized = normalized.substring(0, attachmentsIdx);\n\t\t\t\t\t}\n\t\t\t\t\texistingMessages.add(normalized);\n\t\t\t\t} else if (Array.isArray(content)) {\n\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\ttypeof part === \"object\" &&\n\t\t\t\t\t\t\tpart !== null &&\n\t\t\t\t\t\t\t\"type\" in part &&\n\t\t\t\t\t\t\tpart.type === \"text\" &&\n\t\t\t\t\t\t\t\"text\" in part\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tlet normalized = (part as { type: \"text\"; text: string }).text;\n\t\t\t\t\t\t\tnormalized = normalized.replace(/^\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}\\] /, \"\");\n\t\t\t\t\t\t\tconst attachmentsIdx = normalized.indexOf(\"\\n\\n<slack_attachments>\\n\");\n\t\t\t\t\t\t\tif (attachmentsIdx !== -1) {\n\t\t\t\t\t\t\t\tnormalized = normalized.substring(0, attachmentsIdx);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\texistingMessages.add(normalized);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Read log.jsonl and find user messages not in context\n\tconst logContent = readFileSync(logFile, \"utf-8\");\n\tconst logLines = logContent.trim().split(\"\\n\").filter(Boolean);\n\n\tconst newMessages: Array<{ timestamp: number; message: UserMessage }> = [];\n\n\tfor (const line of logLines) {\n\t\ttry {\n\t\t\tconst logMsg: LogMessage = JSON.parse(line);\n\n\t\t\tconst slackTs = logMsg.ts;\n\t\t\tconst date = logMsg.date;\n\t\t\tif (!slackTs || !date) continue;\n\n\t\t\t// Skip the current message being processed (will be added via prompt())\n\t\t\tif (excludeSlackTs && slackTs === excludeSlackTs) continue;\n\n\t\t\t// Skip bot messages - added through agent flow\n\t\t\tif (logMsg.isBot) continue;\n\n\t\t\t// Build the message text as it would appear in context\n\t\t\tconst messageText = `[${logMsg.userName || logMsg.user || \"unknown\"}]: ${logMsg.text || \"\"}`;\n\n\t\t\t// Skip if this exact message text is already in context\n\t\t\tif (existingMessages.has(messageText)) continue;\n\n\t\t\tconst msgTime = new Date(date).getTime() || Date.now();\n\t\t\tconst userMessage: UserMessage = {\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [{ type: \"text\", text: messageText }],\n\t\t\t\ttimestamp: msgTime,\n\t\t\t};\n\n\t\t\tnewMessages.push({ timestamp: msgTime, message: userMessage });\n\t\t\texistingMessages.add(messageText); // Track to avoid duplicates within this sync\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\tif (newMessages.length === 0) return 0;\n\n\t// Sort by timestamp and add to session\n\tnewMessages.sort((a, b) => a.timestamp - b.timestamp);\n\n\tfor (const { message } of newMessages) {\n\t\tsessionManager.appendMessage(message);\n\t}\n\n\treturn newMessages.length;\n}\n\n// ============================================================================\n// Settings manager for mom\n// ============================================================================\n\ntype MomSettingsStorage = Parameters<typeof SettingsManager.fromStorage>[0];\n\nclass WorkspaceSettingsStorage implements MomSettingsStorage {\n\tprivate settingsPath: string;\n\n\tconstructor(workspaceDir: string) {\n\t\tthis.settingsPath = join(workspaceDir, \"settings.json\");\n\t}\n\n\twithLock(scope: \"global\" | \"project\", fn: (current: string | undefined) => string | undefined): void {\n\t\tif (scope === \"project\") {\n\t\t\t// Mom stores all settings in a single workspace file.\n\t\t\tfn(undefined);\n\t\t\treturn;\n\t\t}\n\n\t\tconst current = existsSync(this.settingsPath) ? readFileSync(this.settingsPath, \"utf-8\") : undefined;\n\t\tconst next = fn(current);\n\t\tif (next === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst dir = dirname(this.settingsPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\twriteFileSync(this.settingsPath, next, \"utf-8\");\n\t}\n}\n\nexport function createMomSettingsManager(workspaceDir: string): SettingsManager {\n\treturn SettingsManager.fromStorage(new WorkspaceSettingsStorage(workspaceDir));\n}\n"]}