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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plan extraction from session events.
|
|
3
|
+
*
|
|
4
|
+
* Handles all three providers:
|
|
5
|
+
* - Claude Code: EnterPlanMode/ExitPlanMode tool calls with accumulated markdown
|
|
6
|
+
* - OpenCode: <proposed_plan> XML blocks in assistant messages
|
|
7
|
+
* - Codex: UpdatePlan tool calls with structured approach arrays
|
|
8
|
+
*/
|
|
9
|
+
/** Checkbox pattern: `- [ ] text` or `- [x] text` */
|
|
10
|
+
const CHECKBOX_PATTERN = /^[-*]\s+\[([ xX])\]\s+(.+)/;
|
|
11
|
+
/** Numbered list pattern: `1. text` or `1) text` */
|
|
12
|
+
const NUMBERED_PATTERN = /^\d+[.)]\s+(.+)/;
|
|
13
|
+
/** Bullet point pattern: `- Step` or `* Step` (excluding checkboxes) */
|
|
14
|
+
const BULLET_PATTERN = /^[-*]\s+(?!\[[ xX]\])(.+)/;
|
|
15
|
+
/** Phase header pattern: `## Phase 1: Setup` */
|
|
16
|
+
const PHASE_HEADER_PATTERN = /^#{2,4}\s+(?:Phase|Step|Stage)\s*\d*[:.]\s*(.+)/i;
|
|
17
|
+
/** Generic H1/H2 header for title extraction */
|
|
18
|
+
const TITLE_HEADER_PATTERN = /^#{1,2}\s+(.+)/;
|
|
19
|
+
/** Complexity keywords */
|
|
20
|
+
const EXPLICIT_COMPLEXITY_PATTERN = /\[(high|medium|low)\]|\((complex|simple)\)/i;
|
|
21
|
+
const HIGH_COMPLEXITY_KEYWORDS = /\b(refactor|migrate|rewrite|redesign|overhaul|rearchitect)\b/i;
|
|
22
|
+
const LOW_COMPLEXITY_KEYWORDS = /\b(update|fix|tweak|rename|adjust|bump|typo)\b/i;
|
|
23
|
+
function inferComplexity(description) {
|
|
24
|
+
const explicitMatch = description.match(EXPLICIT_COMPLEXITY_PATTERN);
|
|
25
|
+
if (explicitMatch) {
|
|
26
|
+
const marker = (explicitMatch[1] || explicitMatch[2]).toLowerCase();
|
|
27
|
+
if (marker === 'high' || marker === 'complex')
|
|
28
|
+
return 'high';
|
|
29
|
+
if (marker === 'low' || marker === 'simple')
|
|
30
|
+
return 'low';
|
|
31
|
+
return 'medium';
|
|
32
|
+
}
|
|
33
|
+
if (HIGH_COMPLEXITY_KEYWORDS.test(description))
|
|
34
|
+
return 'high';
|
|
35
|
+
if (LOW_COMPLEXITY_KEYWORDS.test(description))
|
|
36
|
+
return 'low';
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses plan markdown into structured plan steps.
|
|
41
|
+
*/
|
|
42
|
+
export function parsePlanMarkdown(markdown) {
|
|
43
|
+
if (!markdown || !markdown.trim())
|
|
44
|
+
return { steps: [] };
|
|
45
|
+
const lines = markdown.split('\n');
|
|
46
|
+
const steps = [];
|
|
47
|
+
let title;
|
|
48
|
+
let currentPhase;
|
|
49
|
+
let stepIndex = 0;
|
|
50
|
+
for (const rawLine of lines) {
|
|
51
|
+
const line = rawLine.trim();
|
|
52
|
+
if (!line)
|
|
53
|
+
continue;
|
|
54
|
+
if (!title) {
|
|
55
|
+
const titleMatch = line.match(TITLE_HEADER_PATTERN);
|
|
56
|
+
if (titleMatch)
|
|
57
|
+
title = titleMatch[1].trim();
|
|
58
|
+
}
|
|
59
|
+
const phaseMatch = line.match(PHASE_HEADER_PATTERN);
|
|
60
|
+
if (phaseMatch) {
|
|
61
|
+
currentPhase = phaseMatch[1].trim();
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const checkboxMatch = line.match(CHECKBOX_PATTERN);
|
|
65
|
+
if (checkboxMatch) {
|
|
66
|
+
const checked = checkboxMatch[1].toLowerCase() === 'x';
|
|
67
|
+
const description = checkboxMatch[2].trim();
|
|
68
|
+
steps.push({
|
|
69
|
+
id: `step-${stepIndex}`,
|
|
70
|
+
description,
|
|
71
|
+
status: checked ? 'completed' : 'pending',
|
|
72
|
+
phase: currentPhase,
|
|
73
|
+
complexity: inferComplexity(description),
|
|
74
|
+
});
|
|
75
|
+
stepIndex++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const numberedMatch = line.match(NUMBERED_PATTERN);
|
|
79
|
+
if (numberedMatch) {
|
|
80
|
+
const description = numberedMatch[1].trim();
|
|
81
|
+
steps.push({
|
|
82
|
+
id: `step-${stepIndex}`,
|
|
83
|
+
description,
|
|
84
|
+
status: 'pending',
|
|
85
|
+
phase: currentPhase,
|
|
86
|
+
complexity: inferComplexity(description),
|
|
87
|
+
});
|
|
88
|
+
stepIndex++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const bulletMatch = line.match(BULLET_PATTERN);
|
|
92
|
+
if (bulletMatch) {
|
|
93
|
+
const raw = bulletMatch[1].trim();
|
|
94
|
+
if (raw.length <= 3)
|
|
95
|
+
continue; // skip trivially short lines
|
|
96
|
+
// Normalize bold-colon patterns: **Setup**: desc → Setup: desc
|
|
97
|
+
const description = raw.replace(/\*\*(.+?)\*\*:\s*/, '$1: ');
|
|
98
|
+
steps.push({
|
|
99
|
+
id: `step-${stepIndex}`,
|
|
100
|
+
description,
|
|
101
|
+
status: 'pending',
|
|
102
|
+
phase: currentPhase,
|
|
103
|
+
complexity: inferComplexity(description),
|
|
104
|
+
});
|
|
105
|
+
stepIndex++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { title, steps };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Extracts <proposed_plan> content from text.
|
|
113
|
+
*/
|
|
114
|
+
export function extractProposedPlan(text) {
|
|
115
|
+
const match = text.match(/<proposed_plan>([\s\S]*?)<\/proposed_plan>/);
|
|
116
|
+
return match ? match[1].trim() : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extracts full untruncated text from event.raw message content blocks.
|
|
120
|
+
* Falls back to event.summary if raw is unavailable.
|
|
121
|
+
*/
|
|
122
|
+
function extractFullTextFromRaw(event) {
|
|
123
|
+
const raw = event.raw;
|
|
124
|
+
const message = raw?.message;
|
|
125
|
+
const content = message?.content;
|
|
126
|
+
if (Array.isArray(content)) {
|
|
127
|
+
const texts = [];
|
|
128
|
+
for (const block of content) {
|
|
129
|
+
const b = block;
|
|
130
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
131
|
+
texts.push(b.text);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (texts.length > 0)
|
|
135
|
+
return texts.join('\n');
|
|
136
|
+
}
|
|
137
|
+
// Fall back to summary (may be truncated, but better than nothing)
|
|
138
|
+
return event.summary || null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stateful plan extractor for use with FollowEvent streams.
|
|
142
|
+
*
|
|
143
|
+
* Handles all three providers:
|
|
144
|
+
* - Claude Code: EnterPlanMode/ExitPlanMode tool calls
|
|
145
|
+
* - OpenCode: <proposed_plan> blocks in assistant text
|
|
146
|
+
* - Codex: UpdatePlan tool calls
|
|
147
|
+
*/
|
|
148
|
+
export class PlanExtractor {
|
|
149
|
+
_plan = null;
|
|
150
|
+
_planModeActive = false;
|
|
151
|
+
_planTexts = [];
|
|
152
|
+
_planFileContent = null;
|
|
153
|
+
_planFilePath = null;
|
|
154
|
+
_readFile;
|
|
155
|
+
constructor(readFile) {
|
|
156
|
+
this._readFile = readFile ?? null;
|
|
157
|
+
}
|
|
158
|
+
get plan() {
|
|
159
|
+
return this._plan;
|
|
160
|
+
}
|
|
161
|
+
reset() {
|
|
162
|
+
this._plan = null;
|
|
163
|
+
this._planModeActive = false;
|
|
164
|
+
this._planTexts = [];
|
|
165
|
+
this._planFileContent = null;
|
|
166
|
+
this._planFilePath = null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Process a FollowEvent and extract plan data if present.
|
|
170
|
+
* Returns true if the plan was updated.
|
|
171
|
+
*/
|
|
172
|
+
processEvent(event) {
|
|
173
|
+
// Codex: UpdatePlan tool calls
|
|
174
|
+
if (event.type === 'tool_use' && event.toolName === 'UpdatePlan') {
|
|
175
|
+
return this.extractCodexPlan(event);
|
|
176
|
+
}
|
|
177
|
+
// Claude Code: EnterPlanMode
|
|
178
|
+
if (event.type === 'tool_use' && event.toolName === 'EnterPlanMode') {
|
|
179
|
+
this._planModeActive = true;
|
|
180
|
+
this._planTexts = [];
|
|
181
|
+
this._planFileContent = null;
|
|
182
|
+
this._planFilePath = null;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
// Claude Code: ExitPlanMode
|
|
186
|
+
if (event.type === 'tool_use' && event.toolName === 'ExitPlanMode') {
|
|
187
|
+
return this.finalizePlanMode();
|
|
188
|
+
}
|
|
189
|
+
// Capture Write/Edit tool calls to plan files during plan mode
|
|
190
|
+
if (this._planModeActive && event.type === 'tool_use') {
|
|
191
|
+
const raw = event.raw;
|
|
192
|
+
const input = extractToolInput(raw);
|
|
193
|
+
const filePath = input?.file_path;
|
|
194
|
+
if (event.toolName === 'Write') {
|
|
195
|
+
const content = input?.content;
|
|
196
|
+
if (filePath && content && filePath.includes('.claude/plans/')) {
|
|
197
|
+
this._planFileContent = content;
|
|
198
|
+
this._planFilePath = filePath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (event.toolName === 'Edit') {
|
|
202
|
+
// Edit contains a diff, not full content — capture path for disk-read fallback
|
|
203
|
+
if (filePath && filePath.includes('.claude/plans/')) {
|
|
204
|
+
this._planFilePath = filePath;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Accumulate text during plan mode (Claude Code)
|
|
209
|
+
// Extract full text from event.raw to avoid truncated summaries
|
|
210
|
+
if (this._planModeActive && event.type === 'assistant') {
|
|
211
|
+
const fullText = extractFullTextFromRaw(event);
|
|
212
|
+
if (fullText) {
|
|
213
|
+
this._planTexts.push(fullText);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// OpenCode / Codex: <proposed_plan> blocks
|
|
217
|
+
// Check raw message content first (untruncated), then fall back to summary
|
|
218
|
+
if (event.type === 'assistant') {
|
|
219
|
+
const raw = event.raw;
|
|
220
|
+
const message = raw?.message;
|
|
221
|
+
const content = message?.content;
|
|
222
|
+
if (Array.isArray(content)) {
|
|
223
|
+
for (const block of content) {
|
|
224
|
+
const b = block;
|
|
225
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
226
|
+
const planText = extractProposedPlan(b.text);
|
|
227
|
+
if (planText) {
|
|
228
|
+
return this.extractFromMarkdown(planText, this.getSource(event));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Fall back to summary (may work for short plans)
|
|
234
|
+
if (event.summary) {
|
|
235
|
+
const proposed = extractProposedPlan(event.summary);
|
|
236
|
+
if (proposed) {
|
|
237
|
+
return this.extractFromMarkdown(proposed, this.getSource(event));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
getSource(event) {
|
|
244
|
+
if (event.providerId === 'opencode')
|
|
245
|
+
return 'opencode';
|
|
246
|
+
if (event.providerId === 'codex')
|
|
247
|
+
return 'codex';
|
|
248
|
+
return 'claude-code';
|
|
249
|
+
}
|
|
250
|
+
extractCodexPlan(event) {
|
|
251
|
+
const raw = event.raw;
|
|
252
|
+
const input = extractToolInput(raw);
|
|
253
|
+
if (!input)
|
|
254
|
+
return false;
|
|
255
|
+
// Codex UpdatePlan can use either approach[] or plan[] format
|
|
256
|
+
const approach = (input.approach ?? input.plan);
|
|
257
|
+
if (!approach || !Array.isArray(approach))
|
|
258
|
+
return false;
|
|
259
|
+
const steps = [];
|
|
260
|
+
for (let i = 0; i < approach.length; i++) {
|
|
261
|
+
const entry = approach[i];
|
|
262
|
+
if (typeof entry === 'string') {
|
|
263
|
+
steps.push({
|
|
264
|
+
id: `step-${i}`,
|
|
265
|
+
description: entry,
|
|
266
|
+
status: 'pending',
|
|
267
|
+
complexity: inferComplexity(entry),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else if (typeof entry === 'object' && entry !== null) {
|
|
271
|
+
const obj = entry;
|
|
272
|
+
const desc = String(obj.step || obj.description || '').trim();
|
|
273
|
+
if (!desc)
|
|
274
|
+
continue;
|
|
275
|
+
const rawStatus = String(obj.status || 'pending').toLowerCase();
|
|
276
|
+
let status;
|
|
277
|
+
if (rawStatus === 'completed')
|
|
278
|
+
status = 'completed';
|
|
279
|
+
else if (rawStatus === 'in_progress' || rawStatus === 'in-progress')
|
|
280
|
+
status = 'in_progress';
|
|
281
|
+
else
|
|
282
|
+
status = 'pending';
|
|
283
|
+
steps.push({
|
|
284
|
+
id: `step-${i}`,
|
|
285
|
+
description: desc,
|
|
286
|
+
status,
|
|
287
|
+
complexity: inferComplexity(desc),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (steps.length > 0) {
|
|
292
|
+
this._plan = {
|
|
293
|
+
title: input.title || 'Plan',
|
|
294
|
+
steps,
|
|
295
|
+
source: 'codex',
|
|
296
|
+
};
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
finalizePlanMode() {
|
|
302
|
+
this._planModeActive = false;
|
|
303
|
+
// Prefer captured Write content, then the final file after Edit, then prose fallback.
|
|
304
|
+
// Edit only contains a diff, so reading the file must happen before accumulated text.
|
|
305
|
+
const markdown = this._planFileContent ||
|
|
306
|
+
(this._planFilePath && this._readFile ? this._readFile(this._planFilePath) : null) ||
|
|
307
|
+
(this._planTexts.length > 0 ? this._planTexts.join('\n') : null);
|
|
308
|
+
this._planFileContent = null;
|
|
309
|
+
this._planFilePath = null;
|
|
310
|
+
this._planTexts = [];
|
|
311
|
+
if (!markdown)
|
|
312
|
+
return false;
|
|
313
|
+
return this.extractFromMarkdown(markdown, 'claude-code');
|
|
314
|
+
}
|
|
315
|
+
extractFromMarkdown(markdown, source) {
|
|
316
|
+
const parsed = parsePlanMarkdown(markdown);
|
|
317
|
+
this._plan = {
|
|
318
|
+
title: parsed.title || 'Plan',
|
|
319
|
+
steps: parsed.steps,
|
|
320
|
+
source,
|
|
321
|
+
rawMarkdown: markdown,
|
|
322
|
+
};
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function extractToolInput(raw) {
|
|
327
|
+
if (!raw)
|
|
328
|
+
return null;
|
|
329
|
+
if (raw.input && typeof raw.input === 'object')
|
|
330
|
+
return raw.input;
|
|
331
|
+
const tool = raw.tool;
|
|
332
|
+
if (tool?.input && typeof tool.input === 'object')
|
|
333
|
+
return tool.input;
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-based session activity detection.
|
|
3
|
+
*
|
|
4
|
+
* Uses heuristics on the tail of a JSONL file to determine if a session
|
|
5
|
+
* is genuinely ongoing (AI thinking/calling tools), ended (final output
|
|
6
|
+
* delivered), or stale (no updates for >5 minutes).
|
|
7
|
+
*
|
|
8
|
+
* Inspired by tail-claude's ending-event classification and grace periods.
|
|
9
|
+
*
|
|
10
|
+
* @module parsers/sessionActivityDetector
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
// ── Constants ──
|
|
14
|
+
/** How many bytes to read from the end of the file for analysis */
|
|
15
|
+
const TAIL_BYTES = 32 * 1024;
|
|
16
|
+
/** Grace period to prevent spinner flicker (ms) */
|
|
17
|
+
export const SESSION_ACTIVITY_GRACE_PERIOD_MS = 5_000;
|
|
18
|
+
const GRACE_PERIOD_MS = SESSION_ACTIVITY_GRACE_PERIOD_MS;
|
|
19
|
+
/** If mtime is older than this, session is stale regardless of content */
|
|
20
|
+
export const SESSION_ACTIVITY_STALENESS_MS = 5 * 60 * 1000;
|
|
21
|
+
const STALENESS_THRESHOLD_MS = SESSION_ACTIVITY_STALENESS_MS;
|
|
22
|
+
/** How many trailing events the in-memory classifier inspects (about a 32 KiB tail). */
|
|
23
|
+
const TAIL_EVENTS = 200;
|
|
24
|
+
// ── Event Classification Patterns ──
|
|
25
|
+
/** Patterns indicating the AI is actively working */
|
|
26
|
+
const AI_ACTIVITY_PATTERNS = [
|
|
27
|
+
'"type":"assistant"',
|
|
28
|
+
'"type":"tool_use"',
|
|
29
|
+
'"type":"tool_result"',
|
|
30
|
+
'"stop_reason":"tool_use"',
|
|
31
|
+
];
|
|
32
|
+
/** Patterns indicating the session has ended (final output delivered) */
|
|
33
|
+
const ENDING_PATTERNS = [
|
|
34
|
+
'"stop_reason":"end_turn"',
|
|
35
|
+
'"type":"result"',
|
|
36
|
+
'"type":"user"', // User typing means AI is done with its turn
|
|
37
|
+
];
|
|
38
|
+
/** Patterns that definitely mean the session is done */
|
|
39
|
+
const TERMINAL_PATTERNS = ['"type":"result"'];
|
|
40
|
+
// ── Public API ──
|
|
41
|
+
/**
|
|
42
|
+
* Determines whether a session is ongoing, ended, or stale.
|
|
43
|
+
*
|
|
44
|
+
* Uses a multi-signal approach:
|
|
45
|
+
* 1. File mtime staleness check (>5min → stale)
|
|
46
|
+
* 2. Content-based: reads last ~32KB and classifies the ending pattern
|
|
47
|
+
* 3. Grace period: prevents flicker by requiring 5s of inactivity before "ended"
|
|
48
|
+
*
|
|
49
|
+
* @param sessionPath - Path to the JSONL session file
|
|
50
|
+
* @returns Activity state with classification reason
|
|
51
|
+
*/
|
|
52
|
+
export function detectSessionActivity(sessionPath) {
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.statSync(sessionPath);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { state: 'ended', lastActivityTime: null, reason: 'file-not-found' };
|
|
59
|
+
}
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const mtimeAge = now - stat.mtimeMs;
|
|
62
|
+
// Staleness check: if file hasn't been touched in >5 minutes, it's stale
|
|
63
|
+
if (mtimeAge > STALENESS_THRESHOLD_MS) {
|
|
64
|
+
return { state: 'stale', lastActivityTime: new Date(stat.mtimeMs), reason: 'mtime-stale' };
|
|
65
|
+
}
|
|
66
|
+
// Read the tail of the file for content analysis
|
|
67
|
+
const tail = readTail(sessionPath, stat.size);
|
|
68
|
+
if (!tail) {
|
|
69
|
+
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'empty-file' };
|
|
70
|
+
}
|
|
71
|
+
// Parse the last few JSONL lines
|
|
72
|
+
const lines = tail.split('\n').filter((l) => l.trim().length > 0);
|
|
73
|
+
if (lines.length === 0) {
|
|
74
|
+
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-events' };
|
|
75
|
+
}
|
|
76
|
+
return classifyFromSignals(signalsFromLines(lines), stat.mtimeMs, mtimeAge);
|
|
77
|
+
}
|
|
78
|
+
function signalsFromLines(lines) {
|
|
79
|
+
let lastAiActivityIndex = -1;
|
|
80
|
+
let lastEndingIndex = -1;
|
|
81
|
+
let hasTerminal = false;
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
for (const pattern of AI_ACTIVITY_PATTERNS) {
|
|
85
|
+
if (line.includes(pattern)) {
|
|
86
|
+
lastAiActivityIndex = i;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const pattern of TERMINAL_PATTERNS) {
|
|
91
|
+
if (line.includes(pattern)) {
|
|
92
|
+
hasTerminal = true;
|
|
93
|
+
lastEndingIndex = i;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!hasTerminal) {
|
|
98
|
+
for (const pattern of ENDING_PATTERNS) {
|
|
99
|
+
if (pattern === '"type":"user"' && line.includes('"type":"tool_result"'))
|
|
100
|
+
continue;
|
|
101
|
+
if (line.includes(pattern)) {
|
|
102
|
+
lastEndingIndex = i;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { lastAiActivityIndex, lastEndingIndex, hasTerminal };
|
|
109
|
+
}
|
|
110
|
+
function hasToolResultBlock(event) {
|
|
111
|
+
const content = event.message?.content;
|
|
112
|
+
return (Array.isArray(content) &&
|
|
113
|
+
content.some((block) => typeof block === 'object' &&
|
|
114
|
+
block !== null &&
|
|
115
|
+
block.type === 'tool_result'));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The same signals the tail scanner derives from raw JSONL lines, read from
|
|
119
|
+
* canonical events instead: assistant, tool-use, and tool-result events (and
|
|
120
|
+
* user events carrying tool results) are AI activity; an assistant turn that
|
|
121
|
+
* stopped with `end_turn` or a user prompt is an ending event.
|
|
122
|
+
*/
|
|
123
|
+
function signalsFromEvents(events) {
|
|
124
|
+
let lastAiActivityIndex = -1;
|
|
125
|
+
let lastEndingIndex = -1;
|
|
126
|
+
for (let i = 0; i < events.length; i++) {
|
|
127
|
+
const event = events[i];
|
|
128
|
+
const carriesToolResult = event.type === 'user' && hasToolResultBlock(event);
|
|
129
|
+
if (event.type === 'assistant' ||
|
|
130
|
+
event.type === 'tool_use' ||
|
|
131
|
+
event.type === 'tool_result' ||
|
|
132
|
+
carriesToolResult) {
|
|
133
|
+
lastAiActivityIndex = i;
|
|
134
|
+
}
|
|
135
|
+
if ((event.type === 'assistant' && event.message?.stop_reason === 'end_turn') ||
|
|
136
|
+
(event.type === 'user' && !carriesToolResult)) {
|
|
137
|
+
lastEndingIndex = i;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { lastAiActivityIndex, lastEndingIndex, hasTerminal: false };
|
|
141
|
+
}
|
|
142
|
+
function classifyFromSignals(signals, mtimeMs, mtimeAge) {
|
|
143
|
+
const lastActivityTime = new Date(mtimeMs);
|
|
144
|
+
const { lastAiActivityIndex, lastEndingIndex, hasTerminal } = signals;
|
|
145
|
+
// Terminal patterns mean the session is definitely done
|
|
146
|
+
if (hasTerminal) {
|
|
147
|
+
return { state: 'ended', lastActivityTime, reason: 'terminal-event' };
|
|
148
|
+
}
|
|
149
|
+
// AI activity after the last ending event -> ongoing
|
|
150
|
+
if (lastAiActivityIndex > lastEndingIndex) {
|
|
151
|
+
return { state: 'ongoing', lastActivityTime, reason: 'ai-activity-after-ending' };
|
|
152
|
+
}
|
|
153
|
+
// Ending event with no subsequent AI activity
|
|
154
|
+
if (lastEndingIndex >= 0) {
|
|
155
|
+
// Apply grace period to prevent flicker
|
|
156
|
+
if (mtimeAge < GRACE_PERIOD_MS) {
|
|
157
|
+
return { state: 'ongoing', lastActivityTime, reason: 'grace-period' };
|
|
158
|
+
}
|
|
159
|
+
return { state: 'ended', lastActivityTime, reason: 'ending-event' };
|
|
160
|
+
}
|
|
161
|
+
// No clear signal: if recently modified, assume ongoing
|
|
162
|
+
if (mtimeAge < GRACE_PERIOD_MS) {
|
|
163
|
+
return { state: 'ongoing', lastActivityTime, reason: 'recent-mtime' };
|
|
164
|
+
}
|
|
165
|
+
return { state: 'ended', lastActivityTime, reason: 'no-activity-signal' };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Classify a session from events already in memory, with the same states,
|
|
169
|
+
* reasons, grace period, and staleness rule as {@link detectSessionActivity},
|
|
170
|
+
* but without opening the file again. Works for every provider (the raw
|
|
171
|
+
* tail patterns only ever matched Claude Code JSONL) and for database-backed
|
|
172
|
+
* sessions that have no file to tail.
|
|
173
|
+
*/
|
|
174
|
+
export function classifySessionActivity(options) {
|
|
175
|
+
const now = options.now ?? Date.now();
|
|
176
|
+
const events = options.events;
|
|
177
|
+
const lastEventMs = events.length > 0 ? Date.parse(events[events.length - 1].timestamp) : NaN;
|
|
178
|
+
const mtimeMs = typeof options.mtimeMs === 'number' && Number.isFinite(options.mtimeMs)
|
|
179
|
+
? options.mtimeMs
|
|
180
|
+
: Number.isFinite(lastEventMs)
|
|
181
|
+
? lastEventMs
|
|
182
|
+
: null;
|
|
183
|
+
if (mtimeMs === null) {
|
|
184
|
+
return { state: 'ended', lastActivityTime: null, reason: 'no-events' };
|
|
185
|
+
}
|
|
186
|
+
const mtimeAge = now - mtimeMs;
|
|
187
|
+
if (mtimeAge > STALENESS_THRESHOLD_MS) {
|
|
188
|
+
return { state: 'stale', lastActivityTime: new Date(mtimeMs), reason: 'mtime-stale' };
|
|
189
|
+
}
|
|
190
|
+
if (events.length === 0) {
|
|
191
|
+
return { state: 'ended', lastActivityTime: new Date(mtimeMs), reason: 'no-events' };
|
|
192
|
+
}
|
|
193
|
+
const tail = events.length > TAIL_EVENTS ? events.slice(-TAIL_EVENTS) : events;
|
|
194
|
+
return classifyFromSignals(signalsFromEvents(tail), mtimeMs, mtimeAge);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Refresh a cached classification from its unchanged fingerprint without
|
|
198
|
+
* re-reading the transcript. An active file remains active until it crosses
|
|
199
|
+
* the same five-minute staleness boundary used by detectSessionActivity,
|
|
200
|
+
* unless it was active only by grace period (`previousReason`).
|
|
201
|
+
*/
|
|
202
|
+
export function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now(), previousReason) {
|
|
203
|
+
if (!Number.isFinite(mtimeMs))
|
|
204
|
+
return previous;
|
|
205
|
+
if (nowMs - mtimeMs > STALENESS_THRESHOLD_MS)
|
|
206
|
+
return 'idle';
|
|
207
|
+
if (previous === 'ended')
|
|
208
|
+
return 'ended';
|
|
209
|
+
if (previous === 'idle')
|
|
210
|
+
return 'idle';
|
|
211
|
+
// A session that was only active thanks to the grace period ends once the
|
|
212
|
+
// grace period lapses, exactly as a fresh classification would say.
|
|
213
|
+
if (previous === 'active' &&
|
|
214
|
+
(previousReason === 'grace-period' || previousReason === 'recent-mtime') &&
|
|
215
|
+
nowMs - mtimeMs >= GRACE_PERIOD_MS) {
|
|
216
|
+
return 'ended';
|
|
217
|
+
}
|
|
218
|
+
return previous === 'unknown' ? 'unknown' : 'active';
|
|
219
|
+
}
|
|
220
|
+
// ── Helpers ──
|
|
221
|
+
function readTail(filePath, fileSize) {
|
|
222
|
+
const bytesToRead = Math.min(TAIL_BYTES, fileSize);
|
|
223
|
+
if (bytesToRead <= 0)
|
|
224
|
+
return null;
|
|
225
|
+
let fd = null;
|
|
226
|
+
try {
|
|
227
|
+
fd = fs.openSync(filePath, 'r');
|
|
228
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
229
|
+
const offset = Math.max(0, fileSize - bytesToRead);
|
|
230
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, offset);
|
|
231
|
+
fs.closeSync(fd);
|
|
232
|
+
fd = null;
|
|
233
|
+
return buffer.toString('utf-8', 0, bytesRead);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
if (fd !== null) {
|
|
237
|
+
try {
|
|
238
|
+
fs.closeSync(fd);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|