@mariozechner/pi-coding-agent 0.25.3 → 0.25.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/core/compaction.d.ts.map +1 -1
- package/dist/core/compaction.js +1 -1
- package/dist/core/compaction.js.map +1 -1
- package/dist/core/export-html.d.ts.map +1 -1
- package/dist/core/export-html.js +1 -1
- package/dist/core/export-html.js.map +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +1 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +10 -10
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +0 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/truncate.d.ts.map +1 -1
- package/dist/core/tools/truncate.js +1 -1
- package/dist/core/tools/truncate.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +2 -2
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +3 -3
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +6 -6
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-selector.js +1 -1
- package/dist/modes/interactive/components/hook-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -2
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +14 -14
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -6
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +1 -1
- package/dist/utils/shell.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// If file doesn't exist, loadSessionId() won't set sessionId, so generate one\n\t\t\tif (!this.sessionId) {\n\t\t\t\tthis.sessionId = uuidv4();\n\t\t\t}\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\t/** Check if session persistence is enabled */\n\tisEnabled(): boolean {\n\t\treturn this.enabled;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = \"--\" + cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\") + \"--\";\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(memEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(messageEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path, or null if in --no-session mode (in-memory only).\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Build new entries list (up to but not including branch point)\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.enabled ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.enabled) {\n\t\t\t// Write to file\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t} else {\n\t\t\t// In-memory mode: replace inMemoryEntries, no file created\n\t\t\tthis.inMemoryEntries = newEntries;\n\t\t\tthis.sessionId = newSessionId;\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// If file doesn't exist, loadSessionId() won't set sessionId, so generate one\n\t\t\tif (!this.sessionId) {\n\t\t\t\tthis.sessionId = uuidv4();\n\t\t\t}\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\t/** Check if session persistence is enabled */\n\tisEnabled(): boolean {\n\t\treturn this.enabled;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(messageEntry)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path, or null if in --no-session mode (in-memory only).\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Build new entries list (up to but not including branch point)\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.enabled ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.enabled) {\n\t\t\t// Write to file\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t} else {\n\t\t\t// In-memory mode: replace inMemoryEntries, no file created\n\t\t\tthis.inMemoryEntries = newEntries;\n\t\t\tthis.sessionId = newSessionId;\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -167,7 +167,7 @@ export class SessionManager {
|
|
|
167
167
|
getSessionDirectory() {
|
|
168
168
|
const cwd = process.cwd();
|
|
169
169
|
// Replace all path separators and colons (for Windows drive letters) with dashes
|
|
170
|
-
const safePath =
|
|
170
|
+
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
171
171
|
const configDir = getAgentDir();
|
|
172
172
|
const sessionDir = join(configDir, "sessions", safePath);
|
|
173
173
|
if (!existsSync(sessionDir)) {
|
|
@@ -242,9 +242,9 @@ export class SessionManager {
|
|
|
242
242
|
this.pendingEntries = [];
|
|
243
243
|
// Write to file only if enabled
|
|
244
244
|
if (this.enabled) {
|
|
245
|
-
appendFileSync(this.sessionFile, JSON.stringify(entry)
|
|
245
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
246
246
|
for (const memEntry of this.inMemoryEntries.slice(1)) {
|
|
247
|
-
appendFileSync(this.sessionFile, JSON.stringify(memEntry)
|
|
247
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\n`);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -262,7 +262,7 @@ export class SessionManager {
|
|
|
262
262
|
this.inMemoryEntries.push(entry);
|
|
263
263
|
// Write to file only if enabled
|
|
264
264
|
if (this.enabled) {
|
|
265
|
-
appendFileSync(this.sessionFile, JSON.stringify(entry)
|
|
265
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
}
|
|
@@ -280,7 +280,7 @@ export class SessionManager {
|
|
|
280
280
|
this.inMemoryEntries.push(entry);
|
|
281
281
|
// Write to file only if enabled
|
|
282
282
|
if (this.enabled) {
|
|
283
|
-
appendFileSync(this.sessionFile, JSON.stringify(entry)
|
|
283
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
}
|
|
@@ -299,7 +299,7 @@ export class SessionManager {
|
|
|
299
299
|
this.inMemoryEntries.push(entry);
|
|
300
300
|
// Write to file only if enabled
|
|
301
301
|
if (this.enabled) {
|
|
302
|
-
appendFileSync(this.sessionFile, JSON.stringify(entry)
|
|
302
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
@@ -308,7 +308,7 @@ export class SessionManager {
|
|
|
308
308
|
this.inMemoryEntries.push(entry);
|
|
309
309
|
// Write to file only if enabled
|
|
310
310
|
if (this.enabled) {
|
|
311
|
-
appendFileSync(this.sessionFile, JSON.stringify(entry)
|
|
311
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
/**
|
|
@@ -499,7 +499,7 @@ export class SessionManager {
|
|
|
499
499
|
thinkingLevel: state.thinkingLevel,
|
|
500
500
|
branchedFrom: this.sessionFile,
|
|
501
501
|
};
|
|
502
|
-
appendFileSync(newSessionFile, JSON.stringify(entry)
|
|
502
|
+
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
503
503
|
// Write messages up to and including the branch point (if >= 0)
|
|
504
504
|
if (branchFromIndex >= 0) {
|
|
505
505
|
const messagesToWrite = state.messages.slice(0, branchFromIndex + 1);
|
|
@@ -509,7 +509,7 @@ export class SessionManager {
|
|
|
509
509
|
timestamp: new Date().toISOString(),
|
|
510
510
|
message,
|
|
511
511
|
};
|
|
512
|
-
appendFileSync(newSessionFile, JSON.stringify(messageEntry)
|
|
512
|
+
appendFileSync(newSessionFile, `${JSON.stringify(messageEntry)}\n`);
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
return newSessionFile;
|
|
@@ -544,7 +544,7 @@ export class SessionManager {
|
|
|
544
544
|
if (this.enabled) {
|
|
545
545
|
// Write to file
|
|
546
546
|
for (const entry of newEntries) {
|
|
547
|
-
appendFileSync(newSessionFile, JSON.stringify(entry)
|
|
547
|
+
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
548
548
|
}
|
|
549
549
|
return newSessionFile;
|
|
550
550
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// If file doesn't exist, loadSessionId() won't set sessionId, so generate one\n\t\t\tif (!this.sessionId) {\n\t\t\t\tthis.sessionId = uuidv4();\n\t\t\t}\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\t/** Check if session persistence is enabled */\n\tisEnabled(): boolean {\n\t\treturn this.enabled;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = \"--\" + cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\") + \"--\";\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(memEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(messageEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path, or null if in --no-session mode (in-memory only).\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Build new entries list (up to but not including branch point)\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.enabled ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.enabled) {\n\t\t\t// Write to file\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t} else {\n\t\t\t// In-memory mode: replace inMemoryEntries, no file created\n\t\t\tthis.inMemoryEntries = newEntries;\n\t\t\tthis.sessionId = newSessionId;\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// If file doesn't exist, loadSessionId() won't set sessionId, so generate one\n\t\t\tif (!this.sessionId) {\n\t\t\t\tthis.sessionId = uuidv4();\n\t\t\t}\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\t/** Check if session persistence is enabled */\n\tisEnabled(): boolean {\n\t\treturn this.enabled;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(messageEntry)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path, or null if in --no-session mode (in-memory only).\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Build new entries list (up to but not including branch point)\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.enabled ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.enabled) {\n\t\t\t// Write to file\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t} else {\n\t\t\t// In-memory mode: replace inMemoryEntries, no file created\n\t\t\tthis.inMemoryEntries = newEntries;\n\t\t\tthis.sessionId = newSessionId;\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAgCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAatE;AAqED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAYtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAczF","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (
|
|
1
|
+
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAgCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAatE;AAqED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,EAAE,CAYtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAczF","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (_error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (_error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
@@ -126,13 +126,13 @@ function loadCommandsFromDir(dir, source, subdir = "") {
|
|
|
126
126
|
source: sourceStr,
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
catch (
|
|
129
|
+
catch (_error) {
|
|
130
130
|
// Silently skip files that can't be read
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
catch (
|
|
135
|
+
catch (_error) {
|
|
136
136
|
// Silently skip directories that can't be read
|
|
137
137
|
}
|
|
138
138
|
return commands;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY/D;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAe,EAA4D;IACpG,MAAM,WAAW,GAA2B,EAAE,CAAC;IAE/C,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAE5D,8CAA8C;IAC9C,KAAK,MAAM,IAAI,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC5C,IAAI,KAAK,EAAE,CAAC;YACX,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,CAAC;IACF,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAAA,CAClD;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAY;IAC9D,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,OAAO,GAAkB,IAAI,CAAC;IAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAE3B,IAAI,OAAO,EAAE,CAAC;YACb,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACP,OAAO,IAAI,IAAI,CAAC;YACjB,CAAC;QACF,CAAC;aAAM,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACzC,OAAO,GAAG,IAAI,CAAC;QAChB,CAAC;aAAM,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC1C,IAAI,OAAO,EAAE,CAAC;gBACb,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACnB,OAAO,GAAG,EAAE,CAAC;YACd,CAAC;QACF,CAAC;aAAM,CAAC;YACP,OAAO,IAAI,IAAI,CAAC;QACjB,CAAC;IACF,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,IAAc,EAAU;IACvE,IAAI,MAAM,GAAG,OAAO,CAAC;IAErB,kCAAkC;IAClC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAEhD,4CAA4C;IAC5C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAAA,CACzB,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,GAAW,EAAE,MAA0B,EAAE,MAAM,GAAW,EAAE,EAAsB;IAC9G,MAAM,QAAQ,GAAuB,EAAE,CAAC;IAExC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAE1D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAEvC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACzB,4BAA4B;gBAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBAClE,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;YACpE,CAAC;iBAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrF,IAAI,CAAC;oBACJ,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;oBACnD,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;oBAE9D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;oBAE7D,sBAAsB;oBACtB,IAAI,SAAiB,CAAC;oBACtB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;wBACvB,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;oBACpD,CAAC;yBAAM,CAAC;wBACP,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC;oBAC1D,CAAC;oBAED,2DAA2D;oBAC3D,IAAI,WAAW,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,CAAC;oBAChD,IAAI,CAAC,WAAW,EAAE,CAAC;wBAClB,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;wBAClE,IAAI,SAAS,EAAE,CAAC;4BACf,uBAAuB;4BACvB,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;4BACrC,IAAI,SAAS,CAAC,MAAM,GAAG,EAAE;gCAAE,WAAW,IAAI,KAAK,CAAC;wBACjD,CAAC;oBACF,CAAC;oBAED,+BAA+B;oBAC/B,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;oBAEtE,QAAQ,CAAC,IAAI,CAAC;wBACb,IAAI;wBACJ,WAAW;wBACX,OAAO;wBACP,MAAM,EAAE,SAAS;qBACjB,CAAC,CAAC;gBACJ,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAChB,yCAAyC;gBAC1C,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,+CAA+C;IAChD,CAAC;IAED,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,GAAuB;IACvD,MAAM,QAAQ,GAAuB,EAAE,CAAC;IAExC,mEAAmE;IACnE,MAAM,iBAAiB,GAAG,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjE,8DAA8D;IAC9D,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;IAC/E,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC,CAAC;IAErE,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,YAAgC,EAAU;IAC1F,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAClF,MAAM,UAAU,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IAEvE,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;IACzE,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAC1C,OAAO,cAAc,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY/D;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAe,EAA4D;IACpG,MAAM,WAAW,GAA2B,EAAE,CAAC;IAE/C,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAE5D,8CAA8C;IAC9C,KAAK,MAAM,IAAI,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC5C,IAAI,KAAK,EAAE,CAAC;YACX,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,CAAC;IACF,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAAA,CAClD;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAY;IAC9D,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,OAAO,GAAkB,IAAI,CAAC;IAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAE3B,IAAI,OAAO,EAAE,CAAC;YACb,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACP,OAAO,IAAI,IAAI,CAAC;YACjB,CAAC;QACF,CAAC;aAAM,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACzC,OAAO,GAAG,IAAI,CAAC;QAChB,CAAC;aAAM,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC1C,IAAI,OAAO,EAAE,CAAC;gBACb,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACnB,OAAO,GAAG,EAAE,CAAC;YACd,CAAC;QACF,CAAC;aAAM,CAAC;YACP,OAAO,IAAI,IAAI,CAAC;QACjB,CAAC;IACF,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,IAAc,EAAU;IACvE,IAAI,MAAM,GAAG,OAAO,CAAC;IAErB,kCAAkC;IAClC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAEhD,4CAA4C;IAC5C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAAA,CACzB,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,GAAW,EAAE,MAA0B,EAAE,MAAM,GAAW,EAAE,EAAsB;IAC9G,MAAM,QAAQ,GAAuB,EAAE,CAAC;IAExC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAE1D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAEvC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACzB,4BAA4B;gBAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBAClE,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;YACpE,CAAC;iBAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrF,IAAI,CAAC;oBACJ,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;oBACnD,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;oBAE9D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;oBAE7D,sBAAsB;oBACtB,IAAI,SAAiB,CAAC;oBACtB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;wBACvB,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;oBACpD,CAAC;yBAAM,CAAC;wBACP,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC;oBAC1D,CAAC;oBAED,2DAA2D;oBAC3D,IAAI,WAAW,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,CAAC;oBAChD,IAAI,CAAC,WAAW,EAAE,CAAC;wBAClB,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;wBAClE,IAAI,SAAS,EAAE,CAAC;4BACf,uBAAuB;4BACvB,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;4BACrC,IAAI,SAAS,CAAC,MAAM,GAAG,EAAE;gCAAE,WAAW,IAAI,KAAK,CAAC;wBACjD,CAAC;oBACF,CAAC;oBAED,+BAA+B;oBAC/B,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;oBAEtE,QAAQ,CAAC,IAAI,CAAC;wBACb,IAAI;wBACJ,WAAW;wBACX,OAAO;wBACP,MAAM,EAAE,SAAS;qBACjB,CAAC,CAAC;gBACJ,CAAC;gBAAC,OAAO,MAAM,EAAE,CAAC;oBACjB,yCAAyC;gBAC1C,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QACjB,+CAA+C;IAChD,CAAC;IAED,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,GAAuB;IACvD,MAAM,QAAQ,GAAuB,EAAE,CAAC;IAExC,mEAAmE;IACnE,MAAM,iBAAiB,GAAG,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjE,8DAA8D;IAC9D,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;IAC/E,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC,CAAC;IAErE,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,YAAgC,EAAU;IAC1F,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAClF,MAAM,UAAU,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IAEvE,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;IACzE,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAC1C,OAAO,cAAc,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getCommandsDir } from \"../config.js\";\n\n/**\n * Represents a custom slash command loaded from a file\n */\nexport interface FileSlashCommand {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // e.g., \"(user)\", \"(project)\", \"(project:frontend)\"\n}\n\n/**\n * Parse YAML frontmatter from markdown content\n * Returns { frontmatter, content } where content has frontmatter stripped\n */\nfunction parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {\n\tconst frontmatter: Record<string, string> = {};\n\n\tif (!content.startsWith(\"---\")) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst endIndex = content.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, content };\n\t}\n\n\tconst frontmatterBlock = content.slice(4, endIndex);\n\tconst remainingContent = content.slice(endIndex + 4).trim();\n\n\t// Simple YAML parsing - just key: value pairs\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w+):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tfrontmatter[match[1]] = match[2].trim();\n\t\t}\n\t}\n\n\treturn { frontmatter, content: remainingContent };\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in command content\n * Supports $1, $2, ... for positional args and $@ for all args\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $@ with all args joined\n\tresult = result.replace(/\\$@/g, args.join(\" \"));\n\n\t// Replace $1, $2, etc. with positional args\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\treturn result;\n}\n\n/**\n * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands\n */\nfunction loadCommandsFromDir(dir: string, source: \"user\" | \"project\", subdir: string = \"\"): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn commands;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\t// Recurse into subdirectory\n\t\t\t\tconst newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;\n\t\t\t\tcommands.push(...loadCommandsFromDir(fullPath, source, newSubdir));\n\t\t\t} else if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(\".md\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\t\tconst { frontmatter, content } = parseFrontmatter(rawContent);\n\n\t\t\t\t\tconst name = entry.name.slice(0, -3); // Remove .md extension\n\n\t\t\t\t\t// Build source string\n\t\t\t\t\tlet sourceStr: string;\n\t\t\t\t\tif (source === \"user\") {\n\t\t\t\t\t\tsourceStr = subdir ? `(user:${subdir})` : \"(user)\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsourceStr = subdir ? `(project:${subdir})` : \"(project)\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get description from frontmatter or first non-empty line\n\t\t\t\t\tlet description = frontmatter.description || \"\";\n\t\t\t\t\tif (!description) {\n\t\t\t\t\t\tconst firstLine = content.split(\"\\n\").find((line) => line.trim());\n\t\t\t\t\t\tif (firstLine) {\n\t\t\t\t\t\t\t// Truncate if too long\n\t\t\t\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Append source to description\n\t\t\t\t\tdescription = description ? `${description} ${sourceStr}` : sourceStr;\n\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tdescription,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tsource: sourceStr,\n\t\t\t\t\t});\n\t\t\t\t} catch (_error) {\n\t\t\t\t\t// Silently skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch (_error) {\n\t\t// Silently skip directories that can't be read\n\t}\n\n\treturn commands;\n}\n\n/**\n * Load all custom slash commands from:\n * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/\n * 2. Project: ./{CONFIG_DIR_NAME}/commands/\n */\nexport function loadSlashCommands(): FileSlashCommand[] {\n\tconst commands: FileSlashCommand[] = [];\n\n\t// 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/\n\tconst globalCommandsDir = getCommandsDir();\n\tcommands.push(...loadCommandsFromDir(globalCommandsDir, \"user\"));\n\n\t// 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/\n\tconst projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, \"commands\");\n\tcommands.push(...loadCommandsFromDir(projectCommandsDir, \"project\"));\n\n\treturn commands;\n}\n\n/**\n * Expand a slash command if it matches a file-based command.\n * Returns the expanded content or the original text if not a slash command.\n */\nexport function expandSlashCommand(text: string, fileCommands: FileSlashCommand[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst fileCommand = fileCommands.find((cmd) => cmd.name === commandName);\n\tif (fileCommand) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(fileCommand.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAOrD,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAIH,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CA2QjD,CAAC","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, type Stats, statSync } from \"fs\";\nimport path from \"path\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { expandPath } from \"./path-utils.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.js\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\nexport const grepTool: AgentTool<typeof grepSchema> = {\n\tname: \"grep\",\n\tlabel: \"grep\",\n\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\tparameters: grepSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{\n\t\t\tpattern,\n\t\t\tpath: searchDir,\n\t\t\tglob,\n\t\t\tignoreCase,\n\t\t\tliteral,\n\t\t\tcontext,\n\t\t\tlimit,\n\t\t}: {\n\t\t\tpattern: string;\n\t\t\tpath?: string;\n\t\t\tglob?: string;\n\t\t\tignoreCase?: boolean;\n\t\t\tliteral?: boolean;\n\t\t\tcontext?: number;\n\t\t\tlimit?: number;\n\t\t},\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tfn();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst searchPath = path.resolve(expandPath(searchDir || \".\"));\n\t\t\t\t\tlet searchStat: Stats;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsearchStat = statSync(searchPath);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isDirectory = searchStat.isDirectory();\n\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\n\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\tconst getFileLines = (filePath: string): string[] => {\n\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn lines;\n\t\t\t\t\t};\n\n\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\n\t\t\t\t\tif (ignoreCase) {\n\t\t\t\t\t\targs.push(\"--ignore-case\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (literal) {\n\t\t\t\t\t\targs.push(\"--fixed-strings\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (glob) {\n\t\t\t\t\t\targs.push(\"--glob\", glob);\n\t\t\t\t\t}\n\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\trl.close();\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst stopChild = (dueToLimit: boolean = false) => {\n\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\tstopChild();\n\t\t\t\t\t};\n\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t});\n\n\t\t\t\t\tconst formatBlock = (filePath: string, lineNumber: number): string[] => {\n\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\tconst lines = getFileLines(filePath);\n\t\t\t\t\t\tif (!lines.length) {\n\t\t\t\t\t\t\treturn [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\n\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\n\t\t\t\t\t\t\t// Truncate long lines\n\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\tif (wasTruncated) {\n\t\t\t\t\t\t\t\tlinesTruncated = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isMatchLine) {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn block;\n\t\t\t\t\t};\n\n\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\n\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\") {\n\t\t\t\t\t\t\t\toutputLines.push(...formatBlock(filePath, lineNumber));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\t\tcleanup();\n\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte truncation (no line limit since we already have match limit)\n\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\n\t\t\t\t\t\t// Build notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAOrD,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;AAEvB,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAIH,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CA2QjD,CAAC","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, type Stats, statSync } from \"fs\";\nimport path from \"path\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { expandPath } from \"./path-utils.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.js\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\nexport const grepTool: AgentTool<typeof grepSchema> = {\n\tname: \"grep\",\n\tlabel: \"grep\",\n\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\tparameters: grepSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{\n\t\t\tpattern,\n\t\t\tpath: searchDir,\n\t\t\tglob,\n\t\t\tignoreCase,\n\t\t\tliteral,\n\t\t\tcontext,\n\t\t\tlimit,\n\t\t}: {\n\t\t\tpattern: string;\n\t\t\tpath?: string;\n\t\t\tglob?: string;\n\t\t\tignoreCase?: boolean;\n\t\t\tliteral?: boolean;\n\t\t\tcontext?: number;\n\t\t\tlimit?: number;\n\t\t},\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tfn();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst searchPath = path.resolve(expandPath(searchDir || \".\"));\n\t\t\t\t\tlet searchStat: Stats;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsearchStat = statSync(searchPath);\n\t\t\t\t\t} catch (_err) {\n\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isDirectory = searchStat.isDirectory();\n\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\n\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\tconst getFileLines = (filePath: string): string[] => {\n\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn lines;\n\t\t\t\t\t};\n\n\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\n\t\t\t\t\tif (ignoreCase) {\n\t\t\t\t\t\targs.push(\"--ignore-case\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (literal) {\n\t\t\t\t\t\targs.push(\"--fixed-strings\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (glob) {\n\t\t\t\t\t\targs.push(\"--glob\", glob);\n\t\t\t\t\t}\n\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\trl.close();\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst stopChild = (dueToLimit: boolean = false) => {\n\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\tstopChild();\n\t\t\t\t\t};\n\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t});\n\n\t\t\t\t\tconst formatBlock = (filePath: string, lineNumber: number): string[] => {\n\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\tconst lines = getFileLines(filePath);\n\t\t\t\t\t\tif (!lines.length) {\n\t\t\t\t\t\t\treturn [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\n\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\n\t\t\t\t\t\t\t// Truncate long lines\n\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\tif (wasTruncated) {\n\t\t\t\t\t\t\t\tlinesTruncated = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isMatchLine) {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn block;\n\t\t\t\t\t};\n\n\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\n\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\") {\n\t\t\t\t\t\t\t\toutputLines.push(...formatBlock(filePath, lineNumber));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\t\tcleanup();\n\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte truncation (no line limit since we already have match limit)\n\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\n\t\t\t\t\t\t// Build notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|