@mariozechner/pi-coding-agent 0.25.3 → 0.26.0
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 +18 -0
- package/README.md +47 -5
- package/dist/cli/session-picker.d.ts +2 -2
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +2 -2
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -13
- package/dist/core/agent-session.js.map +1 -1
- 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/custom-tools/loader.d.ts +3 -2
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +5 -4
- package/dist/core/custom-tools/loader.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/hooks/loader.d.ts +2 -5
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +4 -7
- package/dist/core/hooks/loader.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 +5 -4
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +12 -19
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/sdk.d.ts +211 -0
- package/dist/core/sdk.d.ts.map +1 -0
- package/dist/core/sdk.js +462 -0
- package/dist/core/sdk.js.map +1 -0
- package/dist/core/session-manager.d.ts +31 -91
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +188 -353
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +12 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +101 -37
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +7 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +7 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +9 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +12 -9
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts +24 -2
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -16
- package/dist/core/system-prompt.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/index.d.ts +12 -22
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +2 -0
- package/dist/core/tools/index.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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +4 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +142 -312
- package/dist/main.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/session-selector.d.ts +3 -12
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +1 -3
- package/dist/modes/interactive/components/session-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 +7 -6
- 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 +2 -2
- package/dist/utils/shell.js.map +1 -1
- package/docs/sdk.md +819 -0
- package/examples/README.md +29 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +36 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +67 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +37 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +45 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +46 -0
- package/examples/sdk/12-full-control.ts +91 -0
- package/examples/sdk/README.md +138 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport { messageTransformer } from \"./messages.js\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AppMessage): number {\n\tlet chars = 0;\n\n\t// Handle bashExecution messages\n\tif (message.role === \"bashExecution\") {\n\t\tconst bash = message as unknown as { command: string; output: string };\n\t\tchars = bash.command.length + bash.output.length;\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle user messages\n\tif (message.role === \"user\") {\n\t\tconst content = (message as { content: string | Array<{ type: string; text?: string }> }).content;\n\t\tif (typeof content === \"string\") {\n\t\t\tchars = content.length;\n\t\t} else if (Array.isArray(content)) {\n\t\t\tfor (const block of content) {\n\t\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle assistant messages\n\tif (message.role === \"assistant\") {\n\t\tconst assistant = message as AssistantMessage;\n\t\tfor (const block of assistant.content) {\n\t\t\tif (block.type === \"text\") {\n\t\t\t\tchars += block.text.length;\n\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\tchars += block.thinking.length;\n\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle tool results\n\tif (message.role === \"toolResult\") {\n\t\tconst toolResult = message as { content: Array<{ type: string; text?: string }> };\n\t\tfor (const block of toolResult.content) {\n\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\tchars += block.text.length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\t// user, assistant, and bashExecution are valid cut points\n\t\t\t// toolResult must stay with its preceding tool call\n\t\t\tif (role === \"user\" || role === \"assistant\" || role === \"bashExecution\") {\n\t\t\t\tcutPoints.push(i);\n\t\t\t}\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: prompt }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn. \nThis is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.\n\nCreate a handoff summary that captures:\n- What the user originally asked for in this turn\n- Key decisions and progress made early in this turn\n- Important context needed to understand the kept suffix\n\nBe concise. Focus on information needed to understand the retained recent work.`;\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst cutResult = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages for history summary (before the turn that contains the cut point)\n\tconst historyEnd = cutResult.isSplitTurn ? cutResult.turnStartIndex : cutResult.firstKeptEntryIndex;\n\tconst historyMessages: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\thistoryMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Include previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\thistoryMessages.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Extract messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AppMessage[] = [];\n\tif (cutResult.isSplitTurn) {\n\t\tfor (let i = cutResult.turnStartIndex; i < cutResult.firstKeptEntryIndex; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tturnPrefixMessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate summaries (can be parallel if both needed) and merge into one\n\tlet summary: string;\n\n\tif (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {\n\t\t// Generate both summaries in parallel\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\thistoryMessages.length > 0\n\t\t\t\t? generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions)\n\t\t\t\t: Promise.resolve(\"No prior history.\"),\n\t\t\tgenerateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),\n\t\t]);\n\t\t// Merge into single summary\n\t\tsummary = historyResult + \"\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n\" + turnPrefixResult;\n\t} else {\n\t\t// Just generate history summary\n\t\tsummary = await generateSummary(\n\t\t\thistoryMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t);\n\t}\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex: cutResult.firstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix\n\n\tconst transformedMessages = messageTransformer(messages);\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: TURN_PREFIX_SUMMARIZATION_PROMPT }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport { messageTransformer } from \"./messages.js\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.stopReason !== \"error\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AppMessage): number {\n\tlet chars = 0;\n\n\t// Handle bashExecution messages\n\tif (message.role === \"bashExecution\") {\n\t\tconst bash = message as unknown as { command: string; output: string };\n\t\tchars = bash.command.length + bash.output.length;\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle user messages\n\tif (message.role === \"user\") {\n\t\tconst content = (message as { content: string | Array<{ type: string; text?: string }> }).content;\n\t\tif (typeof content === \"string\") {\n\t\t\tchars = content.length;\n\t\t} else if (Array.isArray(content)) {\n\t\t\tfor (const block of content) {\n\t\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle assistant messages\n\tif (message.role === \"assistant\") {\n\t\tconst assistant = message as AssistantMessage;\n\t\tfor (const block of assistant.content) {\n\t\t\tif (block.type === \"text\") {\n\t\t\t\tchars += block.text.length;\n\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\tchars += block.thinking.length;\n\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle tool results\n\tif (message.role === \"toolResult\") {\n\t\tconst toolResult = message as { content: Array<{ type: string; text?: string }> };\n\t\tfor (const block of toolResult.content) {\n\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\tchars += block.text.length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\t// user, assistant, and bashExecution are valid cut points\n\t\t\t// toolResult must stay with its preceding tool call\n\t\t\tif (role === \"user\" || role === \"assistant\" || role === \"bashExecution\") {\n\t\t\t\tcutPoints.push(i);\n\t\t\t}\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: prompt }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn. \nThis is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.\n\nCreate a handoff summary that captures:\n- What the user originally asked for in this turn\n- Key decisions and progress made early in this turn\n- Important context needed to understand the kept suffix\n\nBe concise. Focus on information needed to understand the retained recent work.`;\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst cutResult = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages for history summary (before the turn that contains the cut point)\n\tconst historyEnd = cutResult.isSplitTurn ? cutResult.turnStartIndex : cutResult.firstKeptEntryIndex;\n\tconst historyMessages: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\thistoryMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Include previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\thistoryMessages.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Extract messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AppMessage[] = [];\n\tif (cutResult.isSplitTurn) {\n\t\tfor (let i = cutResult.turnStartIndex; i < cutResult.firstKeptEntryIndex; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tturnPrefixMessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate summaries (can be parallel if both needed) and merge into one\n\tlet summary: string;\n\n\tif (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {\n\t\t// Generate both summaries in parallel\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\thistoryMessages.length > 0\n\t\t\t\t? generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions)\n\t\t\t\t: Promise.resolve(\"No prior history.\"),\n\t\t\tgenerateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),\n\t\t]);\n\t\t// Merge into single summary\n\t\tsummary = `${historyResult}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixResult}`;\n\t} else {\n\t\t// Just generate history summary\n\t\tsummary = await generateSummary(\n\t\t\thistoryMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t);\n\t}\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex: cutResult.firstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix\n\n\tconst transformedMessages = messageTransformer(messages);\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: TURN_PREFIX_SUMMARIZATION_PROMPT }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\treturn response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n}\n"]}
|
|
@@ -11,7 +11,7 @@ import type { CustomToolsLoadResult } from "./types.js";
|
|
|
11
11
|
export declare function loadCustomTools(paths: string[], cwd: string, builtInToolNames: string[]): Promise<CustomToolsLoadResult>;
|
|
12
12
|
/**
|
|
13
13
|
* Discover and load tools from standard locations:
|
|
14
|
-
* 1.
|
|
14
|
+
* 1. agentDir/tools/*.ts (global)
|
|
15
15
|
* 2. cwd/.pi/tools/*.ts (project-local)
|
|
16
16
|
*
|
|
17
17
|
* Plus any explicitly configured paths from settings or CLI.
|
|
@@ -19,6 +19,7 @@ export declare function loadCustomTools(paths: string[], cwd: string, builtInToo
|
|
|
19
19
|
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
|
20
20
|
* @param cwd - Current working directory
|
|
21
21
|
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
22
|
+
* @param agentDir - Agent config directory. Default: from getAgentDir()
|
|
22
23
|
*/
|
|
23
|
-
export declare function discoverAndLoadCustomTools(configuredPaths: string[], cwd: string, builtInToolNames: string[]): Promise<CustomToolsLoadResult>;
|
|
24
|
+
export declare function discoverAndLoadCustomTools(configuredPaths: string[], cwd: string, builtInToolNames: string[], agentDir?: string): Promise<CustomToolsLoadResult>;
|
|
24
25
|
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAEX,qBAAqB,EAKrB,MAAM,YAAY,CAAC;AAmMpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAgCD;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAEX,qBAAqB,EAKrB,MAAM,YAAY,CAAC;AAmMpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA8ChC;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC/C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA2BhC","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
|
|
@@ -250,7 +250,7 @@ function discoverToolsInDir(dir) {
|
|
|
250
250
|
}
|
|
251
251
|
/**
|
|
252
252
|
* Discover and load tools from standard locations:
|
|
253
|
-
* 1.
|
|
253
|
+
* 1. agentDir/tools/*.ts (global)
|
|
254
254
|
* 2. cwd/.pi/tools/*.ts (project-local)
|
|
255
255
|
*
|
|
256
256
|
* Plus any explicitly configured paths from settings or CLI.
|
|
@@ -258,8 +258,9 @@ function discoverToolsInDir(dir) {
|
|
|
258
258
|
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
|
259
259
|
* @param cwd - Current working directory
|
|
260
260
|
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
261
|
+
* @param agentDir - Agent config directory. Default: from getAgentDir()
|
|
261
262
|
*/
|
|
262
|
-
export async function discoverAndLoadCustomTools(configuredPaths, cwd, builtInToolNames) {
|
|
263
|
+
export async function discoverAndLoadCustomTools(configuredPaths, cwd, builtInToolNames, agentDir = getAgentDir()) {
|
|
263
264
|
const allPaths = [];
|
|
264
265
|
const seen = new Set();
|
|
265
266
|
// Helper to add paths without duplicates
|
|
@@ -272,8 +273,8 @@ export async function discoverAndLoadCustomTools(configuredPaths, cwd, builtInTo
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
};
|
|
275
|
-
// 1. Global tools:
|
|
276
|
-
const globalToolsDir = path.join(
|
|
276
|
+
// 1. Global tools: agentDir/tools/
|
|
277
|
+
const globalToolsDir = path.join(agentDir, "tools");
|
|
277
278
|
addPaths(discoverToolsInDir(globalToolsDir));
|
|
278
279
|
// 2. Project-local tools: cwd/.pi/tools/
|
|
279
280
|
const localToolsDir = path.join(cwd, ".pi", "tools");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. ~/.pi/agent/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: ~/.pi/agent/tools/\n\tconst globalToolsDir = path.join(getAgentDir(), \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/custom-tools/loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool loader - loads TypeScript tool modules using jiti.\n */\n\nimport { spawn } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti\";\nimport { getAgentDir } from \"../../config.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type {\n\tCustomToolFactory,\n\tCustomToolsLoadResult,\n\tExecOptions,\n\tExecResult,\n\tLoadedCustomTool,\n\tToolAPI,\n} from \"./types.js\";\n\n// Create require function to resolve module paths at runtime\nconst require = createRequire(import.meta.url);\n\n// Lazily computed aliases - resolved at runtime to handle global installs\nlet _aliases: Record<string, string> | null = null;\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-tui\": require.resolve(\"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": require.resolve(\"@mariozechner/pi-ai\"),\n\t\t\"@sinclair/typebox\": require.resolve(\"@sinclair/typebox\"),\n\t};\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\n/**\n * Resolve tool path.\n * - Absolute paths used as-is\n * - Paths starting with ~ expanded to home directory\n * - Relative paths resolved from cwd\n */\nfunction resolveToolPath(toolPath: string, cwd: string): string {\n\tconst expanded = expandPath(toolPath);\n\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\n\t// Relative paths resolved from cwd\n\treturn path.resolve(cwd, expanded);\n}\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\tcode: code ?? 0,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: stderr || err.message,\n\t\t\t\tcode: 1,\n\t\t\t\tkilled,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/**\n * Create a no-op UI context for headless modes.\n */\nfunction createNoOpUIContext(): HookUIContext {\n\treturn {\n\t\tselect: async () => null,\n\t\tconfirm: async () => false,\n\t\tinput: async () => null,\n\t\tnotify: () => {},\n\t};\n}\n\n/**\n * Load a single tool module using jiti.\n */\nasync function loadTool(\n\ttoolPath: string,\n\tcwd: string,\n\tsharedApi: ToolAPI,\n): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {\n\tconst resolvedPath = resolveToolPath(toolPath, cwd);\n\n\ttry {\n\t\t// Create jiti instance for TypeScript/ESM loading\n\t\t// Use aliases to resolve package imports since tools are loaded from user directories\n\t\t// (e.g. ~/.pi/agent/tools) but import from packages installed with pi-coding-agent\n\t\tconst jiti = createJiti(import.meta.url, {\n\t\t\talias: getAliases(),\n\t\t});\n\n\t\t// Import the module\n\t\tconst module = await jiti.import(resolvedPath, { default: true });\n\t\tconst factory = module as CustomToolFactory;\n\n\t\tif (typeof factory !== \"function\") {\n\t\t\treturn { tools: null, error: \"Tool must export a default function\" };\n\t\t}\n\n\t\t// Call factory with shared API\n\t\tconst result = await factory(sharedApi);\n\n\t\t// Handle single tool or array of tools\n\t\tconst toolsArray = Array.isArray(result) ? result : [result];\n\n\t\tconst loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({\n\t\t\tpath: toolPath,\n\t\t\tresolvedPath,\n\t\t\ttool,\n\t\t}));\n\n\t\treturn { tools: loadedTools, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { tools: null, error: `Failed to load tool: ${message}` };\n\t}\n}\n\n/**\n * Load all tools from configuration.\n * @param paths - Array of tool file paths\n * @param cwd - Current working directory for resolving relative paths\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n */\nexport async function loadCustomTools(\n\tpaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n): Promise<CustomToolsLoadResult> {\n\tconst tools: LoadedCustomTool[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst seenNames = new Set<string>(builtInToolNames);\n\n\t// Shared API object - all tools get the same instance\n\tconst sharedApi: ToolAPI = {\n\t\tcwd,\n\t\texec: (command: string, args: string[], options?: ExecOptions) => execCommand(command, args, cwd, options),\n\t\tui: createNoOpUIContext(),\n\t\thasUI: false,\n\t};\n\n\tfor (const toolPath of paths) {\n\t\tconst { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: toolPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (loadedTools) {\n\t\t\tfor (const loadedTool of loadedTools) {\n\t\t\t\t// Check for name conflicts\n\t\t\t\tif (seenNames.has(loadedTool.tool.name)) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\tpath: toolPath,\n\t\t\t\t\t\terror: `Tool name \"${loadedTool.tool.name}\" conflicts with existing tool`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tseenNames.add(loadedTool.tool.name);\n\t\t\t\ttools.push(loadedTool);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttools,\n\t\terrors,\n\t\tsetUIContext(uiContext, hasUI) {\n\t\t\tsharedApi.ui = uiContext;\n\t\t\tsharedApi.hasUI = hasUI;\n\t\t},\n\t};\n}\n\n/**\n * Discover tool files from a directory.\n * Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).\n */\nfunction discoverToolsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst tools: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\t// Check for index.ts in subdirectory\n\t\t\t\tconst indexPath = path.join(dir, entry.name, \"index.ts\");\n\t\t\t\tif (fs.existsSync(indexPath)) {\n\t\t\t\t\ttools.push(indexPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn tools;\n}\n\n/**\n * Discover and load tools from standard locations:\n * 1. agentDir/tools/*.ts (global)\n * 2. cwd/.pi/tools/*.ts (project-local)\n *\n * Plus any explicitly configured paths from settings or CLI.\n *\n * @param configuredPaths - Explicit paths from settings.json and CLI --tool flags\n * @param cwd - Current working directory\n * @param builtInToolNames - Names of built-in tools to check for conflicts\n * @param agentDir - Agent config directory. Default: from getAgentDir()\n */\nexport async function discoverAndLoadCustomTools(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tbuiltInToolNames: string[],\n\tagentDir: string = getAgentDir(),\n): Promise<CustomToolsLoadResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\t// Helper to add paths without duplicates\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Global tools: agentDir/tools/\n\tconst globalToolsDir = path.join(agentDir, \"tools\");\n\taddPaths(discoverToolsInDir(globalToolsDir));\n\n\t// 2. Project-local tools: cwd/.pi/tools/\n\tconst localToolsDir = path.join(cwd, \".pi\", \"tools\");\n\taddPaths(discoverToolsInDir(localToolsDir));\n\n\t// 3. Explicitly configured paths (can override/add)\n\taddPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));\n\n\treturn loadCustomTools(allPaths, cwd, builtInToolNames);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export-html.d.ts","sourceRoot":"","sources":["../../src/core/export-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAO9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA8xB3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBlH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB7E","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Message, ToolResultMessage, UserMessage } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename } from \"path\";\nimport { APP_NAME, VERSION } from \"../config.js\";\nimport { type BashExecutionMessage, isBashExecutionMessage } from \"./messages.js\";\nimport type { SessionManager } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface MessageEvent {\n\ttype: \"message\";\n\tmessage: Message;\n\ttimestamp?: number;\n}\n\ninterface ModelChangeEvent {\n\ttype: \"model_change\";\n\tprovider: string;\n\tmodelId: string;\n\ttimestamp?: number;\n}\n\ninterface CompactionEvent {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\ttokensBefore: number;\n}\n\ntype SessionEvent = MessageEvent | ModelChangeEvent | CompactionEvent;\n\ninterface ParsedSessionData {\n\tsessionId: string;\n\ttimestamp: string;\n\tsystemPrompt?: string;\n\tmodelsUsed: Set<string>;\n\tmessages: Message[];\n\ttoolResultsMap: Map<string, ToolResultMessage>;\n\tsessionEvents: SessionEvent[];\n\ttokenStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\tcostStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\ttools?: { name: string; description: string }[];\n\tcontextWindow?: number;\n\tisStreamingFormat?: boolean;\n}\n\n// ============================================================================\n// Color scheme (matching TUI)\n// ============================================================================\n\nconst COLORS = {\n\tuserMessageBg: \"rgb(52, 53, 65)\",\n\ttoolPendingBg: \"rgb(40, 40, 50)\",\n\ttoolSuccessBg: \"rgb(40, 50, 40)\",\n\ttoolErrorBg: \"rgb(60, 40, 40)\",\n\tuserBashBg: \"rgb(50, 48, 35)\", // Faint yellow/brown for user-executed bash\n\tuserBashErrorBg: \"rgb(60, 45, 35)\", // Slightly more orange for errors\n\tbodyBg: \"rgb(24, 24, 30)\",\n\tcontainerBg: \"rgb(30, 30, 36)\",\n\ttext: \"rgb(229, 229, 231)\",\n\ttextDim: \"rgb(161, 161, 170)\",\n\tcyan: \"rgb(103, 232, 249)\",\n\tgreen: \"rgb(34, 197, 94)\",\n\tred: \"rgb(239, 68, 68)\",\n\tyellow: \"rgb(234, 179, 8)\",\n};\n\n// ============================================================================\n// Utility functions\n// ============================================================================\n\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nfunction shortenPath(path: string): string {\n\tconst home = homedir();\n\treturn path.startsWith(home) ? \"~\" + path.slice(home.length) : path;\n}\n\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nfunction formatTimestamp(timestamp: number | string | undefined): string {\n\tif (!timestamp) return \"\";\n\tconst date = new Date(typeof timestamp === \"string\" ? timestamp : timestamp);\n\treturn date.toLocaleTimeString(undefined, { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" });\n}\n\nfunction formatExpandableOutput(lines: string[], maxLines: number): string {\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tif (remaining > 0) {\n\t\tlet out = '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\tout += '<div class=\"output-preview\">';\n\t\tfor (const line of displayLines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\tout += \"</div>\";\n\t\tout += '<div class=\"output-full\">';\n\t\tfor (const line of lines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += \"</div></div>\";\n\t\treturn out;\n\t}\n\n\tlet out = '<div class=\"tool-output\">';\n\tfor (const line of displayLines) {\n\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t}\n\tout += \"</div>\";\n\treturn out;\n}\n\n// ============================================================================\n// Parsing functions\n// ============================================================================\n\nfunction parseSessionManagerFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t};\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; [key: string]: unknown };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; [key: string]: unknown };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tswitch (entry.type) {\n\t\t\tcase \"session\":\n\t\t\t\tdata.sessionId = (entry.id as string) || \"unknown\";\n\t\t\t\tdata.timestamp = (entry.timestamp as string) || data.timestamp;\n\t\t\t\tdata.systemPrompt = entry.systemPrompt as string | undefined;\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message\": {\n\t\t\t\tconst message = entry.message as Message;\n\t\t\t\tdata.messages.push(message);\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\tmessage,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\n\t\t\t\tif (message.role === \"toolResult\") {\n\t\t\t\t\tconst toolResult = message as ToolResultMessage;\n\t\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"model_change\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"model_change\",\n\t\t\t\t\tprovider: entry.provider as string,\n\t\t\t\t\tmodelId: entry.modelId as string,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"compaction\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"compaction\",\n\t\t\t\t\ttimestamp: entry.timestamp as string,\n\t\t\t\t\tsummary: entry.summary as string,\n\t\t\t\t\ttokensBefore: entry.tokensBefore as number,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn data;\n}\n\nfunction parseStreamingEventFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tisStreamingFormat: true,\n\t};\n\n\tlet timestampSet = false;\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; message?: Message };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; message?: Message };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (entry.type === \"message_end\" && entry.message) {\n\t\t\tconst msg = entry.message;\n\t\t\tdata.messages.push(msg);\n\t\t\tdata.sessionEvents.push({\n\t\t\t\ttype: \"message\",\n\t\t\t\tmessage: msg,\n\t\t\t\ttimestamp: (msg as { timestamp?: number }).timestamp,\n\t\t\t});\n\n\t\t\tif (msg.role === \"toolResult\") {\n\t\t\t\tconst toolResult = msg as ToolResultMessage;\n\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t} else if (msg.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = msg as AssistantMessage;\n\t\t\t\tif (assistantMsg.model) {\n\t\t\t\t\tconst modelInfo = assistantMsg.provider\n\t\t\t\t\t\t? `${assistantMsg.provider}/${assistantMsg.model}`\n\t\t\t\t\t\t: assistantMsg.model;\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!timestampSet && (msg as { timestamp?: number }).timestamp) {\n\t\t\t\tdata.timestamp = new Date((msg as { timestamp: number }).timestamp).toISOString();\n\t\t\t\ttimestampSet = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tdata.sessionId = `stream-${data.timestamp.replace(/[:.]/g, \"-\")}`;\n\treturn data;\n}\n\nfunction detectFormat(lines: string[]): \"session-manager\" | \"streaming-events\" | \"unknown\" {\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as { type: string };\n\t\t\tif (entry.type === \"session\") return \"session-manager\";\n\t\t\tif (entry.type === \"agent_start\" || entry.type === \"message_start\" || entry.type === \"turn_start\") {\n\t\t\t\treturn \"streaming-events\";\n\t\t\t}\n\t\t} catch {}\n\t}\n\treturn \"unknown\";\n}\n\nfunction parseSessionFile(content: string): ParsedSessionData {\n\tconst lines = content\n\t\t.trim()\n\t\t.split(\"\\n\")\n\t\t.filter((l) => l.trim());\n\n\tif (lines.length === 0) {\n\t\tthrow new Error(\"Empty session file\");\n\t}\n\n\tconst format = detectFormat(lines);\n\tif (format === \"unknown\") {\n\t\tthrow new Error(\"Unknown session file format\");\n\t}\n\n\treturn format === \"session-manager\" ? parseSessionManagerFormat(lines) : parseStreamingEventFormat(lines);\n}\n\n// ============================================================================\n// HTML formatting functions\n// ============================================================================\n\nfunction formatToolExecution(\n\ttoolName: string,\n\targs: Record<string, unknown>,\n\tresult?: ToolResultMessage,\n): { html: string; bgColor: string } {\n\tlet html = \"\";\n\tconst isError = result?.isError || false;\n\tconst bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;\n\n\tconst getTextOutput = (): string => {\n\t\tif (!result) return \"\";\n\t\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\\n\");\n\t};\n\n\tswitch (toolName) {\n\t\tcase \"bash\": {\n\t\t\tconst command = (args?.command as string) || \"\";\n\t\t\thtml = `<div class=\"tool-command\">$ ${escapeHtml(command || \"...\")}</div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 5);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"read\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\tconst offset = args?.offset as number | undefined;\n\t\t\tconst limit = args?.limit as number | undefined;\n\n\t\t\t// Build path display with offset/limit suffix (in yellow color if offset/limit used)\n\t\t\tlet pathHtml = escapeHtml(path || \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathHtml += `<span class=\"line-numbers\" style=\"color: ${COLORS.yellow}\">:${startLine}${endLine ? `-${endLine}` : \"\"}</span>`;\n\t\t\t}\n\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">read</span> <span class=\"tool-path\">${pathHtml}</span></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 10);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"write\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\tconst fileContent = (args?.content as string) || \"\";\n\t\t\tconst lines = fileContent ? fileContent.split(\"\\n\") : [];\n\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">write</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span>`;\n\t\t\tif (lines.length > 10) {\n\t\t\t\thtml += ` <span class=\"line-count\">(${lines.length} lines)</span>`;\n\t\t\t}\n\t\t\thtml += \"</div>\";\n\n\t\t\tif (fileContent) {\n\t\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"edit\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">edit</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\n\t\t\tif (result?.details?.diff) {\n\t\t\t\tconst diffLines = result.details.diff.split(\"\\n\");\n\t\t\t\thtml += '<div class=\"tool-diff\">';\n\t\t\t\tfor (const line of diffLines) {\n\t\t\t\t\tif (line.startsWith(\"+\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-new\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else if (line.startsWith(\"-\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-old\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-context\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tdefault: {\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">${escapeHtml(toolName)}</span></div>`;\n\t\t\thtml += `<div class=\"tool-output\"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { html, bgColor };\n}\n\nfunction formatMessage(message: Message, toolResultsMap: Map<string, ToolResultMessage>): string {\n\tlet html = \"\";\n\tconst timestamp = (message as { timestamp?: number }).timestamp;\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${formatTimestamp(timestamp)}</div>` : \"\";\n\n\t// Handle bash execution messages (user-executed via ! command)\n\tif (isBashExecutionMessage(message)) {\n\t\tconst bashMsg = message as unknown as BashExecutionMessage;\n\t\tconst isError = bashMsg.cancelled || (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null);\n\t\tconst bgColor = isError ? COLORS.userBashErrorBg : COLORS.userBashBg;\n\n\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">`;\n\t\thtml += timestampHtml;\n\t\thtml += `<div class=\"tool-command\">$ ${escapeHtml(bashMsg.command)}</div>`;\n\n\t\tif (bashMsg.output) {\n\t\t\tconst lines = bashMsg.output.split(\"\\n\");\n\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t}\n\n\t\tif (bashMsg.cancelled) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.yellow}\">(cancelled)</div>`;\n\t\t} else if (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.red}\">(exit ${bashMsg.exitCode})</div>`;\n\t\t}\n\n\t\tif (bashMsg.truncated && bashMsg.fullOutputPath) {\n\t\t\thtml += `<div class=\"bash-truncation\" style=\"color: ${COLORS.yellow}\">Output truncated. Full output: ${escapeHtml(bashMsg.fullOutputPath)}</div>`;\n\t\t}\n\n\t\thtml += `</div>`;\n\t\treturn html;\n\t}\n\n\tif (message.role === \"user\") {\n\t\tconst userMsg = message as UserMessage;\n\t\tlet textContent = \"\";\n\n\t\tif (typeof userMsg.content === \"string\") {\n\t\t\ttextContent = userMsg.content;\n\t\t} else {\n\t\t\tconst textBlocks = userMsg.content.filter((c) => c.type === \"text\");\n\t\t\ttextContent = textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\");\n\t\t}\n\n\t\tif (textContent.trim()) {\n\t\t\thtml += `<div class=\"user-message\">${timestampHtml}${escapeHtml(textContent).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t}\n\t} else if (message.role === \"assistant\") {\n\t\tconst assistantMsg = message as AssistantMessage;\n\t\thtml += timestampHtml ? `<div class=\"assistant-message\">${timestampHtml}` : \"\";\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\thtml += `<div class=\"assistant-text\">${escapeHtml(content.text.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\thtml += `<div class=\"thinking-text\">${escapeHtml(content.thinking.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\tconst toolResult = toolResultsMap.get(content.id);\n\t\t\t\tconst { html: toolHtml, bgColor } = formatToolExecution(\n\t\t\t\t\tcontent.name,\n\t\t\t\t\tcontent.arguments as Record<string, unknown>,\n\t\t\t\t\ttoolResult,\n\t\t\t\t);\n\t\t\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">${toolHtml}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tconst hasToolCalls = assistantMsg.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (assistantMsg.stopReason === \"aborted\") {\n\t\t\t\thtml += '<div class=\"error-text\">Aborted</div>';\n\t\t\t} else if (assistantMsg.stopReason === \"error\") {\n\t\t\t\thtml += `<div class=\"error-text\">Error: ${escapeHtml(assistantMsg.errorMessage || \"Unknown error\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tif (timestampHtml) {\n\t\t\thtml += \"</div>\";\n\t\t}\n\t}\n\n\treturn html;\n}\n\nfunction formatModelChange(event: ModelChangeEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst modelInfo = `${event.provider}/${event.modelId}`;\n\treturn `<div class=\"model-change\">${timestampHtml}<div class=\"model-change-text\">Switched to model: <span class=\"model-name\">${escapeHtml(modelInfo)}</span></div></div>`;\n}\n\nfunction formatCompaction(event: CompactionEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst summaryHtml = escapeHtml(event.summary).replace(/\\n/g, \"<br>\");\n\n\treturn `<div class=\"compaction-container\">\n\t\t<div class=\"compaction-header\" onclick=\"this.parentElement.classList.toggle('expanded')\">\n\t\t\t${timestampHtml}\n\t\t\t<div class=\"compaction-header-row\">\n\t\t\t\t<span class=\"compaction-toggle\">▶</span>\n\t\t\t\t<span class=\"compaction-title\">Context compacted from ${event.tokensBefore.toLocaleString()} tokens</span>\n\t\t\t\t<span class=\"compaction-hint\">(click to expand summary)</span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"compaction-content\">\n\t\t\t<div class=\"compaction-summary\">\n\t\t\t\t<div class=\"compaction-summary-header\">Summary sent to model</div>\n\t\t\t\t<div class=\"compaction-summary-content\">${summaryHtml}</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>`;\n}\n\n// ============================================================================\n// HTML generation\n// ============================================================================\n\nfunction generateHtml(data: ParsedSessionData, filename: string): string {\n\tconst userMessages = data.messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = data.messages.filter((m) => m.role === \"assistant\").length;\n\n\tlet toolCallsCount = 0;\n\tfor (const message of data.messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\ttoolCallsCount += (message as AssistantMessage).content.filter((c) => c.type === \"toolCall\").length;\n\t\t}\n\t}\n\n\tconst lastAssistantMessage = data.messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => m.role === \"assistant\" && (m as AssistantMessage).stopReason !== \"aborted\") as\n\t\t| AssistantMessage\n\t\t| undefined;\n\n\tconst contextTokens = lastAssistantMessage\n\t\t? lastAssistantMessage.usage.input +\n\t\t\tlastAssistantMessage.usage.output +\n\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t: 0;\n\n\tconst lastModel = lastAssistantMessage?.model || \"unknown\";\n\tconst lastProvider = lastAssistantMessage?.provider || \"\";\n\tconst lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;\n\n\tconst contextWindow = data.contextWindow || 0;\n\tconst contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;\n\n\tlet messagesHtml = \"\";\n\tfor (const event of data.sessionEvents) {\n\t\tswitch (event.type) {\n\t\t\tcase \"message\":\n\t\t\t\tif (event.message.role !== \"toolResult\") {\n\t\t\t\t\tmessagesHtml += formatMessage(event.message, data.toolResultsMap);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"model_change\":\n\t\t\t\tmessagesHtml += formatModelChange(event);\n\t\t\t\tbreak;\n\t\t\tcase \"compaction\":\n\t\t\t\tmessagesHtml += formatCompaction(event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst systemPromptHtml = data.systemPrompt\n\t\t? `<div class=\"system-prompt\">\n <div class=\"system-prompt-header\">System Prompt</div>\n <div class=\"system-prompt-content\">${escapeHtml(data.systemPrompt)}</div>\n </div>`\n\t\t: \"\";\n\n\tconst toolsHtml = data.tools\n\t\t? `<div class=\"tools-list\">\n <div class=\"tools-header\">Available Tools</div>\n <div class=\"tools-content\">\n ${data.tools.map((tool) => `<div class=\"tool-item\"><span class=\"tool-item-name\">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`).join(\"\")}\n </div>\n </div>`\n\t\t: \"\";\n\n\tconst streamingNotice = data.isStreamingFormat\n\t\t? `<div class=\"streaming-notice\">\n <em>Note: This session was reconstructed from raw agent event logs, which do not contain system prompt or tool definitions.</em>\n </div>`\n\t\t: \"\";\n\n\tconst contextUsageText = contextPercent\n\t\t? `${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}`\n\t\t: `${contextTokens.toLocaleString()} tokens (last turn) - ${escapeHtml(lastModelInfo)}`;\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Session Export - ${escapeHtml(filename)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: ${COLORS.text};\n background: ${COLORS.bodyBg};\n padding: 24px;\n }\n .container { max-width: 700px; margin: 0 auto; }\n .header {\n margin-bottom: 24px;\n padding: 16px;\n background: ${COLORS.containerBg};\n border-radius: 4px;\n }\n .header h1 {\n font-size: 14px;\n font-weight: bold;\n margin-bottom: 12px;\n color: ${COLORS.cyan};\n }\n .header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }\n .info-item { color: ${COLORS.textDim}; display: flex; align-items: baseline; }\n .info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }\n .info-value { color: ${COLORS.text}; flex: 1; }\n .info-value.cost { font-family: 'SF Mono', monospace; }\n .messages { display: flex; flex-direction: column; gap: 16px; }\n .message-timestamp { font-size: 10px; color: ${COLORS.textDim}; margin-bottom: 4px; opacity: 0.8; }\n .user-message {\n background: ${COLORS.userMessageBg};\n padding: 12px 16px;\n border-radius: 4px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .assistant-message { padding: 0; }\n .assistant-text, .thinking-text {\n padding: 12px 16px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .thinking-text { color: ${COLORS.textDim}; font-style: italic; }\n .model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }\n .model-change-text { color: ${COLORS.textDim}; font-size: 11px; }\n .model-name { color: ${COLORS.cyan}; font-weight: bold; }\n .compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }\n .compaction-header { padding: 12px 16px; cursor: pointer; }\n .compaction-header:hover { background: rgba(255, 255, 255, 0.05); }\n .compaction-header-row { display: flex; align-items: center; gap: 8px; }\n .compaction-toggle { color: ${COLORS.cyan}; font-size: 10px; transition: transform 0.2s; }\n .compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }\n .compaction-title { color: ${COLORS.text}; font-weight: bold; }\n .compaction-hint { color: ${COLORS.textDim}; font-size: 11px; }\n .compaction-content { display: none; padding: 0 16px 16px 16px; }\n .compaction-container.expanded .compaction-content { display: block; }\n .compaction-summary { background: rgba(0, 0, 0, 0.2); border-radius: 4px; padding: 12px; }\n .compaction-summary-header { font-weight: bold; color: ${COLORS.cyan}; margin-bottom: 8px; font-size: 11px; }\n .compaction-summary-content { color: ${COLORS.text}; white-space: pre-wrap; word-wrap: break-word; }\n .tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }\n .tool-header, .tool-name { font-weight: bold; }\n .tool-path { color: ${COLORS.cyan}; word-break: break-all; }\n .line-count { color: ${COLORS.textDim}; }\n .tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }\n .tool-output {\n margin-top: 12px;\n color: ${COLORS.textDim};\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n font-family: inherit;\n overflow-x: auto;\n }\n .tool-output > div { line-height: 1.4; }\n .tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .tool-output.expandable { cursor: pointer; }\n .tool-output.expandable:hover { opacity: 0.9; }\n .tool-output.expandable .output-full { display: none; }\n .tool-output.expandable.expanded .output-preview { display: none; }\n .tool-output.expandable.expanded .output-full { display: block; }\n .expand-hint { color: ${COLORS.cyan}; font-style: italic; margin-top: 4px; }\n .system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }\n .system-prompt-header, .tools-header { font-weight: bold; color: ${COLORS.yellow}; margin-bottom: 8px; }\n .system-prompt-content, .tools-content { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }\n .tool-item { margin: 4px 0; }\n .tool-item-name { font-weight: bold; color: ${COLORS.text}; }\n .tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }\n .diff-line-old { color: ${COLORS.red}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-new { color: ${COLORS.green}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-context { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .error-text { color: ${COLORS.red}; padding: 12px 16px; }\n .footer { margin-top: 48px; padding: 20px; text-align: center; color: ${COLORS.textDim}; font-size: 10px; }\n .streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: ${COLORS.textDim}; font-size: 11px; }\n @media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>${APP_NAME} v${VERSION}</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Session:</span><span class=\"info-value\">${escapeHtml(data.sessionId)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Date:</span><span class=\"info-value\">${new Date(data.timestamp).toLocaleString()}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Models:</span><span class=\"info-value\">${\n\t\t\t\t\t\t\tArray.from(data.modelsUsed)\n\t\t\t\t\t\t\t\t.map((m) => escapeHtml(m))\n\t\t\t\t\t\t\t\t.join(\", \") || \"unknown\"\n\t\t\t\t\t\t}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Messages</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">User:</span><span class=\"info-value\">${userMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Assistant:</span><span class=\"info-value\">${assistantMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Tool Calls:</span><span class=\"info-value\">${toolCallsCount}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Tokens & Cost</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Input:</span><span class=\"info-value\">${data.tokenStats.input.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output:</span><span class=\"info-value\">${data.tokenStats.output.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read:</span><span class=\"info-value\">${data.tokenStats.cacheRead.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write:</span><span class=\"info-value\">${data.tokenStats.cacheWrite.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total:</span><span class=\"info-value\">${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Input Cost:</span><span class=\"info-value cost\">$${data.costStats.input.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output Cost:</span><span class=\"info-value cost\">$${data.costStats.output.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheRead.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheWrite.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total Cost:</span><span class=\"info-value cost\"><strong>$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}</strong></span></div>\n <div class=\"info-item\"><span class=\"info-label\">Context Usage:</span><span class=\"info-value\">${contextUsageText}</span></div>\n </div>\n </div>\n\n ${systemPromptHtml}\n ${toolsHtml}\n ${streamingNotice}\n\n <div class=\"messages\">\n ${messagesHtml}\n </div>\n\n <div class=\"footer\">\n Generated by ${APP_NAME} coding-agent on ${new Date().toLocaleString()}\n </div>\n </div>\n</body>\n</html>`;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string {\n\tconst sessionFile = sessionManager.getSessionFile();\n\tconst content = readFileSync(sessionFile, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\t// Enrich with data from AgentState (tools, context window)\n\tdata.tools = state.tools.map((t) => ({ name: t.name, description: t.description }));\n\tdata.contextWindow = state.model?.contextWindow;\n\tif (!data.systemPrompt) {\n\t\tdata.systemPrompt = state.systemPrompt;\n\t}\n\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(sessionFile));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Auto-detects format: session manager format or streaming event format.\n * Used by CLI for exporting arbitrary session files.\n */\nexport function exportFromFile(inputPath: string, outputPath?: string): string {\n\tif (!existsSync(inputPath)) {\n\t\tthrow new Error(`File not found: ${inputPath}`);\n\t}\n\n\tconst content = readFileSync(inputPath, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(inputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(inputPath));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"export-html.d.ts","sourceRoot":"","sources":["../../src/core/export-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAO9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA8xB3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBlH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB7E","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Message, ToolResultMessage, UserMessage } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename } from \"path\";\nimport { APP_NAME, VERSION } from \"../config.js\";\nimport { type BashExecutionMessage, isBashExecutionMessage } from \"./messages.js\";\nimport type { SessionManager } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface MessageEvent {\n\ttype: \"message\";\n\tmessage: Message;\n\ttimestamp?: number;\n}\n\ninterface ModelChangeEvent {\n\ttype: \"model_change\";\n\tprovider: string;\n\tmodelId: string;\n\ttimestamp?: number;\n}\n\ninterface CompactionEvent {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\ttokensBefore: number;\n}\n\ntype SessionEvent = MessageEvent | ModelChangeEvent | CompactionEvent;\n\ninterface ParsedSessionData {\n\tsessionId: string;\n\ttimestamp: string;\n\tsystemPrompt?: string;\n\tmodelsUsed: Set<string>;\n\tmessages: Message[];\n\ttoolResultsMap: Map<string, ToolResultMessage>;\n\tsessionEvents: SessionEvent[];\n\ttokenStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\tcostStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\ttools?: { name: string; description: string }[];\n\tcontextWindow?: number;\n\tisStreamingFormat?: boolean;\n}\n\n// ============================================================================\n// Color scheme (matching TUI)\n// ============================================================================\n\nconst COLORS = {\n\tuserMessageBg: \"rgb(52, 53, 65)\",\n\ttoolPendingBg: \"rgb(40, 40, 50)\",\n\ttoolSuccessBg: \"rgb(40, 50, 40)\",\n\ttoolErrorBg: \"rgb(60, 40, 40)\",\n\tuserBashBg: \"rgb(50, 48, 35)\", // Faint yellow/brown for user-executed bash\n\tuserBashErrorBg: \"rgb(60, 45, 35)\", // Slightly more orange for errors\n\tbodyBg: \"rgb(24, 24, 30)\",\n\tcontainerBg: \"rgb(30, 30, 36)\",\n\ttext: \"rgb(229, 229, 231)\",\n\ttextDim: \"rgb(161, 161, 170)\",\n\tcyan: \"rgb(103, 232, 249)\",\n\tgreen: \"rgb(34, 197, 94)\",\n\tred: \"rgb(239, 68, 68)\",\n\tyellow: \"rgb(234, 179, 8)\",\n};\n\n// ============================================================================\n// Utility functions\n// ============================================================================\n\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nfunction shortenPath(path: string): string {\n\tconst home = homedir();\n\treturn path.startsWith(home) ? `~${path.slice(home.length)}` : path;\n}\n\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nfunction formatTimestamp(timestamp: number | string | undefined): string {\n\tif (!timestamp) return \"\";\n\tconst date = new Date(typeof timestamp === \"string\" ? timestamp : timestamp);\n\treturn date.toLocaleTimeString(undefined, { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" });\n}\n\nfunction formatExpandableOutput(lines: string[], maxLines: number): string {\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tif (remaining > 0) {\n\t\tlet out = '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\tout += '<div class=\"output-preview\">';\n\t\tfor (const line of displayLines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\tout += \"</div>\";\n\t\tout += '<div class=\"output-full\">';\n\t\tfor (const line of lines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += \"</div></div>\";\n\t\treturn out;\n\t}\n\n\tlet out = '<div class=\"tool-output\">';\n\tfor (const line of displayLines) {\n\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t}\n\tout += \"</div>\";\n\treturn out;\n}\n\n// ============================================================================\n// Parsing functions\n// ============================================================================\n\nfunction parseSessionManagerFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t};\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; [key: string]: unknown };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; [key: string]: unknown };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tswitch (entry.type) {\n\t\t\tcase \"session\":\n\t\t\t\tdata.sessionId = (entry.id as string) || \"unknown\";\n\t\t\t\tdata.timestamp = (entry.timestamp as string) || data.timestamp;\n\t\t\t\tdata.systemPrompt = entry.systemPrompt as string | undefined;\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message\": {\n\t\t\t\tconst message = entry.message as Message;\n\t\t\t\tdata.messages.push(message);\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\tmessage,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\n\t\t\t\tif (message.role === \"toolResult\") {\n\t\t\t\t\tconst toolResult = message as ToolResultMessage;\n\t\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"model_change\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"model_change\",\n\t\t\t\t\tprovider: entry.provider as string,\n\t\t\t\t\tmodelId: entry.modelId as string,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"compaction\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"compaction\",\n\t\t\t\t\ttimestamp: entry.timestamp as string,\n\t\t\t\t\tsummary: entry.summary as string,\n\t\t\t\t\ttokensBefore: entry.tokensBefore as number,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn data;\n}\n\nfunction parseStreamingEventFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tisStreamingFormat: true,\n\t};\n\n\tlet timestampSet = false;\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; message?: Message };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; message?: Message };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (entry.type === \"message_end\" && entry.message) {\n\t\t\tconst msg = entry.message;\n\t\t\tdata.messages.push(msg);\n\t\t\tdata.sessionEvents.push({\n\t\t\t\ttype: \"message\",\n\t\t\t\tmessage: msg,\n\t\t\t\ttimestamp: (msg as { timestamp?: number }).timestamp,\n\t\t\t});\n\n\t\t\tif (msg.role === \"toolResult\") {\n\t\t\t\tconst toolResult = msg as ToolResultMessage;\n\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t} else if (msg.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = msg as AssistantMessage;\n\t\t\t\tif (assistantMsg.model) {\n\t\t\t\t\tconst modelInfo = assistantMsg.provider\n\t\t\t\t\t\t? `${assistantMsg.provider}/${assistantMsg.model}`\n\t\t\t\t\t\t: assistantMsg.model;\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!timestampSet && (msg as { timestamp?: number }).timestamp) {\n\t\t\t\tdata.timestamp = new Date((msg as { timestamp: number }).timestamp).toISOString();\n\t\t\t\ttimestampSet = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tdata.sessionId = `stream-${data.timestamp.replace(/[:.]/g, \"-\")}`;\n\treturn data;\n}\n\nfunction detectFormat(lines: string[]): \"session-manager\" | \"streaming-events\" | \"unknown\" {\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as { type: string };\n\t\t\tif (entry.type === \"session\") return \"session-manager\";\n\t\t\tif (entry.type === \"agent_start\" || entry.type === \"message_start\" || entry.type === \"turn_start\") {\n\t\t\t\treturn \"streaming-events\";\n\t\t\t}\n\t\t} catch {}\n\t}\n\treturn \"unknown\";\n}\n\nfunction parseSessionFile(content: string): ParsedSessionData {\n\tconst lines = content\n\t\t.trim()\n\t\t.split(\"\\n\")\n\t\t.filter((l) => l.trim());\n\n\tif (lines.length === 0) {\n\t\tthrow new Error(\"Empty session file\");\n\t}\n\n\tconst format = detectFormat(lines);\n\tif (format === \"unknown\") {\n\t\tthrow new Error(\"Unknown session file format\");\n\t}\n\n\treturn format === \"session-manager\" ? parseSessionManagerFormat(lines) : parseStreamingEventFormat(lines);\n}\n\n// ============================================================================\n// HTML formatting functions\n// ============================================================================\n\nfunction formatToolExecution(\n\ttoolName: string,\n\targs: Record<string, unknown>,\n\tresult?: ToolResultMessage,\n): { html: string; bgColor: string } {\n\tlet html = \"\";\n\tconst isError = result?.isError || false;\n\tconst bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;\n\n\tconst getTextOutput = (): string => {\n\t\tif (!result) return \"\";\n\t\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\\n\");\n\t};\n\n\tswitch (toolName) {\n\t\tcase \"bash\": {\n\t\t\tconst command = (args?.command as string) || \"\";\n\t\t\thtml = `<div class=\"tool-command\">$ ${escapeHtml(command || \"...\")}</div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 5);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"read\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\tconst offset = args?.offset as number | undefined;\n\t\t\tconst limit = args?.limit as number | undefined;\n\n\t\t\t// Build path display with offset/limit suffix (in yellow color if offset/limit used)\n\t\t\tlet pathHtml = escapeHtml(path || \"...\");\n\t\t\tif (offset !== undefined || limit !== undefined) {\n\t\t\t\tconst startLine = offset ?? 1;\n\t\t\t\tconst endLine = limit !== undefined ? startLine + limit - 1 : \"\";\n\t\t\t\tpathHtml += `<span class=\"line-numbers\" style=\"color: ${COLORS.yellow}\">:${startLine}${endLine ? `-${endLine}` : \"\"}</span>`;\n\t\t\t}\n\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">read</span> <span class=\"tool-path\">${pathHtml}</span></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 10);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"write\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\tconst fileContent = (args?.content as string) || \"\";\n\t\t\tconst lines = fileContent ? fileContent.split(\"\\n\") : [];\n\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">write</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span>`;\n\t\t\tif (lines.length > 10) {\n\t\t\t\thtml += ` <span class=\"line-count\">(${lines.length} lines)</span>`;\n\t\t\t}\n\t\t\thtml += \"</div>\";\n\n\t\t\tif (fileContent) {\n\t\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"edit\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">edit</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\n\t\t\tif (result?.details?.diff) {\n\t\t\t\tconst diffLines = result.details.diff.split(\"\\n\");\n\t\t\t\thtml += '<div class=\"tool-diff\">';\n\t\t\t\tfor (const line of diffLines) {\n\t\t\t\t\tif (line.startsWith(\"+\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-new\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else if (line.startsWith(\"-\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-old\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-context\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tdefault: {\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">${escapeHtml(toolName)}</span></div>`;\n\t\t\thtml += `<div class=\"tool-output\"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { html, bgColor };\n}\n\nfunction formatMessage(message: Message, toolResultsMap: Map<string, ToolResultMessage>): string {\n\tlet html = \"\";\n\tconst timestamp = (message as { timestamp?: number }).timestamp;\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${formatTimestamp(timestamp)}</div>` : \"\";\n\n\t// Handle bash execution messages (user-executed via ! command)\n\tif (isBashExecutionMessage(message)) {\n\t\tconst bashMsg = message as unknown as BashExecutionMessage;\n\t\tconst isError = bashMsg.cancelled || (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null);\n\t\tconst bgColor = isError ? COLORS.userBashErrorBg : COLORS.userBashBg;\n\n\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">`;\n\t\thtml += timestampHtml;\n\t\thtml += `<div class=\"tool-command\">$ ${escapeHtml(bashMsg.command)}</div>`;\n\n\t\tif (bashMsg.output) {\n\t\t\tconst lines = bashMsg.output.split(\"\\n\");\n\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t}\n\n\t\tif (bashMsg.cancelled) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.yellow}\">(cancelled)</div>`;\n\t\t} else if (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.red}\">(exit ${bashMsg.exitCode})</div>`;\n\t\t}\n\n\t\tif (bashMsg.truncated && bashMsg.fullOutputPath) {\n\t\t\thtml += `<div class=\"bash-truncation\" style=\"color: ${COLORS.yellow}\">Output truncated. Full output: ${escapeHtml(bashMsg.fullOutputPath)}</div>`;\n\t\t}\n\n\t\thtml += `</div>`;\n\t\treturn html;\n\t}\n\n\tif (message.role === \"user\") {\n\t\tconst userMsg = message as UserMessage;\n\t\tlet textContent = \"\";\n\n\t\tif (typeof userMsg.content === \"string\") {\n\t\t\ttextContent = userMsg.content;\n\t\t} else {\n\t\t\tconst textBlocks = userMsg.content.filter((c) => c.type === \"text\");\n\t\t\ttextContent = textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\");\n\t\t}\n\n\t\tif (textContent.trim()) {\n\t\t\thtml += `<div class=\"user-message\">${timestampHtml}${escapeHtml(textContent).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t}\n\t} else if (message.role === \"assistant\") {\n\t\tconst assistantMsg = message as AssistantMessage;\n\t\thtml += timestampHtml ? `<div class=\"assistant-message\">${timestampHtml}` : \"\";\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\thtml += `<div class=\"assistant-text\">${escapeHtml(content.text.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\thtml += `<div class=\"thinking-text\">${escapeHtml(content.thinking.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\tconst toolResult = toolResultsMap.get(content.id);\n\t\t\t\tconst { html: toolHtml, bgColor } = formatToolExecution(\n\t\t\t\t\tcontent.name,\n\t\t\t\t\tcontent.arguments as Record<string, unknown>,\n\t\t\t\t\ttoolResult,\n\t\t\t\t);\n\t\t\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">${toolHtml}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tconst hasToolCalls = assistantMsg.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (assistantMsg.stopReason === \"aborted\") {\n\t\t\t\thtml += '<div class=\"error-text\">Aborted</div>';\n\t\t\t} else if (assistantMsg.stopReason === \"error\") {\n\t\t\t\thtml += `<div class=\"error-text\">Error: ${escapeHtml(assistantMsg.errorMessage || \"Unknown error\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tif (timestampHtml) {\n\t\t\thtml += \"</div>\";\n\t\t}\n\t}\n\n\treturn html;\n}\n\nfunction formatModelChange(event: ModelChangeEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst modelInfo = `${event.provider}/${event.modelId}`;\n\treturn `<div class=\"model-change\">${timestampHtml}<div class=\"model-change-text\">Switched to model: <span class=\"model-name\">${escapeHtml(modelInfo)}</span></div></div>`;\n}\n\nfunction formatCompaction(event: CompactionEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst summaryHtml = escapeHtml(event.summary).replace(/\\n/g, \"<br>\");\n\n\treturn `<div class=\"compaction-container\">\n\t\t<div class=\"compaction-header\" onclick=\"this.parentElement.classList.toggle('expanded')\">\n\t\t\t${timestampHtml}\n\t\t\t<div class=\"compaction-header-row\">\n\t\t\t\t<span class=\"compaction-toggle\">▶</span>\n\t\t\t\t<span class=\"compaction-title\">Context compacted from ${event.tokensBefore.toLocaleString()} tokens</span>\n\t\t\t\t<span class=\"compaction-hint\">(click to expand summary)</span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"compaction-content\">\n\t\t\t<div class=\"compaction-summary\">\n\t\t\t\t<div class=\"compaction-summary-header\">Summary sent to model</div>\n\t\t\t\t<div class=\"compaction-summary-content\">${summaryHtml}</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>`;\n}\n\n// ============================================================================\n// HTML generation\n// ============================================================================\n\nfunction generateHtml(data: ParsedSessionData, filename: string): string {\n\tconst userMessages = data.messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = data.messages.filter((m) => m.role === \"assistant\").length;\n\n\tlet toolCallsCount = 0;\n\tfor (const message of data.messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\ttoolCallsCount += (message as AssistantMessage).content.filter((c) => c.type === \"toolCall\").length;\n\t\t}\n\t}\n\n\tconst lastAssistantMessage = data.messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => m.role === \"assistant\" && (m as AssistantMessage).stopReason !== \"aborted\") as\n\t\t| AssistantMessage\n\t\t| undefined;\n\n\tconst contextTokens = lastAssistantMessage\n\t\t? lastAssistantMessage.usage.input +\n\t\t\tlastAssistantMessage.usage.output +\n\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t: 0;\n\n\tconst lastModel = lastAssistantMessage?.model || \"unknown\";\n\tconst lastProvider = lastAssistantMessage?.provider || \"\";\n\tconst lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;\n\n\tconst contextWindow = data.contextWindow || 0;\n\tconst contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;\n\n\tlet messagesHtml = \"\";\n\tfor (const event of data.sessionEvents) {\n\t\tswitch (event.type) {\n\t\t\tcase \"message\":\n\t\t\t\tif (event.message.role !== \"toolResult\") {\n\t\t\t\t\tmessagesHtml += formatMessage(event.message, data.toolResultsMap);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"model_change\":\n\t\t\t\tmessagesHtml += formatModelChange(event);\n\t\t\t\tbreak;\n\t\t\tcase \"compaction\":\n\t\t\t\tmessagesHtml += formatCompaction(event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst systemPromptHtml = data.systemPrompt\n\t\t? `<div class=\"system-prompt\">\n <div class=\"system-prompt-header\">System Prompt</div>\n <div class=\"system-prompt-content\">${escapeHtml(data.systemPrompt)}</div>\n </div>`\n\t\t: \"\";\n\n\tconst toolsHtml = data.tools\n\t\t? `<div class=\"tools-list\">\n <div class=\"tools-header\">Available Tools</div>\n <div class=\"tools-content\">\n ${data.tools.map((tool) => `<div class=\"tool-item\"><span class=\"tool-item-name\">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`).join(\"\")}\n </div>\n </div>`\n\t\t: \"\";\n\n\tconst streamingNotice = data.isStreamingFormat\n\t\t? `<div class=\"streaming-notice\">\n <em>Note: This session was reconstructed from raw agent event logs, which do not contain system prompt or tool definitions.</em>\n </div>`\n\t\t: \"\";\n\n\tconst contextUsageText = contextPercent\n\t\t? `${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}`\n\t\t: `${contextTokens.toLocaleString()} tokens (last turn) - ${escapeHtml(lastModelInfo)}`;\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Session Export - ${escapeHtml(filename)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: ${COLORS.text};\n background: ${COLORS.bodyBg};\n padding: 24px;\n }\n .container { max-width: 700px; margin: 0 auto; }\n .header {\n margin-bottom: 24px;\n padding: 16px;\n background: ${COLORS.containerBg};\n border-radius: 4px;\n }\n .header h1 {\n font-size: 14px;\n font-weight: bold;\n margin-bottom: 12px;\n color: ${COLORS.cyan};\n }\n .header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }\n .info-item { color: ${COLORS.textDim}; display: flex; align-items: baseline; }\n .info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }\n .info-value { color: ${COLORS.text}; flex: 1; }\n .info-value.cost { font-family: 'SF Mono', monospace; }\n .messages { display: flex; flex-direction: column; gap: 16px; }\n .message-timestamp { font-size: 10px; color: ${COLORS.textDim}; margin-bottom: 4px; opacity: 0.8; }\n .user-message {\n background: ${COLORS.userMessageBg};\n padding: 12px 16px;\n border-radius: 4px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .assistant-message { padding: 0; }\n .assistant-text, .thinking-text {\n padding: 12px 16px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .thinking-text { color: ${COLORS.textDim}; font-style: italic; }\n .model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }\n .model-change-text { color: ${COLORS.textDim}; font-size: 11px; }\n .model-name { color: ${COLORS.cyan}; font-weight: bold; }\n .compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }\n .compaction-header { padding: 12px 16px; cursor: pointer; }\n .compaction-header:hover { background: rgba(255, 255, 255, 0.05); }\n .compaction-header-row { display: flex; align-items: center; gap: 8px; }\n .compaction-toggle { color: ${COLORS.cyan}; font-size: 10px; transition: transform 0.2s; }\n .compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }\n .compaction-title { color: ${COLORS.text}; font-weight: bold; }\n .compaction-hint { color: ${COLORS.textDim}; font-size: 11px; }\n .compaction-content { display: none; padding: 0 16px 16px 16px; }\n .compaction-container.expanded .compaction-content { display: block; }\n .compaction-summary { background: rgba(0, 0, 0, 0.2); border-radius: 4px; padding: 12px; }\n .compaction-summary-header { font-weight: bold; color: ${COLORS.cyan}; margin-bottom: 8px; font-size: 11px; }\n .compaction-summary-content { color: ${COLORS.text}; white-space: pre-wrap; word-wrap: break-word; }\n .tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }\n .tool-header, .tool-name { font-weight: bold; }\n .tool-path { color: ${COLORS.cyan}; word-break: break-all; }\n .line-count { color: ${COLORS.textDim}; }\n .tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }\n .tool-output {\n margin-top: 12px;\n color: ${COLORS.textDim};\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n font-family: inherit;\n overflow-x: auto;\n }\n .tool-output > div { line-height: 1.4; }\n .tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .tool-output.expandable { cursor: pointer; }\n .tool-output.expandable:hover { opacity: 0.9; }\n .tool-output.expandable .output-full { display: none; }\n .tool-output.expandable.expanded .output-preview { display: none; }\n .tool-output.expandable.expanded .output-full { display: block; }\n .expand-hint { color: ${COLORS.cyan}; font-style: italic; margin-top: 4px; }\n .system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }\n .system-prompt-header, .tools-header { font-weight: bold; color: ${COLORS.yellow}; margin-bottom: 8px; }\n .system-prompt-content, .tools-content { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }\n .tool-item { margin: 4px 0; }\n .tool-item-name { font-weight: bold; color: ${COLORS.text}; }\n .tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }\n .diff-line-old { color: ${COLORS.red}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-new { color: ${COLORS.green}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-context { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .error-text { color: ${COLORS.red}; padding: 12px 16px; }\n .footer { margin-top: 48px; padding: 20px; text-align: center; color: ${COLORS.textDim}; font-size: 10px; }\n .streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: ${COLORS.textDim}; font-size: 11px; }\n @media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>${APP_NAME} v${VERSION}</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Session:</span><span class=\"info-value\">${escapeHtml(data.sessionId)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Date:</span><span class=\"info-value\">${new Date(data.timestamp).toLocaleString()}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Models:</span><span class=\"info-value\">${\n\t\t\t\t\t\t\tArray.from(data.modelsUsed)\n\t\t\t\t\t\t\t\t.map((m) => escapeHtml(m))\n\t\t\t\t\t\t\t\t.join(\", \") || \"unknown\"\n\t\t\t\t\t\t}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Messages</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">User:</span><span class=\"info-value\">${userMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Assistant:</span><span class=\"info-value\">${assistantMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Tool Calls:</span><span class=\"info-value\">${toolCallsCount}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Tokens & Cost</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Input:</span><span class=\"info-value\">${data.tokenStats.input.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output:</span><span class=\"info-value\">${data.tokenStats.output.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read:</span><span class=\"info-value\">${data.tokenStats.cacheRead.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write:</span><span class=\"info-value\">${data.tokenStats.cacheWrite.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total:</span><span class=\"info-value\">${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Input Cost:</span><span class=\"info-value cost\">$${data.costStats.input.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output Cost:</span><span class=\"info-value cost\">$${data.costStats.output.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheRead.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheWrite.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total Cost:</span><span class=\"info-value cost\"><strong>$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}</strong></span></div>\n <div class=\"info-item\"><span class=\"info-label\">Context Usage:</span><span class=\"info-value\">${contextUsageText}</span></div>\n </div>\n </div>\n\n ${systemPromptHtml}\n ${toolsHtml}\n ${streamingNotice}\n\n <div class=\"messages\">\n ${messagesHtml}\n </div>\n\n <div class=\"footer\">\n Generated by ${APP_NAME} coding-agent on ${new Date().toLocaleString()}\n </div>\n </div>\n</body>\n</html>`;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string {\n\tconst sessionFile = sessionManager.getSessionFile();\n\tconst content = readFileSync(sessionFile, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\t// Enrich with data from AgentState (tools, context window)\n\tdata.tools = state.tools.map((t) => ({ name: t.name, description: t.description }));\n\tdata.contextWindow = state.model?.contextWindow;\n\tif (!data.systemPrompt) {\n\t\tdata.systemPrompt = state.systemPrompt;\n\t}\n\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(sessionFile));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Auto-detects format: session manager format or streaming event format.\n * Used by CLI for exporting arbitrary session files.\n */\nexport function exportFromFile(inputPath: string, outputPath?: string): string {\n\tif (!existsSync(inputPath)) {\n\t\tthrow new Error(`File not found: ${inputPath}`);\n\t}\n\n\tconst content = readFileSync(inputPath, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(inputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(inputPath));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
|
package/dist/core/export-html.js
CHANGED
|
@@ -35,7 +35,7 @@ function escapeHtml(text) {
|
|
|
35
35
|
}
|
|
36
36
|
function shortenPath(path) {
|
|
37
37
|
const home = homedir();
|
|
38
|
-
return path.startsWith(home) ?
|
|
38
|
+
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
39
39
|
}
|
|
40
40
|
function replaceTabs(text) {
|
|
41
41
|
return text.replace(/\t/g, " ");
|