sidekick-shared 0.27.6 → 0.28.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 +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Converts OpenCode message + parts into SessionEvent format.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode stores messages and their parts as separate JSON/DB records.
|
|
5
|
+
* This module converts them to the SessionEvent format used by
|
|
6
|
+
* SessionMonitor and CLI consumers, enabling all existing event processing
|
|
7
|
+
* logic to work unchanged with OpenCode sessions.
|
|
8
|
+
*
|
|
9
|
+
* Moved from sidekick-vscode/src/services/providers/OpenCodeMessageParser.ts
|
|
10
|
+
*
|
|
11
|
+
* @module parsers/openCodeParser
|
|
12
|
+
*/
|
|
13
|
+
import { normalizeProviderUsage } from '../usageNormalization';
|
|
14
|
+
/**
|
|
15
|
+
* OpenCode → Claude Code tool name mapping.
|
|
16
|
+
* OpenCode uses lowercase names; downstream code expects PascalCase.
|
|
17
|
+
*/
|
|
18
|
+
const TOOL_NAME_MAP = {
|
|
19
|
+
read: 'Read',
|
|
20
|
+
write: 'Write',
|
|
21
|
+
edit: 'Edit',
|
|
22
|
+
multiedit: 'MultiEdit',
|
|
23
|
+
bash: 'Bash',
|
|
24
|
+
glob: 'Glob',
|
|
25
|
+
grep: 'Grep',
|
|
26
|
+
webfetch: 'WebFetch',
|
|
27
|
+
websearch: 'WebSearch',
|
|
28
|
+
task: 'Task',
|
|
29
|
+
todowrite: 'TodoWrite',
|
|
30
|
+
todoread: 'TodoRead',
|
|
31
|
+
apply_patch: 'ApplyPatch',
|
|
32
|
+
list: 'List',
|
|
33
|
+
lsp: 'Lsp',
|
|
34
|
+
question: 'AskUserQuestion',
|
|
35
|
+
skill: 'Skill',
|
|
36
|
+
plan_enter: 'EnterPlanMode',
|
|
37
|
+
plan_exit: 'ExitPlanMode',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes an OpenCode tool name to PascalCase.
|
|
41
|
+
*
|
|
42
|
+
* Uses the known mapping table for common tools, and falls back to
|
|
43
|
+
* capitalizing the first letter for unknown names.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeToolName(name) {
|
|
46
|
+
if (!name)
|
|
47
|
+
return name;
|
|
48
|
+
// Check mapping (case-insensitive lookup via lowercase key)
|
|
49
|
+
const mapped = TOOL_NAME_MAP[name.toLowerCase()];
|
|
50
|
+
if (mapped)
|
|
51
|
+
return mapped;
|
|
52
|
+
// Already PascalCase (starts with uppercase)? Pass through.
|
|
53
|
+
if (name[0] >= 'A' && name[0] <= 'Z')
|
|
54
|
+
return name;
|
|
55
|
+
// Fallback: capitalize first letter
|
|
56
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Detects plan mode enter/exit from assistant text content.
|
|
60
|
+
*
|
|
61
|
+
* OpenCode models mention plan mode in text/thinking blocks but never emit
|
|
62
|
+
* plan_enter/plan_exit tool invocations. This heuristic detects those
|
|
63
|
+
* mentions and returns the appropriate direction.
|
|
64
|
+
*
|
|
65
|
+
* @param text - Text content to scan for plan mode mentions
|
|
66
|
+
* @returns 'enter' if entering plan mode, 'exit' if leaving, null if no match
|
|
67
|
+
*/
|
|
68
|
+
export function detectPlanModeFromText(text) {
|
|
69
|
+
// Check exit first (more specific patterns)
|
|
70
|
+
if (/(exiting|leaving|deactivat(?:e|ing)|left) plan mode/i.test(text) ||
|
|
71
|
+
/plan mode (?:is )?(?:off|disabled|ended|complete)/i.test(text)) {
|
|
72
|
+
return 'exit';
|
|
73
|
+
}
|
|
74
|
+
// Check enter
|
|
75
|
+
if (/(entering|switch(?:ed|ing)? to|activat(?:e|ing)|now in) plan mode/i.test(text) ||
|
|
76
|
+
/plan mode (?:is )?(?:active|enabled|on)/i.test(text)) {
|
|
77
|
+
return 'enter';
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalizes tool input fields for downstream compatibility.
|
|
83
|
+
*
|
|
84
|
+
* OpenCode uses camelCase field names (e.g. `filePath`); downstream code
|
|
85
|
+
* (MindMapDataService, SessionMonitor) expects snake_case (e.g. `file_path`).
|
|
86
|
+
* This adds snake_case aliases without removing the originals.
|
|
87
|
+
*/
|
|
88
|
+
export function normalizeToolInput(input) {
|
|
89
|
+
const result = { ...input };
|
|
90
|
+
// filePath → file_path (used by MindMapDataService.extractFiles, SessionMonitor.extractToolContext)
|
|
91
|
+
if ('filePath' in result && !('file_path' in result)) {
|
|
92
|
+
result.file_path = result.filePath;
|
|
93
|
+
}
|
|
94
|
+
// oldString → old_string (used by lineChangeCalculator, TempFilesTreeProvider, ToolInspectorProvider)
|
|
95
|
+
if ('oldString' in result && !('old_string' in result)) {
|
|
96
|
+
result.old_string = result.oldString;
|
|
97
|
+
}
|
|
98
|
+
// newString → new_string (used by lineChangeCalculator, TempFilesTreeProvider, ToolInspectorProvider)
|
|
99
|
+
if ('newString' in result && !('new_string' in result)) {
|
|
100
|
+
result.new_string = result.newString;
|
|
101
|
+
}
|
|
102
|
+
// replaceAll → replace_all (used by ToolInspectorProvider)
|
|
103
|
+
if ('replaceAll' in result && !('replace_all' in result)) {
|
|
104
|
+
result.replace_all = result.replaceAll;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Converts a timestamp value (ISO string or Unix ms) to ISO string.
|
|
110
|
+
*/
|
|
111
|
+
function toISOString(time) {
|
|
112
|
+
if (!time)
|
|
113
|
+
return new Date().toISOString();
|
|
114
|
+
if (typeof time === 'string')
|
|
115
|
+
return time;
|
|
116
|
+
return new Date(time).toISOString();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Converts an OpenCode message and its parts into SessionEvent array.
|
|
120
|
+
*
|
|
121
|
+
* Maps OpenCode's message/part model to the flat event stream that
|
|
122
|
+
* SessionMonitor expects. A single OpenCode message may produce multiple
|
|
123
|
+
* events (assistant message, tool results, compaction markers).
|
|
124
|
+
*
|
|
125
|
+
* @param message - OpenCode MessageV2 object
|
|
126
|
+
* @param parts - Array of OpenCode parts belonging to this message
|
|
127
|
+
* @returns Array of SessionEvent objects
|
|
128
|
+
*/
|
|
129
|
+
export function convertOpenCodeMessage(message, parts) {
|
|
130
|
+
const events = [];
|
|
131
|
+
// Sort parts by index if available
|
|
132
|
+
const sortedParts = [...parts].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
133
|
+
if (message.role === 'user') {
|
|
134
|
+
events.push(...convertUserMessage(message, sortedParts));
|
|
135
|
+
}
|
|
136
|
+
else if (message.role === 'assistant') {
|
|
137
|
+
events.push(...convertAssistantMessage(message, sortedParts));
|
|
138
|
+
}
|
|
139
|
+
return events;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Converts a user-role message to events.
|
|
143
|
+
*/
|
|
144
|
+
function convertUserMessage(message, parts) {
|
|
145
|
+
const events = [];
|
|
146
|
+
const content = [];
|
|
147
|
+
for (const part of parts) {
|
|
148
|
+
if (part.type === 'text') {
|
|
149
|
+
content.push({ type: 'text', text: part.text });
|
|
150
|
+
}
|
|
151
|
+
else if (part.type === 'file') {
|
|
152
|
+
content.push({
|
|
153
|
+
type: 'text',
|
|
154
|
+
text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (part.type === 'subtask') {
|
|
158
|
+
content.push({ type: 'text', text: `[Subtask: ${part.description || 'unknown'}]` });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (content.length > 0) {
|
|
162
|
+
events.push({
|
|
163
|
+
type: 'user',
|
|
164
|
+
message: { role: 'user', id: message.id, content },
|
|
165
|
+
timestamp: toISOString(message.time.created),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return events;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Converts an assistant-role message to events.
|
|
172
|
+
*
|
|
173
|
+
* Produces:
|
|
174
|
+
* 1. An assistant event with text, thinking, and tool_use content blocks
|
|
175
|
+
* 2. Synthetic user events for completed tool results
|
|
176
|
+
* 3. A summary event if compaction was detected
|
|
177
|
+
*/
|
|
178
|
+
function convertAssistantMessage(message, parts) {
|
|
179
|
+
const events = [];
|
|
180
|
+
const content = [];
|
|
181
|
+
for (const part of parts) {
|
|
182
|
+
switch (part.type) {
|
|
183
|
+
case 'text':
|
|
184
|
+
content.push({ type: 'text', text: part.text });
|
|
185
|
+
break;
|
|
186
|
+
case 'reasoning':
|
|
187
|
+
content.push({ type: 'thinking', thinking: part.text });
|
|
188
|
+
break;
|
|
189
|
+
case 'tool-invocation':
|
|
190
|
+
content.push({
|
|
191
|
+
type: 'tool_use',
|
|
192
|
+
id: part.callID,
|
|
193
|
+
name: normalizeToolName(part.tool),
|
|
194
|
+
input: normalizeToolInput(part.state.input || {}),
|
|
195
|
+
});
|
|
196
|
+
break;
|
|
197
|
+
case 'tool':
|
|
198
|
+
content.push({
|
|
199
|
+
type: 'tool_use',
|
|
200
|
+
id: part.callID,
|
|
201
|
+
name: normalizeToolName(part.tool),
|
|
202
|
+
input: normalizeToolInput(part.state.input || {}),
|
|
203
|
+
});
|
|
204
|
+
break;
|
|
205
|
+
case 'patch':
|
|
206
|
+
// Emit per-file synthetic Write tool_use blocks so downstream code
|
|
207
|
+
// (TempFilesTreeProvider, MindMapDataService) picks up file touches
|
|
208
|
+
// via the standard file_path field extraction.
|
|
209
|
+
if (part.files && part.files.length > 0) {
|
|
210
|
+
for (let i = 0; i < part.files.length; i++) {
|
|
211
|
+
content.push({
|
|
212
|
+
type: 'tool_use',
|
|
213
|
+
id: `patch-${part.id}-${i}`,
|
|
214
|
+
name: 'Write',
|
|
215
|
+
input: { file_path: part.files[i] },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
content.push({
|
|
221
|
+
type: 'tool_use',
|
|
222
|
+
id: `patch-${part.id}`,
|
|
223
|
+
name: 'Patch',
|
|
224
|
+
input: { hash: part.hash, files: part.files },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case 'step-start':
|
|
229
|
+
case 'step-finish':
|
|
230
|
+
// Timeline markers — no content to emit
|
|
231
|
+
break;
|
|
232
|
+
case 'subtask':
|
|
233
|
+
content.push({
|
|
234
|
+
type: 'tool_use',
|
|
235
|
+
id: `subtask-${part.id}`,
|
|
236
|
+
name: 'Subtask',
|
|
237
|
+
input: {
|
|
238
|
+
description: part.description,
|
|
239
|
+
agent: part.agent,
|
|
240
|
+
model: part.model,
|
|
241
|
+
prompt: part.prompt,
|
|
242
|
+
command: part.command,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
case 'file':
|
|
247
|
+
content.push({
|
|
248
|
+
type: 'text',
|
|
249
|
+
text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
case 'retry':
|
|
253
|
+
content.push({
|
|
254
|
+
type: 'text',
|
|
255
|
+
text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]`,
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
case 'agent':
|
|
259
|
+
case 'snapshot':
|
|
260
|
+
// Metadata only — no user-visible content
|
|
261
|
+
break;
|
|
262
|
+
case 'compaction':
|
|
263
|
+
// Will add summary event below
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Surface message-level errors as text blocks so they appear in the timeline.
|
|
268
|
+
// OpenCode messages can carry structured errors (AuthError, APIError, etc.)
|
|
269
|
+
if (message.error) {
|
|
270
|
+
const errType = message.error.type || 'Error';
|
|
271
|
+
const errMsg = message.error.message || 'Unknown error';
|
|
272
|
+
const errCode = message.error.code ? ` (code: ${message.error.code})` : '';
|
|
273
|
+
content.push({
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: `[${errType}]${errCode} ${errMsg}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Detect plan mode mentions in text/thinking blocks and inject synthetic
|
|
279
|
+
// tool_use blocks (OpenCode doesn't emit plan_enter/plan_exit tools).
|
|
280
|
+
const hasExistingPlanMode = content.some((b) => {
|
|
281
|
+
const block = b;
|
|
282
|
+
return (block.type === 'tool_use' && (block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode'));
|
|
283
|
+
});
|
|
284
|
+
if (!hasExistingPlanMode) {
|
|
285
|
+
for (const block of content) {
|
|
286
|
+
const b = block;
|
|
287
|
+
let text = null;
|
|
288
|
+
if (b.type === 'text')
|
|
289
|
+
text = b.text;
|
|
290
|
+
else if (b.type === 'thinking')
|
|
291
|
+
text = b.thinking;
|
|
292
|
+
if (!text)
|
|
293
|
+
continue;
|
|
294
|
+
const direction = detectPlanModeFromText(text);
|
|
295
|
+
if (direction) {
|
|
296
|
+
const toolName = direction === 'enter' ? 'EnterPlanMode' : 'ExitPlanMode';
|
|
297
|
+
const rawName = direction === 'enter' ? 'plan_enter' : 'plan_exit';
|
|
298
|
+
content.push({
|
|
299
|
+
type: 'tool_use',
|
|
300
|
+
id: `plan-${direction}-${message.id}`,
|
|
301
|
+
name: toolName,
|
|
302
|
+
input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName },
|
|
303
|
+
});
|
|
304
|
+
break; // Only inject one plan mode block per message
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Sum cost from step-finish parts as fallback for reported_cost
|
|
309
|
+
// when the message-level cost is unavailable. Step-finish parts carry
|
|
310
|
+
// per-step cost from the provider which can improve accuracy.
|
|
311
|
+
let stepFinishCost = 0;
|
|
312
|
+
for (const part of parts) {
|
|
313
|
+
if (part.type === 'step-finish' && part.cost && part.cost > 0) {
|
|
314
|
+
stepFinishCost += part.cost;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Build usage data from message tokens
|
|
318
|
+
const reportedCost = typeof message.cost === 'number' && Number.isFinite(message.cost) && message.cost >= 0
|
|
319
|
+
? message.cost
|
|
320
|
+
: stepFinishCost > 0
|
|
321
|
+
? stepFinishCost
|
|
322
|
+
: undefined;
|
|
323
|
+
const usage = {
|
|
324
|
+
input_tokens: message.tokens.input || 0,
|
|
325
|
+
output_tokens: message.tokens.output || 0,
|
|
326
|
+
cache_creation_input_tokens: message.tokens.cacheWrite || 0,
|
|
327
|
+
cache_read_input_tokens: message.tokens.cacheRead || 0,
|
|
328
|
+
reported_cost: reportedCost,
|
|
329
|
+
reasoning_tokens: message.tokens.reasoning || 0,
|
|
330
|
+
};
|
|
331
|
+
const normalizedUsage = normalizeProviderUsage({
|
|
332
|
+
semantics: message.providerID === 'openai'
|
|
333
|
+
? 'openai'
|
|
334
|
+
: message.providerID === 'anthropic'
|
|
335
|
+
? 'anthropic'
|
|
336
|
+
: 'sidekick',
|
|
337
|
+
provider: message.providerID ?? 'opencode',
|
|
338
|
+
source: 'opencode-message',
|
|
339
|
+
model: message.modelID,
|
|
340
|
+
inputTokens: message.tokens.input,
|
|
341
|
+
outputTokens: message.tokens.output,
|
|
342
|
+
cacheReadTokens: message.tokens.cacheRead,
|
|
343
|
+
cacheWriteTokens: message.tokens.cacheWrite,
|
|
344
|
+
reasoningTokens: message.tokens.reasoning,
|
|
345
|
+
reasoningIncludedInOutput: message.providerID === 'openai',
|
|
346
|
+
reportedCostUsd: reportedCost,
|
|
347
|
+
});
|
|
348
|
+
const timestamp = toISOString(message.time.completed || message.time.created);
|
|
349
|
+
const retryAttempts = parts
|
|
350
|
+
.filter((part) => part.type === 'retry')
|
|
351
|
+
.map((part) => part.attempt)
|
|
352
|
+
.filter((attempt) => typeof attempt === 'number');
|
|
353
|
+
const finishReasons = parts
|
|
354
|
+
.filter((part) => part.type === 'step-finish')
|
|
355
|
+
.map((part) => part.reason)
|
|
356
|
+
.filter((reason) => typeof reason === 'string' && reason.length > 0);
|
|
357
|
+
// Emit assistant event (even if content is empty, usage data is valuable)
|
|
358
|
+
if (content.length > 0 || message.tokens.input > 0 || message.tokens.output > 0) {
|
|
359
|
+
events.push({
|
|
360
|
+
type: 'assistant',
|
|
361
|
+
message: {
|
|
362
|
+
role: 'assistant',
|
|
363
|
+
id: message.id,
|
|
364
|
+
model: message.modelID,
|
|
365
|
+
usage,
|
|
366
|
+
normalizedUsage,
|
|
367
|
+
content,
|
|
368
|
+
},
|
|
369
|
+
timestamp,
|
|
370
|
+
providerMetadata: {
|
|
371
|
+
providerId: message.providerID ?? 'opencode',
|
|
372
|
+
source: 'opencode-message',
|
|
373
|
+
...(retryAttempts.length > 0 ? { retryAttempts } : {}),
|
|
374
|
+
...(finishReasons.length > 0 ? { finishReasons } : {}),
|
|
375
|
+
...(message.error ? { providerError: message.error } : {}),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Emit synthetic tool_result events for completed tool invocations
|
|
380
|
+
// and subtask parts. In Claude Code's format, tool results come as
|
|
381
|
+
// user messages with tool_result content blocks.
|
|
382
|
+
for (const part of parts) {
|
|
383
|
+
if ((part.type === 'tool-invocation' || part.type === 'tool') &&
|
|
384
|
+
(part.state.status === 'completed' || part.state.status === 'error')) {
|
|
385
|
+
// Calculate duration from provider-supplied timestamps when available
|
|
386
|
+
let duration;
|
|
387
|
+
if (part.state.time?.start && part.state.time?.end) {
|
|
388
|
+
const startMs = typeof part.state.time.start === 'number'
|
|
389
|
+
? part.state.time.start
|
|
390
|
+
: new Date(part.state.time.start).getTime();
|
|
391
|
+
const endMs = typeof part.state.time.end === 'number'
|
|
392
|
+
? part.state.time.end
|
|
393
|
+
: new Date(part.state.time.end).getTime();
|
|
394
|
+
if (endMs > startMs) {
|
|
395
|
+
duration = endMs - startMs;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const resultContent = [
|
|
399
|
+
{
|
|
400
|
+
type: 'tool_result',
|
|
401
|
+
tool_use_id: part.callID,
|
|
402
|
+
content: part.state.status === 'error' ? part.state.error : part.state.output,
|
|
403
|
+
is_error: part.state.status === 'error',
|
|
404
|
+
...(duration !== undefined && { duration }),
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
const resultTimestamp = part.state.time?.end ? toISOString(part.state.time.end) : timestamp;
|
|
408
|
+
events.push({
|
|
409
|
+
type: 'user',
|
|
410
|
+
message: {
|
|
411
|
+
role: 'user',
|
|
412
|
+
id: `${message.id}:${part.callID}:result`,
|
|
413
|
+
content: resultContent,
|
|
414
|
+
},
|
|
415
|
+
timestamp: resultTimestamp,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
// Subtask parts get a synthetic tool_result so they appear in the timeline
|
|
419
|
+
if (part.type === 'subtask') {
|
|
420
|
+
events.push({
|
|
421
|
+
type: 'user',
|
|
422
|
+
message: {
|
|
423
|
+
role: 'user',
|
|
424
|
+
id: `${message.id}:subtask-${part.id}:result`,
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: 'tool_result',
|
|
428
|
+
tool_use_id: `subtask-${part.id}`,
|
|
429
|
+
content: part.description || 'Subtask completed',
|
|
430
|
+
is_error: false,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
timestamp,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Detect compaction via summary flag or compaction part
|
|
439
|
+
const hasCompaction = message.summary || parts.some((p) => p.type === 'compaction');
|
|
440
|
+
if (hasCompaction) {
|
|
441
|
+
events.push({
|
|
442
|
+
type: 'summary',
|
|
443
|
+
message: { role: 'assistant', id: `${message.id}:summary`, content: 'Context compacted' },
|
|
444
|
+
timestamp,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return events;
|
|
448
|
+
}
|
|
449
|
+
// --- DB-to-OpenCode adapter functions ---
|
|
450
|
+
/**
|
|
451
|
+
* Parses a database message row's JSON `data` field into an OpenCodeMessage.
|
|
452
|
+
*
|
|
453
|
+
* DB messages store data as a JSON blob with slightly different field layout
|
|
454
|
+
* than the file-based format (e.g., `modelID` is at top level, tokens are
|
|
455
|
+
* nested under `tokens`).
|
|
456
|
+
*/
|
|
457
|
+
export function parseDbMessageData(row) {
|
|
458
|
+
const data = JSON.parse(row.data);
|
|
459
|
+
const time = data.time;
|
|
460
|
+
const tokens = data.tokens;
|
|
461
|
+
const cache = tokens?.cache;
|
|
462
|
+
const summary = data.summary;
|
|
463
|
+
// Parse error field if present (AuthError, APIError, OutputLengthError, etc.)
|
|
464
|
+
const errorData = data.error;
|
|
465
|
+
const error = errorData
|
|
466
|
+
? {
|
|
467
|
+
type: errorData.type || undefined,
|
|
468
|
+
message: errorData.message || undefined,
|
|
469
|
+
code: errorData.code,
|
|
470
|
+
}
|
|
471
|
+
: undefined;
|
|
472
|
+
return {
|
|
473
|
+
id: row.id,
|
|
474
|
+
sessionID: row.session_id,
|
|
475
|
+
role: data.role,
|
|
476
|
+
modelID: data.modelID,
|
|
477
|
+
summary: summary != null ? true : undefined,
|
|
478
|
+
cost: data.cost || undefined,
|
|
479
|
+
providerID: data.providerID || undefined,
|
|
480
|
+
tokens: {
|
|
481
|
+
input: tokens?.input || 0,
|
|
482
|
+
output: tokens?.output || 0,
|
|
483
|
+
cacheRead: cache?.read || 0,
|
|
484
|
+
cacheWrite: cache?.write || 0,
|
|
485
|
+
reasoning: tokens?.reasoning || 0,
|
|
486
|
+
},
|
|
487
|
+
time: {
|
|
488
|
+
created: time?.created || row.time_created,
|
|
489
|
+
completed: time?.completed || undefined,
|
|
490
|
+
},
|
|
491
|
+
error,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Parses a database part row's JSON `data` field into an OpenCodePart.
|
|
496
|
+
*/
|
|
497
|
+
export function parseDbPartData(row) {
|
|
498
|
+
const data = JSON.parse(row.data);
|
|
499
|
+
const type = data.type;
|
|
500
|
+
const base = { id: row.id, messageID: row.message_id };
|
|
501
|
+
switch (type) {
|
|
502
|
+
case 'text':
|
|
503
|
+
return { ...base, type: 'text', text: data.text || '' };
|
|
504
|
+
case 'reasoning':
|
|
505
|
+
return { ...base, type: 'reasoning', text: data.text || '' };
|
|
506
|
+
case 'tool': {
|
|
507
|
+
const state = data.state;
|
|
508
|
+
return {
|
|
509
|
+
...base,
|
|
510
|
+
type: 'tool',
|
|
511
|
+
callID: data.callID || '',
|
|
512
|
+
tool: data.tool || '',
|
|
513
|
+
state: {
|
|
514
|
+
status: (state?.status || 'completed'),
|
|
515
|
+
input: state?.input,
|
|
516
|
+
output: state?.output,
|
|
517
|
+
error: state?.error,
|
|
518
|
+
title: state?.title,
|
|
519
|
+
metadata: state?.metadata,
|
|
520
|
+
time: state?.time,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case 'tool-invocation': {
|
|
525
|
+
const state = data.state;
|
|
526
|
+
return {
|
|
527
|
+
...base,
|
|
528
|
+
type: 'tool-invocation',
|
|
529
|
+
callID: data.callID || '',
|
|
530
|
+
tool: data.tool || '',
|
|
531
|
+
state: {
|
|
532
|
+
status: (state?.status || 'completed'),
|
|
533
|
+
input: state?.input,
|
|
534
|
+
output: state?.output,
|
|
535
|
+
error: state?.error,
|
|
536
|
+
time: state?.time,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
case 'compaction':
|
|
541
|
+
return { ...base, type: 'compaction', text: data.text };
|
|
542
|
+
case 'step-start':
|
|
543
|
+
return { ...base, type: 'step-start', snapshot: data.snapshot };
|
|
544
|
+
case 'step-finish': {
|
|
545
|
+
const tokensData = data.tokens;
|
|
546
|
+
const cacheData = tokensData?.cache;
|
|
547
|
+
return {
|
|
548
|
+
...base,
|
|
549
|
+
type: 'step-finish',
|
|
550
|
+
reason: data.reason,
|
|
551
|
+
snapshot: data.snapshot,
|
|
552
|
+
cost: data.cost,
|
|
553
|
+
tokens: tokensData
|
|
554
|
+
? {
|
|
555
|
+
input: tokensData.input,
|
|
556
|
+
output: tokensData.output,
|
|
557
|
+
reasoning: tokensData.reasoning,
|
|
558
|
+
cache: cacheData
|
|
559
|
+
? {
|
|
560
|
+
read: cacheData.read,
|
|
561
|
+
write: cacheData.write,
|
|
562
|
+
}
|
|
563
|
+
: undefined,
|
|
564
|
+
}
|
|
565
|
+
: undefined,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
case 'patch':
|
|
569
|
+
return {
|
|
570
|
+
...base,
|
|
571
|
+
type: 'patch',
|
|
572
|
+
hash: data.hash,
|
|
573
|
+
files: data.files,
|
|
574
|
+
};
|
|
575
|
+
case 'subtask':
|
|
576
|
+
return {
|
|
577
|
+
...base,
|
|
578
|
+
type: 'subtask',
|
|
579
|
+
prompt: data.prompt,
|
|
580
|
+
description: data.description,
|
|
581
|
+
agent: data.agent,
|
|
582
|
+
model: data.model,
|
|
583
|
+
command: data.command,
|
|
584
|
+
};
|
|
585
|
+
case 'agent':
|
|
586
|
+
return {
|
|
587
|
+
...base,
|
|
588
|
+
type: 'agent',
|
|
589
|
+
name: data.name,
|
|
590
|
+
source: data.source,
|
|
591
|
+
};
|
|
592
|
+
case 'file':
|
|
593
|
+
return {
|
|
594
|
+
...base,
|
|
595
|
+
type: 'file',
|
|
596
|
+
mime: data.mime,
|
|
597
|
+
filename: data.filename,
|
|
598
|
+
url: data.url,
|
|
599
|
+
};
|
|
600
|
+
case 'retry': {
|
|
601
|
+
const errorData = data.error;
|
|
602
|
+
return {
|
|
603
|
+
...base,
|
|
604
|
+
type: 'retry',
|
|
605
|
+
attempt: data.attempt,
|
|
606
|
+
error: errorData
|
|
607
|
+
? {
|
|
608
|
+
message: errorData.message,
|
|
609
|
+
code: errorData.code,
|
|
610
|
+
}
|
|
611
|
+
: undefined,
|
|
612
|
+
time: data.time,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
case 'snapshot':
|
|
616
|
+
return {
|
|
617
|
+
...base,
|
|
618
|
+
type: 'snapshot',
|
|
619
|
+
snapshot: data.snapshot,
|
|
620
|
+
};
|
|
621
|
+
default:
|
|
622
|
+
// Unknown part type — treat as text with raw JSON
|
|
623
|
+
return { ...base, type: 'text', text: JSON.stringify(data) };
|
|
624
|
+
}
|
|
625
|
+
}
|