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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threshold-crossing detection for subscription quota windows.
|
|
3
|
+
*
|
|
4
|
+
* Pure and host-neutral: the VS Code extension and the CLI dashboard both
|
|
5
|
+
* feed it every quota sample and raise whatever alerts it returns, so a user
|
|
6
|
+
* hears "your five-hour window is at 80%" the same way in both. Alerts fire
|
|
7
|
+
* once per threshold per reset window, on the rising edge; when a window
|
|
8
|
+
* resets the memory resets with it.
|
|
9
|
+
*
|
|
10
|
+
* @module quotaThresholds
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Defaults tuned to the two surprises users report: the afternoon five-hour
|
|
14
|
+
* cutoff (warn at 80%, escalate at 95%) and the mid-week weekly cap (90%).
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_QUOTA_THRESHOLDS = {
|
|
17
|
+
fiveHour: [80, 95],
|
|
18
|
+
sevenDay: [90],
|
|
19
|
+
};
|
|
20
|
+
function sanitizeThresholds(values) {
|
|
21
|
+
return [...new Set(values.filter((v) => Number.isFinite(v) && v > 0 && v <= 100))].sort((a, b) => a - b);
|
|
22
|
+
}
|
|
23
|
+
function evaluateWindow(window, quota, thresholds, previous) {
|
|
24
|
+
const sorted = sanitizeThresholds(thresholds);
|
|
25
|
+
const utilization = Number.isFinite(quota.utilization) ? quota.utilization : 0;
|
|
26
|
+
const resetsAt = quota.resetsAt ?? '';
|
|
27
|
+
let level = 0;
|
|
28
|
+
for (const threshold of sorted)
|
|
29
|
+
if (utilization >= threshold)
|
|
30
|
+
level = threshold;
|
|
31
|
+
const previousLevel = previous && previous.resetsAt === resetsAt ? previous.level : 0;
|
|
32
|
+
const memory = { resetsAt, level: Math.max(level, previousLevel) };
|
|
33
|
+
if (level <= previousLevel)
|
|
34
|
+
return { alert: null, memory };
|
|
35
|
+
return {
|
|
36
|
+
alert: {
|
|
37
|
+
window,
|
|
38
|
+
threshold: level,
|
|
39
|
+
utilization,
|
|
40
|
+
resetsAt,
|
|
41
|
+
severity: level >= sorted[sorted.length - 1] ? 'critical' : 'warning',
|
|
42
|
+
},
|
|
43
|
+
memory,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compare a quota sample against the thresholds and the alerts already
|
|
48
|
+
* raised. Unavailable samples produce no alerts and leave the memory alone.
|
|
49
|
+
*/
|
|
50
|
+
export function evaluateQuotaThresholds(quota, config = DEFAULT_QUOTA_THRESHOLDS, memory = {}) {
|
|
51
|
+
if (!quota.available)
|
|
52
|
+
return { alerts: [], memory };
|
|
53
|
+
const fiveHour = evaluateWindow('fiveHour', quota.fiveHour, config.fiveHour, memory.fiveHour);
|
|
54
|
+
const sevenDay = evaluateWindow('sevenDay', quota.sevenDay, config.sevenDay, memory.sevenDay);
|
|
55
|
+
return {
|
|
56
|
+
alerts: [fiveHour.alert, sevenDay.alert].filter((alert) => alert !== null),
|
|
57
|
+
memory: { fiveHour: fiveHour.memory, sevenDay: sevenDay.memory },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const WINDOW_LABELS = {
|
|
61
|
+
fiveHour: 'five-hour window',
|
|
62
|
+
sevenDay: 'seven-day window',
|
|
63
|
+
};
|
|
64
|
+
function formatResetTime(resetsAt, now) {
|
|
65
|
+
const ms = Date.parse(resetsAt);
|
|
66
|
+
if (!Number.isFinite(ms))
|
|
67
|
+
return null;
|
|
68
|
+
const reset = new Date(ms);
|
|
69
|
+
const sameDay = reset.getFullYear() === now.getFullYear() &&
|
|
70
|
+
reset.getMonth() === now.getMonth() &&
|
|
71
|
+
reset.getDate() === now.getDate();
|
|
72
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
73
|
+
...(sameDay ? {} : { weekday: 'short' }),
|
|
74
|
+
hour: '2-digit',
|
|
75
|
+
minute: '2-digit',
|
|
76
|
+
hour12: false,
|
|
77
|
+
}).format(reset);
|
|
78
|
+
}
|
|
79
|
+
/** One-line, reset-aware message: `Claude five-hour window at 82% (resets 14:00)`. */
|
|
80
|
+
export function describeQuotaThresholdAlert(alert, options = {}) {
|
|
81
|
+
const provider = options.providerLabel ?? 'Claude';
|
|
82
|
+
const reset = formatResetTime(alert.resetsAt, options.now ?? new Date());
|
|
83
|
+
const suffix = reset ? ` (resets ${reset})` : '';
|
|
84
|
+
return `${provider} ${WINDOW_LABELS[alert.window]} at ${Math.round(alert.utilization)}%${suffix}`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for persisted decisions.
|
|
3
|
+
*/
|
|
4
|
+
import { readProjectJsonStore } from './helpers';
|
|
5
|
+
export async function readDecisions(project, opts) {
|
|
6
|
+
const store = await readProjectJsonStore(project, 'decisions');
|
|
7
|
+
if (!store)
|
|
8
|
+
return [];
|
|
9
|
+
let decisions = store.decisions && typeof store.decisions === 'object' && !Array.isArray(store.decisions)
|
|
10
|
+
? Object.values(store.decisions)
|
|
11
|
+
: [];
|
|
12
|
+
// Sort by timestamp descending
|
|
13
|
+
decisions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
14
|
+
if (opts?.search) {
|
|
15
|
+
const q = opts.search.toLowerCase();
|
|
16
|
+
decisions = decisions.filter((d) => d.description.toLowerCase().includes(q) ||
|
|
17
|
+
d.rationale.toLowerCase().includes(q) ||
|
|
18
|
+
d.chosenOption.toLowerCase().includes(q));
|
|
19
|
+
}
|
|
20
|
+
if (opts?.limit && opts.limit > 0) {
|
|
21
|
+
decisions = decisions.slice(0, opts.limit);
|
|
22
|
+
}
|
|
23
|
+
return decisions;
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for session handoff documents.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { getProjectDataPath } from '../paths';
|
|
6
|
+
export async function readLatestHandoff(project) {
|
|
7
|
+
const candidates = typeof project === 'string' ? [project] : project.candidates;
|
|
8
|
+
for (const slug of candidates) {
|
|
9
|
+
const filePath = getProjectDataPath(slug, 'handoffs').replace(/\.json$/, '-latest.md');
|
|
10
|
+
try {
|
|
11
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
12
|
+
if (content.trim())
|
|
13
|
+
return content.trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Try the legacy candidate.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader helpers.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { getProjectDataPath } from '../paths';
|
|
6
|
+
/**
|
|
7
|
+
* Reads and parses a JSON store file. Returns null if file missing or malformed.
|
|
8
|
+
*/
|
|
9
|
+
export async function readJsonStore(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Reads the canonical project store, falling back to the legacy symlink slug. */
|
|
19
|
+
export async function readProjectJsonStore(project, subdomain) {
|
|
20
|
+
const candidates = typeof project === 'string' ? [project] : project.candidates;
|
|
21
|
+
for (const slug of candidates) {
|
|
22
|
+
const store = await readJsonStore(getProjectDataPath(slug, subdomain));
|
|
23
|
+
if (store)
|
|
24
|
+
return store;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for historical data.
|
|
3
|
+
*/
|
|
4
|
+
import { getGlobalDataPath } from '../paths';
|
|
5
|
+
import { readJsonStore } from './helpers';
|
|
6
|
+
export async function readHistory() {
|
|
7
|
+
const filePath = getGlobalDataPath('historical-data.json');
|
|
8
|
+
return readJsonStore(filePath);
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for knowledge notes.
|
|
3
|
+
*/
|
|
4
|
+
import { readProjectJsonStore } from './helpers';
|
|
5
|
+
export async function readNotes(project, opts) {
|
|
6
|
+
const store = await readProjectJsonStore(project, 'knowledge-notes');
|
|
7
|
+
if (!store)
|
|
8
|
+
return [];
|
|
9
|
+
let notes = [];
|
|
10
|
+
const notesByFile = store.notesByFile && typeof store.notesByFile === 'object' && !Array.isArray(store.notesByFile)
|
|
11
|
+
? store.notesByFile
|
|
12
|
+
: {};
|
|
13
|
+
if (opts?.file) {
|
|
14
|
+
// Normalize file path for comparison
|
|
15
|
+
const normalizedFile = opts.file.replace(/\\/g, '/');
|
|
16
|
+
for (const [filePath, fileNotes] of Object.entries(notesByFile)) {
|
|
17
|
+
const normalizedKey = filePath.replace(/\\/g, '/');
|
|
18
|
+
if (normalizedKey === normalizedFile ||
|
|
19
|
+
normalizedKey.endsWith('/' + normalizedFile) ||
|
|
20
|
+
normalizedFile.endsWith('/' + normalizedKey)) {
|
|
21
|
+
if (Array.isArray(fileNotes))
|
|
22
|
+
notes.push(...fileNotes);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const fileNotes of Object.values(notesByFile)) {
|
|
28
|
+
if (Array.isArray(fileNotes))
|
|
29
|
+
notes.push(...fileNotes);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (opts?.type) {
|
|
33
|
+
notes = notes.filter((n) => n.noteType === opts.type);
|
|
34
|
+
}
|
|
35
|
+
if (opts?.status) {
|
|
36
|
+
notes = notes.filter((n) => n.status === opts.status);
|
|
37
|
+
}
|
|
38
|
+
// Sort by importance then updatedAt
|
|
39
|
+
const importanceOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
40
|
+
notes.sort((a, b) => {
|
|
41
|
+
const ia = importanceOrder[a.importance] ?? 4;
|
|
42
|
+
const ib = importanceOrder[b.importance] ?? 4;
|
|
43
|
+
if (ia !== ib)
|
|
44
|
+
return ia - ib;
|
|
45
|
+
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
46
|
+
});
|
|
47
|
+
return notes;
|
|
48
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader/writer for persisted plan history.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { PLAN_SCHEMA_VERSION, MAX_PLANS_PER_PROJECT } from '../types/plan';
|
|
7
|
+
import { getProjectDataPath, encodeWorkspacePath } from '../paths';
|
|
8
|
+
import { readJsonStore } from './helpers';
|
|
9
|
+
import { parsePlanMarkdown } from '../parsers/planExtractor';
|
|
10
|
+
import { atomicWriteJson } from '../writers/atomic';
|
|
11
|
+
import { getClaudeProjectsDir, getLiveClaudeHome } from '../claudePaths';
|
|
12
|
+
export async function readPlans(slug, opts) {
|
|
13
|
+
const filePath = getProjectDataPath(slug, 'plans');
|
|
14
|
+
const store = await readJsonStore(filePath);
|
|
15
|
+
if (!store)
|
|
16
|
+
return [];
|
|
17
|
+
let plans = Array.isArray(store.plans) ? [...store.plans] : [];
|
|
18
|
+
const status = opts?.status ?? 'all';
|
|
19
|
+
if (status !== 'all') {
|
|
20
|
+
plans = plans.filter((p) => p.status === status);
|
|
21
|
+
}
|
|
22
|
+
if (opts?.source) {
|
|
23
|
+
plans = plans.filter((p) => p.source === opts.source);
|
|
24
|
+
}
|
|
25
|
+
// Already sorted by createdAt desc in storage
|
|
26
|
+
if (opts?.limit && opts.limit > 0) {
|
|
27
|
+
plans = plans.slice(0, opts.limit);
|
|
28
|
+
}
|
|
29
|
+
return plans;
|
|
30
|
+
}
|
|
31
|
+
export async function getLatestPlan(slug) {
|
|
32
|
+
const plans = await readPlans(slug, { limit: 1 });
|
|
33
|
+
return plans[0] ?? null;
|
|
34
|
+
}
|
|
35
|
+
export function getPlanAnalytics(plans) {
|
|
36
|
+
const totalPlans = plans.length;
|
|
37
|
+
if (totalPlans === 0) {
|
|
38
|
+
return {
|
|
39
|
+
totalPlans: 0,
|
|
40
|
+
completedPlans: 0,
|
|
41
|
+
failedPlans: 0,
|
|
42
|
+
abandonedPlans: 0,
|
|
43
|
+
avgCompletionRate: 0,
|
|
44
|
+
avgDurationMs: 0,
|
|
45
|
+
avgStepsPerPlan: 0,
|
|
46
|
+
avgTokensPerPlan: 0,
|
|
47
|
+
avgCostPerPlan: 0,
|
|
48
|
+
plansBySource: {},
|
|
49
|
+
mostCommonFailureStep: null,
|
|
50
|
+
completionTrend: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const completedPlans = plans.filter((p) => p.status === 'completed').length;
|
|
54
|
+
const failedPlans = plans.filter((p) => p.status === 'failed').length;
|
|
55
|
+
const abandonedPlans = plans.filter((p) => p.status === 'abandoned').length;
|
|
56
|
+
const avgCompletionRate = plans.reduce((s, p) => s + p.completionRate, 0) / totalPlans;
|
|
57
|
+
const plansWithDuration = plans.filter((p) => p.totalDurationMs != null && p.totalDurationMs > 0);
|
|
58
|
+
const avgDurationMs = plansWithDuration.length > 0
|
|
59
|
+
? plansWithDuration.reduce((s, p) => s + (p.totalDurationMs || 0), 0) /
|
|
60
|
+
plansWithDuration.length
|
|
61
|
+
: 0;
|
|
62
|
+
const avgStepsPerPlan = plans.reduce((s, p) => s + p.steps.length, 0) / totalPlans;
|
|
63
|
+
const plansWithTokens = plans.filter((p) => p.totalTokensUsed != null && p.totalTokensUsed > 0);
|
|
64
|
+
const avgTokensPerPlan = plansWithTokens.length > 0
|
|
65
|
+
? plansWithTokens.reduce((s, p) => s + (p.totalTokensUsed || 0), 0) / plansWithTokens.length
|
|
66
|
+
: 0;
|
|
67
|
+
const plansWithCost = plans.filter((p) => p.totalCostUsd != null && p.totalCostUsd > 0);
|
|
68
|
+
const avgCostPerPlan = plansWithCost.length > 0
|
|
69
|
+
? plansWithCost.reduce((s, p) => s + (p.totalCostUsd || 0), 0) / plansWithCost.length
|
|
70
|
+
: 0;
|
|
71
|
+
// Plans by source
|
|
72
|
+
const plansBySource = {};
|
|
73
|
+
for (const p of plans) {
|
|
74
|
+
plansBySource[p.source] = (plansBySource[p.source] || 0) + 1;
|
|
75
|
+
}
|
|
76
|
+
// Most common failure step (which step index fails most)
|
|
77
|
+
const failureCounts = new Map();
|
|
78
|
+
for (const p of plans) {
|
|
79
|
+
for (let i = 0; i < p.steps.length; i++) {
|
|
80
|
+
if (p.steps[i].status === 'failed') {
|
|
81
|
+
failureCounts.set(i, (failureCounts.get(i) || 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let mostCommonFailureStep = null;
|
|
86
|
+
if (failureCounts.size > 0) {
|
|
87
|
+
let maxCount = 0;
|
|
88
|
+
let maxIdx = 0;
|
|
89
|
+
for (const [idx, count] of failureCounts) {
|
|
90
|
+
if (count > maxCount) {
|
|
91
|
+
maxCount = count;
|
|
92
|
+
maxIdx = idx;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
mostCommonFailureStep = `Step ${maxIdx + 1}`;
|
|
96
|
+
}
|
|
97
|
+
// Completion trend (grouped by date)
|
|
98
|
+
const byDate = new Map();
|
|
99
|
+
for (const p of plans) {
|
|
100
|
+
const date = p.createdAt.substring(0, 10); // YYYY-MM-DD
|
|
101
|
+
const existing = byDate.get(date) || { sum: 0, count: 0 };
|
|
102
|
+
existing.sum += p.completionRate;
|
|
103
|
+
existing.count += 1;
|
|
104
|
+
byDate.set(date, existing);
|
|
105
|
+
}
|
|
106
|
+
const completionTrend = Array.from(byDate.entries())
|
|
107
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
108
|
+
.map(([date, { sum, count }]) => ({ date, rate: sum / count }));
|
|
109
|
+
return {
|
|
110
|
+
totalPlans,
|
|
111
|
+
completedPlans,
|
|
112
|
+
failedPlans,
|
|
113
|
+
abandonedPlans,
|
|
114
|
+
avgCompletionRate,
|
|
115
|
+
avgDurationMs,
|
|
116
|
+
avgStepsPerPlan,
|
|
117
|
+
avgTokensPerPlan,
|
|
118
|
+
avgCostPerPlan,
|
|
119
|
+
plansBySource,
|
|
120
|
+
mostCommonFailureStep,
|
|
121
|
+
completionTrend,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export async function writePlans(slug, plans) {
|
|
125
|
+
const filePath = getProjectDataPath(slug, 'plans');
|
|
126
|
+
// Sort by createdAt desc and cap
|
|
127
|
+
const sorted = [...plans].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
128
|
+
const capped = sorted.slice(0, MAX_PLANS_PER_PROJECT);
|
|
129
|
+
const store = {
|
|
130
|
+
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
131
|
+
plans: capped,
|
|
132
|
+
lastSaved: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
await atomicWriteJson(filePath, store);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Reads raw plan markdown files from ~/.claude/plans/ that belong to
|
|
138
|
+
* sessions in the given workspace. Maps session JSONL slugs to plan filenames.
|
|
139
|
+
*/
|
|
140
|
+
export async function readClaudeCodePlanFiles(workspacePath) {
|
|
141
|
+
const claudePlansDir = path.join(getLiveClaudeHome(), 'plans');
|
|
142
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
143
|
+
// Find the project directory in ~/.claude/projects/
|
|
144
|
+
const cwd = workspacePath || process.cwd();
|
|
145
|
+
const encoded = encodeWorkspacePath(path.resolve(cwd));
|
|
146
|
+
const projectDir = path.join(claudeProjectsDir, encoded);
|
|
147
|
+
if (!fs.existsSync(projectDir) || !fs.existsSync(claudePlansDir)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
// Extract unique slugs from session JSONL files (read first match per file)
|
|
151
|
+
const slugSet = new Set();
|
|
152
|
+
let jsonlFiles;
|
|
153
|
+
try {
|
|
154
|
+
jsonlFiles = (await fs.promises.readdir(projectDir)).filter((f) => f.endsWith('.jsonl'));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
// Read slugs in parallel (grep first "slug" field from each JSONL)
|
|
160
|
+
const SLUG_RE = /"slug":"([^"]+)"/;
|
|
161
|
+
await Promise.all(jsonlFiles.map(async (f) => {
|
|
162
|
+
try {
|
|
163
|
+
const filePath = path.join(projectDir, f);
|
|
164
|
+
const handle = await fs.promises.open(filePath, 'r');
|
|
165
|
+
try {
|
|
166
|
+
// Read first 32KB — slug is always in the first few lines
|
|
167
|
+
const buf = Buffer.alloc(32768);
|
|
168
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
|
|
169
|
+
const chunk = buf.toString('utf-8', 0, bytesRead);
|
|
170
|
+
for (const line of chunk.split('\n')) {
|
|
171
|
+
const m = line.match(SLUG_RE);
|
|
172
|
+
if (m) {
|
|
173
|
+
slugSet.add(m[1]);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await handle.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* skip unreadable files */
|
|
184
|
+
}
|
|
185
|
+
}));
|
|
186
|
+
if (slugSet.size === 0)
|
|
187
|
+
return [];
|
|
188
|
+
// Read matching plan files
|
|
189
|
+
const plans = [];
|
|
190
|
+
for (const slug of slugSet) {
|
|
191
|
+
const planPath = path.join(claudePlansDir, `${slug}.md`);
|
|
192
|
+
try {
|
|
193
|
+
const stat = await fs.promises.stat(planPath);
|
|
194
|
+
const content = await fs.promises.readFile(planPath, 'utf-8');
|
|
195
|
+
const parsed = parsePlanMarkdown(content);
|
|
196
|
+
const steps = parsed.steps.map((s) => ({
|
|
197
|
+
id: s.id,
|
|
198
|
+
description: s.description,
|
|
199
|
+
status: s.status,
|
|
200
|
+
phase: s.phase,
|
|
201
|
+
complexity: s.complexity,
|
|
202
|
+
}));
|
|
203
|
+
const completed = steps.filter((s) => s.status === 'completed').length;
|
|
204
|
+
const total = steps.length;
|
|
205
|
+
plans.push({
|
|
206
|
+
id: `claude-plan-${slug}`,
|
|
207
|
+
projectSlug: encoded,
|
|
208
|
+
sessionId: slug,
|
|
209
|
+
title: parsed.title || slug,
|
|
210
|
+
source: 'claude-code',
|
|
211
|
+
createdAt: stat.mtime.toISOString(),
|
|
212
|
+
status: total > 0 && completed === total ? 'completed' : 'in_progress',
|
|
213
|
+
steps,
|
|
214
|
+
completionRate: total > 0 ? completed / total : 0,
|
|
215
|
+
rawMarkdown: content,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* plan file missing or unreadable */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Sort by createdAt descending
|
|
223
|
+
plans.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
224
|
+
return plans;
|
|
225
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for persisted tasks.
|
|
3
|
+
*/
|
|
4
|
+
import { readProjectJsonStore } from './helpers';
|
|
5
|
+
export async function readTasks(project, opts) {
|
|
6
|
+
const store = await readProjectJsonStore(project, 'tasks');
|
|
7
|
+
if (!store)
|
|
8
|
+
return [];
|
|
9
|
+
let tasks = store.tasks && typeof store.tasks === 'object' && !Array.isArray(store.tasks)
|
|
10
|
+
? Object.values(store.tasks)
|
|
11
|
+
: [];
|
|
12
|
+
const status = opts?.status ?? 'all';
|
|
13
|
+
if (status !== 'all') {
|
|
14
|
+
tasks = tasks.filter((t) => t.status === status);
|
|
15
|
+
}
|
|
16
|
+
// Sort by updatedAt descending
|
|
17
|
+
tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
18
|
+
return tasks;
|
|
19
|
+
}
|