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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only SQLite wrapper for Codex CLI's state database.
|
|
3
|
+
* Ported from sidekick-vscode/src/services/providers/CodexDatabase.ts
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { execFile, execFileSync } from 'child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Ids per inlined `IN (…)` query. Values are passed inside a single argv
|
|
10
|
+
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
11
|
+
*/
|
|
12
|
+
const SQL_ID_CHUNK_SIZE = 400;
|
|
13
|
+
/** Parent thread id recorded in a Codex `threads.source` JSON value, if any. */
|
|
14
|
+
export function spawnParentFromSource(source) {
|
|
15
|
+
let parsed = source;
|
|
16
|
+
if (typeof source === 'string') {
|
|
17
|
+
if (!source.startsWith('{'))
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(source);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!parsed || typeof parsed !== 'object')
|
|
27
|
+
return undefined;
|
|
28
|
+
const parent = parsed
|
|
29
|
+
.subagent?.thread_spawn?.parent_thread_id;
|
|
30
|
+
return typeof parent === 'string' ? parent : undefined;
|
|
31
|
+
}
|
|
32
|
+
export class CodexDatabase {
|
|
33
|
+
dbPath;
|
|
34
|
+
sqlite3Available = null;
|
|
35
|
+
runtimeStatus = null;
|
|
36
|
+
constructor(codexHome) {
|
|
37
|
+
this.dbPath = findLatestStateDatabase(codexHome) ?? path.join(codexHome, 'state.sqlite');
|
|
38
|
+
}
|
|
39
|
+
isAvailable() {
|
|
40
|
+
try {
|
|
41
|
+
return fs.statSync(this.dbPath).size > 0;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
open() {
|
|
48
|
+
if (this.sqlite3Available !== null)
|
|
49
|
+
return this.sqlite3Available;
|
|
50
|
+
if (!this.isAvailable()) {
|
|
51
|
+
this.sqlite3Available = false;
|
|
52
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
execFileSync('sqlite3', ['--version'], {
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: 4000,
|
|
59
|
+
killSignal: 'SIGKILL',
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
this.sqlite3Available = true;
|
|
63
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.sqlite3Available = false;
|
|
68
|
+
this.runtimeStatus = classifySqliteError(error);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
getRuntimeStatus() {
|
|
73
|
+
if (this.runtimeStatus)
|
|
74
|
+
return this.runtimeStatus;
|
|
75
|
+
if (!this.isAvailable())
|
|
76
|
+
return { available: false, kind: 'db_missing' };
|
|
77
|
+
return {
|
|
78
|
+
available: false,
|
|
79
|
+
kind: 'query_failed',
|
|
80
|
+
message: 'Codex database has not been initialized yet.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
close() { }
|
|
84
|
+
query(sql, params = []) {
|
|
85
|
+
if (!this.sqlite3Available)
|
|
86
|
+
return [];
|
|
87
|
+
let paramIndex = 0;
|
|
88
|
+
const query = sql.replace(/\?/g, () => {
|
|
89
|
+
if (paramIndex >= params.length)
|
|
90
|
+
return '?';
|
|
91
|
+
const param = params[paramIndex++];
|
|
92
|
+
if (typeof param === 'number') {
|
|
93
|
+
if (!Number.isFinite(param))
|
|
94
|
+
return '0';
|
|
95
|
+
return String(param);
|
|
96
|
+
}
|
|
97
|
+
const escaped = String(param).replace(/'/g, "''");
|
|
98
|
+
return `'${escaped}'`;
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
const result = execFileSync('sqlite3', ['-json', '-readonly', this.dbPath, query], {
|
|
102
|
+
encoding: 'utf-8',
|
|
103
|
+
timeout: 4000,
|
|
104
|
+
killSignal: 'SIGKILL',
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
107
|
+
});
|
|
108
|
+
const trimmed = result.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
return [];
|
|
111
|
+
return JSON.parse(trimmed);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.runtimeStatus = {
|
|
115
|
+
...classifySqliteError(error, 'query_failed'),
|
|
116
|
+
available: this.sqlite3Available === true,
|
|
117
|
+
};
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async queryAsync(sql, params = []) {
|
|
122
|
+
if (!this.isAvailable()) {
|
|
123
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const query = bindSqlParams(sql, params);
|
|
127
|
+
try {
|
|
128
|
+
const result = await execSqlite(this.dbPath, query, 10 * 1024 * 1024);
|
|
129
|
+
this.sqlite3Available = true;
|
|
130
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
131
|
+
const trimmed = result.trim();
|
|
132
|
+
return trimmed ? JSON.parse(trimmed) : [];
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const status = classifySqliteError(error, 'query_failed');
|
|
136
|
+
// Only a missing sqlite3 binary gates future attempts. A failed query
|
|
137
|
+
// is transient and must not latch the database off (mirrors query()).
|
|
138
|
+
if (status.kind === 'sqlite_missing')
|
|
139
|
+
this.sqlite3Available = false;
|
|
140
|
+
this.runtimeStatus = status;
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
queryOne(sql, params = []) {
|
|
145
|
+
return this.query(sql, params)[0] ?? null;
|
|
146
|
+
}
|
|
147
|
+
getThreadsByCwd(cwd) {
|
|
148
|
+
const normalized = normalizePath(cwd);
|
|
149
|
+
const exact = this.query('SELECT * FROM threads WHERE cwd = ? ORDER BY updated_at DESC', [normalized]);
|
|
150
|
+
if (exact.length > 0)
|
|
151
|
+
return exact;
|
|
152
|
+
const all = this.query('SELECT * FROM threads ORDER BY updated_at DESC');
|
|
153
|
+
return all.filter((t) => {
|
|
154
|
+
const threadCwd = normalizePath(t.cwd);
|
|
155
|
+
return (threadCwd === normalized ||
|
|
156
|
+
normalized.startsWith(threadCwd + path.sep) ||
|
|
157
|
+
threadCwd.startsWith(normalized + path.sep));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
getMostRecentThread(cwd) {
|
|
161
|
+
return this.getThreadsByCwd(cwd)[0] ?? null;
|
|
162
|
+
}
|
|
163
|
+
getAllDistinctCwds() {
|
|
164
|
+
return this.query('SELECT cwd, COUNT(*) as count, MAX(updated_at) as lastUpdated FROM threads GROUP BY cwd ORDER BY lastUpdated DESC');
|
|
165
|
+
}
|
|
166
|
+
getThread(id) {
|
|
167
|
+
return this.queryOne('SELECT * FROM threads WHERE id = ?', [id]);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolve many thread ids with non-blocking sqlite3 subprocesses — one per
|
|
171
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
172
|
+
*/
|
|
173
|
+
async getThreadsByIdsAsync(ids) {
|
|
174
|
+
const results = [];
|
|
175
|
+
for (let i = 0; i < ids.length; i += SQL_ID_CHUNK_SIZE) {
|
|
176
|
+
const chunk = ids.slice(i, i + SQL_ID_CHUNK_SIZE);
|
|
177
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
178
|
+
results.push(...(await this.queryAsync(`SELECT * FROM threads WHERE id IN (${placeholders})`, [...chunk])));
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
threadColumns = null;
|
|
183
|
+
/** Column names of the `threads` table (cached; the schema varies across Codex versions). */
|
|
184
|
+
getThreadColumns() {
|
|
185
|
+
if (!this.threadColumns) {
|
|
186
|
+
const rows = this.query("SELECT name FROM pragma_table_info('threads')");
|
|
187
|
+
if (rows.length === 0)
|
|
188
|
+
return new Set();
|
|
189
|
+
this.threadColumns = new Set(rows.map((row) => row.name));
|
|
190
|
+
}
|
|
191
|
+
return this.threadColumns;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get all threads forked from a given session ID. Older Codex schemas carry
|
|
195
|
+
* a `forked_from_id` column; newer ones drop it, so the query only runs
|
|
196
|
+
* when the column exists (otherwise it fails and reports a diagnostic).
|
|
197
|
+
*/
|
|
198
|
+
getThreadsByForkedFromId(parentId) {
|
|
199
|
+
if (!this.getThreadColumns().has('forked_from_id'))
|
|
200
|
+
return [];
|
|
201
|
+
return this.query('SELECT * FROM threads WHERE forked_from_id = ? ORDER BY created_at ASC', [parentId]);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get the subagent threads a session spawned. Codex records the parent in
|
|
205
|
+
* the `source` JSON (`{"subagent":{"thread_spawn":{"parent_thread_id":…}}}`);
|
|
206
|
+
* the LIKE narrows the scan and the JSON parse confirms the match.
|
|
207
|
+
*/
|
|
208
|
+
getSpawnedChildThreads(parentId) {
|
|
209
|
+
if (!this.getThreadColumns().has('source'))
|
|
210
|
+
return [];
|
|
211
|
+
const rows = this.query('SELECT * FROM threads WHERE source LIKE ? ORDER BY created_at ASC', [`%${parentId}%`]);
|
|
212
|
+
return rows.filter((row) => spawnParentFromSource(row.source) === parentId);
|
|
213
|
+
}
|
|
214
|
+
/** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
|
|
215
|
+
getDbMtime() {
|
|
216
|
+
try {
|
|
217
|
+
return fs.statSync(this.dbPath).mtime.getTime();
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function classifySqliteError(error, fallback = 'sqlite_blocked') {
|
|
225
|
+
const code = error?.code;
|
|
226
|
+
if (code === 'ENOENT') {
|
|
227
|
+
return {
|
|
228
|
+
available: false,
|
|
229
|
+
kind: 'sqlite_missing',
|
|
230
|
+
message: 'The sqlite3 executable was not found.',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
available: false,
|
|
235
|
+
kind: fallback,
|
|
236
|
+
message: error instanceof Error ? error.message : 'sqlite3 could not be executed.',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function bindSqlParams(sql, params) {
|
|
240
|
+
let paramIndex = 0;
|
|
241
|
+
return sql.replace(/\?/g, () => {
|
|
242
|
+
if (paramIndex >= params.length)
|
|
243
|
+
return '?';
|
|
244
|
+
const param = params[paramIndex++];
|
|
245
|
+
if (typeof param === 'number')
|
|
246
|
+
return Number.isFinite(param) ? String(param) : '0';
|
|
247
|
+
return `'${String(param).replace(/'/g, "''")}'`;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function execSqlite(dbPath, query, maxBuffer) {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
execFile('sqlite3', ['-json', '-readonly', dbPath, query], {
|
|
253
|
+
encoding: 'utf8',
|
|
254
|
+
timeout: 4_000,
|
|
255
|
+
killSignal: 'SIGKILL',
|
|
256
|
+
maxBuffer,
|
|
257
|
+
}, (error, stdout) => {
|
|
258
|
+
if (error)
|
|
259
|
+
reject(error);
|
|
260
|
+
else
|
|
261
|
+
resolve(stdout);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function findLatestStateDatabase(codexHome) {
|
|
266
|
+
try {
|
|
267
|
+
const entries = fs.readdirSync(codexHome, { withFileTypes: true });
|
|
268
|
+
const candidates = [];
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
if (!entry.isFile() || !/^state(?:_\d+)?\.sqlite$/.test(entry.name))
|
|
271
|
+
continue;
|
|
272
|
+
const dbPath = path.join(codexHome, entry.name);
|
|
273
|
+
try {
|
|
274
|
+
const stat = fs.statSync(dbPath);
|
|
275
|
+
if (stat.size > 0) {
|
|
276
|
+
candidates.push({ path: dbPath, mtime: stat.mtime.getTime() });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Skip inaccessible candidates.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
284
|
+
return candidates[0]?.path ?? null;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function normalizePath(input) {
|
|
291
|
+
try {
|
|
292
|
+
return fs.realpathSync(input);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return path.resolve(input);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex rollout-file helpers shared by the provider and by callers that only
|
|
3
|
+
* list rollouts: bounded first-line reads, session_meta and cwd lookups, and
|
|
4
|
+
* the async session-file listing. Kept apart from the provider class so a
|
|
5
|
+
* listing does not bundle the provider's stats, context and reader code.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { getCodexSessionsDirs } from '../codexPaths';
|
|
10
|
+
import { DirectoryListingCache } from './directoryListingCache';
|
|
11
|
+
import { walkRolloutFilesAsync } from './rolloutWalker';
|
|
12
|
+
/** Every directory across the monitored homes that may hold rollouts, archived ones included. */
|
|
13
|
+
export function getSessionsDirs() {
|
|
14
|
+
return getCodexSessionsDirs();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read the first N complete lines from a file using chunked reads.
|
|
18
|
+
* Handles arbitrarily long lines (Codex session_meta can be 10KB+).
|
|
19
|
+
*/
|
|
20
|
+
export function readFirstLines(filePath, maxLines, maxBytes = 256 * 1024) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
let fd;
|
|
23
|
+
try {
|
|
24
|
+
fd = fs.openSync(filePath, 'r');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
let pending = '';
|
|
30
|
+
let totalRead = 0;
|
|
31
|
+
const chunkSize = 16384;
|
|
32
|
+
try {
|
|
33
|
+
while (totalRead < maxBytes && lines.length < maxLines) {
|
|
34
|
+
const buf = Buffer.alloc(Math.min(chunkSize, maxBytes - totalRead));
|
|
35
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, totalRead);
|
|
36
|
+
if (bytesRead === 0)
|
|
37
|
+
break;
|
|
38
|
+
totalRead += bytesRead;
|
|
39
|
+
pending += buf.toString('utf-8', 0, bytesRead);
|
|
40
|
+
let nlIdx;
|
|
41
|
+
while ((nlIdx = pending.indexOf('\n')) !== -1 && lines.length < maxLines) {
|
|
42
|
+
lines.push(pending.substring(0, nlIdx));
|
|
43
|
+
pending = pending.substring(nlIdx + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (pending.trim() && lines.length < maxLines) {
|
|
47
|
+
lines.push(pending);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
/** Read session_meta from the beginning of a rollout file. */
|
|
56
|
+
export function readSessionMeta(rolloutPath) {
|
|
57
|
+
try {
|
|
58
|
+
const lines = readFirstLines(rolloutPath, 1);
|
|
59
|
+
const firstLine = lines[0]?.trim();
|
|
60
|
+
if (!firstLine)
|
|
61
|
+
return null;
|
|
62
|
+
const parsed = JSON.parse(firstLine);
|
|
63
|
+
if (parsed.type === 'session_meta') {
|
|
64
|
+
return parsed.payload;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Skip
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* First line of a rollout, chunk-read under the same 256 KiB cap as
|
|
74
|
+
* readFirstLines(). `complete` is false while the line has no newline yet
|
|
75
|
+
* (a rollout that was just created) or the file could not be read, so
|
|
76
|
+
* callers know not to cache the answer.
|
|
77
|
+
*/
|
|
78
|
+
export async function readFirstLineAsync(rolloutPath) {
|
|
79
|
+
// Mirrors readSessionMeta()/readFirstLines(): chunk-read until the first
|
|
80
|
+
// line is complete under the same 256 KiB cap — session_meta carries
|
|
81
|
+
// `instructions` and can far exceed a single chunk.
|
|
82
|
+
const maxBytes = 256 * 1024;
|
|
83
|
+
const chunkSize = 64 * 1024;
|
|
84
|
+
let handle = null;
|
|
85
|
+
try {
|
|
86
|
+
handle = await fs.promises.open(rolloutPath, 'r');
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let totalRead = 0;
|
|
89
|
+
let newlineFound = false;
|
|
90
|
+
while (totalRead < maxBytes && !newlineFound) {
|
|
91
|
+
const buffer = Buffer.alloc(Math.min(chunkSize, maxBytes - totalRead));
|
|
92
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, totalRead);
|
|
93
|
+
if (bytesRead === 0)
|
|
94
|
+
break;
|
|
95
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
96
|
+
chunks.push(chunk);
|
|
97
|
+
totalRead += bytesRead;
|
|
98
|
+
newlineFound = chunk.includes(0x0a);
|
|
99
|
+
}
|
|
100
|
+
const firstLine = Buffer.concat(chunks).toString('utf8').split('\n', 1)[0]?.trim();
|
|
101
|
+
return { line: firstLine || null, complete: newlineFound || totalRead >= maxBytes };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { line: null, complete: false };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await handle?.close().catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function parseSessionMetaLine(line) {
|
|
111
|
+
if (!line)
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(line);
|
|
115
|
+
return parsed.type === 'session_meta' ? parsed.payload : null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read the CWD from a rollout file's session_meta. Cached per path, bounded
|
|
123
|
+
* so a long-lived host watching a large history cannot grow it without limit
|
|
124
|
+
* (insertion order doubles as an eviction order).
|
|
125
|
+
*/
|
|
126
|
+
const CWD_CACHE_LIMIT = 5000;
|
|
127
|
+
const cwdCache = new Map();
|
|
128
|
+
function rememberCwd(rolloutPath, cwd) {
|
|
129
|
+
if (!cwdCache.has(rolloutPath) && cwdCache.size >= CWD_CACHE_LIMIT) {
|
|
130
|
+
const oldest = cwdCache.keys().next().value;
|
|
131
|
+
if (oldest !== undefined)
|
|
132
|
+
cwdCache.delete(oldest);
|
|
133
|
+
}
|
|
134
|
+
cwdCache.set(rolloutPath, cwd);
|
|
135
|
+
}
|
|
136
|
+
export function readSessionCwd(rolloutPath) {
|
|
137
|
+
const cached = cwdCache.get(rolloutPath);
|
|
138
|
+
if (cached !== undefined)
|
|
139
|
+
return cached;
|
|
140
|
+
const meta = readSessionMeta(rolloutPath);
|
|
141
|
+
const cwd = meta?.cwd || null;
|
|
142
|
+
rememberCwd(rolloutPath, cwd);
|
|
143
|
+
return cwd;
|
|
144
|
+
}
|
|
145
|
+
/** Async twin of readSessionCwd, sharing its cache so repeat listings do no content reads. */
|
|
146
|
+
export async function readSessionCwdAsync(rolloutPath) {
|
|
147
|
+
const cached = cwdCache.get(rolloutPath);
|
|
148
|
+
if (cached !== undefined)
|
|
149
|
+
return cached;
|
|
150
|
+
const { line, complete } = await readFirstLineAsync(rolloutPath);
|
|
151
|
+
const cwd = parseSessionMetaLine(line)?.cwd || null;
|
|
152
|
+
// A rollout observed before its session_meta line is flushed has no cwd
|
|
153
|
+
// yet; caching that null would hide the session from its workspace until
|
|
154
|
+
// the entry aged out, so only conclusive answers are remembered.
|
|
155
|
+
if (cwd !== null || complete)
|
|
156
|
+
rememberCwd(rolloutPath, cwd);
|
|
157
|
+
return cwd;
|
|
158
|
+
}
|
|
159
|
+
/** Built-in enumerators report explicit zeros rather than leaving a counter unset. */
|
|
160
|
+
export function initializeCounters(stats) {
|
|
161
|
+
if (!stats)
|
|
162
|
+
return;
|
|
163
|
+
stats.directoriesListed ??= 0;
|
|
164
|
+
stats.directoriesStatted ??= 0;
|
|
165
|
+
stats.filesStatted ??= 0;
|
|
166
|
+
}
|
|
167
|
+
/** Check if a session's CWD matches a workspace path. */
|
|
168
|
+
export function cwdMatches(sessionCwd, workspacePath) {
|
|
169
|
+
const normalizedSession = normalizePath(sessionCwd);
|
|
170
|
+
const normalizedWorkspace = normalizePath(workspacePath);
|
|
171
|
+
return (normalizedSession === normalizedWorkspace ||
|
|
172
|
+
normalizedWorkspace.startsWith(normalizedSession + path.sep) ||
|
|
173
|
+
normalizedSession.startsWith(normalizedWorkspace + path.sep));
|
|
174
|
+
}
|
|
175
|
+
/** Normalize a path using realpathSync. */
|
|
176
|
+
export function normalizePath(input) {
|
|
177
|
+
try {
|
|
178
|
+
return fs.realpathSync(input);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return path.resolve(input);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* List rollout files across the monitored Codex homes, newest first. With
|
|
186
|
+
* `workspacePath`, keeps rollouts whose session_meta cwd equals, contains, or
|
|
187
|
+
* sits inside it.
|
|
188
|
+
*/
|
|
189
|
+
export async function listCodexSessionFilesAsync(workspacePath, options = {}, cache = new DirectoryListingCache()) {
|
|
190
|
+
const results = [];
|
|
191
|
+
initializeCounters(options.stats);
|
|
192
|
+
// One capped walk across the homes (deduplicated, newest first) through
|
|
193
|
+
// the directory cache. With a workspace filter the limit applies after
|
|
194
|
+
// filtering, so the walk itself is bounded only by its file cap.
|
|
195
|
+
const files = await walkRolloutFilesAsync(getSessionsDirs(), {
|
|
196
|
+
limit: workspacePath ? undefined : options.limit,
|
|
197
|
+
since: options.since,
|
|
198
|
+
cache,
|
|
199
|
+
revalidateRecent: options.revalidateRecent,
|
|
200
|
+
stats: options.stats,
|
|
201
|
+
});
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
if (options.limit !== undefined && results.length >= options.limit)
|
|
204
|
+
break;
|
|
205
|
+
const info = { ...file };
|
|
206
|
+
if (workspacePath) {
|
|
207
|
+
const cwd = await readSessionCwdAsync(file.path);
|
|
208
|
+
if (!cwd || !cwdMatches(cwd, workspacePath))
|
|
209
|
+
continue;
|
|
210
|
+
info.workspacePath = cwd;
|
|
211
|
+
}
|
|
212
|
+
results.push(info);
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-based provider auto-detection.
|
|
3
|
+
* Ported from sidekick-vscode/src/services/providers/ProviderDetector.ts
|
|
4
|
+
* without VS Code dependency (no vscode.workspace.getConfiguration).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getCodexMonitoringHomes } from '../codexPaths';
|
|
9
|
+
import { getOpenCodeDataDir } from './openCode';
|
|
10
|
+
import { getClaudeProjectsDir } from '../claudePaths';
|
|
11
|
+
function getCodexHomes() {
|
|
12
|
+
return getCodexMonitoringHomes();
|
|
13
|
+
}
|
|
14
|
+
function hasCodexStateDb(codexHome) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.readdirSync(codexHome).some((entry) => /^state(?:_\d+)?\.sqlite$/.test(entry));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function getCodexStateDbMtime(codexHome) {
|
|
23
|
+
try {
|
|
24
|
+
let latest = 0;
|
|
25
|
+
for (const entry of fs.readdirSync(codexHome)) {
|
|
26
|
+
if (!/^state(?:_\d+)?\.sqlite$/.test(entry))
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
const mtime = fs.statSync(path.join(codexHome, entry)).mtime.getTime();
|
|
30
|
+
if (mtime > latest)
|
|
31
|
+
latest = mtime;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Skip inaccessible files.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return latest;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getMostRecentMtime(dir) {
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(dir))
|
|
46
|
+
return 0;
|
|
47
|
+
let latest = 0;
|
|
48
|
+
const entries = fs.readdirSync(dir);
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
try {
|
|
51
|
+
const stats = fs.statSync(path.join(dir, entry));
|
|
52
|
+
if (stats.mtime.getTime() > latest) {
|
|
53
|
+
latest = stats.mtime.getTime();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* skip */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return latest;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function getMostRecentFileMtime(dir, isCandidate, depth) {
|
|
67
|
+
if (depth < 0)
|
|
68
|
+
return 0;
|
|
69
|
+
let latest = 0;
|
|
70
|
+
try {
|
|
71
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
72
|
+
const filePath = path.join(dir, entry);
|
|
73
|
+
try {
|
|
74
|
+
const stats = fs.statSync(filePath);
|
|
75
|
+
if (stats.isDirectory?.()) {
|
|
76
|
+
latest = Math.max(latest, getMostRecentFileMtime(filePath, isCandidate, depth - 1));
|
|
77
|
+
}
|
|
78
|
+
else if (isCandidate(filePath)) {
|
|
79
|
+
latest = Math.max(latest, stats.mtime.getTime());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* skip */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
return latest;
|
|
91
|
+
}
|
|
92
|
+
function getClaudeActivityMtime(claudeBase) {
|
|
93
|
+
return getMostRecentFileMtime(claudeBase, (filePath) => filePath.endsWith('.jsonl'), 2);
|
|
94
|
+
}
|
|
95
|
+
function getOpenCodeActivityMtime() {
|
|
96
|
+
const dataDir = getOpenCodeDataDir();
|
|
97
|
+
const dbPath = path.join(dataDir, 'opencode.db');
|
|
98
|
+
try {
|
|
99
|
+
const dbMtime = fs.statSync(dbPath).mtime.getTime();
|
|
100
|
+
if (dbMtime > 0)
|
|
101
|
+
return dbMtime;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* no DB */
|
|
105
|
+
}
|
|
106
|
+
const storageDir = path.join(dataDir, 'storage');
|
|
107
|
+
const sessionMtime = getMostRecentMtime(path.join(storageDir, 'session'));
|
|
108
|
+
const messageMtime = getMostRecentMtime(path.join(storageDir, 'message'));
|
|
109
|
+
const partMtime = getMostRecentMtime(path.join(storageDir, 'part'));
|
|
110
|
+
return Math.max(sessionMtime, messageMtime, partMtime);
|
|
111
|
+
}
|
|
112
|
+
function getCodexActivityMtime() {
|
|
113
|
+
let latest = 0;
|
|
114
|
+
for (const codexHome of getCodexHomes()) {
|
|
115
|
+
const dbMtime = getCodexStateDbMtime(codexHome);
|
|
116
|
+
if (dbMtime > latest)
|
|
117
|
+
latest = dbMtime;
|
|
118
|
+
const sessionsMtime = getMostRecentFileMtime(path.join(codexHome, 'sessions'), (filePath) => path.basename(filePath).startsWith('rollout-') && filePath.endsWith('.jsonl'), 4);
|
|
119
|
+
if (sessionsMtime > latest)
|
|
120
|
+
latest = sessionsMtime;
|
|
121
|
+
}
|
|
122
|
+
return latest;
|
|
123
|
+
}
|
|
124
|
+
function getProviderPaths() {
|
|
125
|
+
const claudeBase = getClaudeProjectsDir();
|
|
126
|
+
const openCodeDataDir = getOpenCodeDataDir();
|
|
127
|
+
const codexHomes = getCodexHomes();
|
|
128
|
+
return {
|
|
129
|
+
claudeBase,
|
|
130
|
+
openCodeDbPath: path.join(openCodeDataDir, 'opencode.db'),
|
|
131
|
+
openCodeStorageDir: path.join(openCodeDataDir, 'storage'),
|
|
132
|
+
codexHomes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Ranked detection results are reused within a process for this long. */
|
|
136
|
+
const DETECTION_CACHE_TTL_MS = 5_000;
|
|
137
|
+
let detectionCache = null;
|
|
138
|
+
/** Forget the memoised ranking. Test-only. */
|
|
139
|
+
export function _resetProviderDetectionCache() {
|
|
140
|
+
detectionCache = null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Providers with data on disk, most recently active first.
|
|
144
|
+
*
|
|
145
|
+
* Detection walks every provider's session corpus (a recursive stat over
|
|
146
|
+
* `~/.claude/projects`, every Codex home, and the OpenCode data dir), so the
|
|
147
|
+
* ranking is memoised briefly: the CLI resolves the provider several times per
|
|
148
|
+
* command and the extension asks again on every settings-driven re-detect.
|
|
149
|
+
*/
|
|
150
|
+
function rankDetectedProviders(now = Date.now()) {
|
|
151
|
+
if (detectionCache && now - detectionCache.at < DETECTION_CACHE_TTL_MS) {
|
|
152
|
+
return detectionCache.ranked;
|
|
153
|
+
}
|
|
154
|
+
const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
|
|
155
|
+
const hasClaude = fs.existsSync(claudeBase);
|
|
156
|
+
const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
|
|
157
|
+
const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
|
|
158
|
+
const available = [];
|
|
159
|
+
if (hasClaude)
|
|
160
|
+
available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
|
|
161
|
+
if (hasOpenCode)
|
|
162
|
+
available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
|
|
163
|
+
if (hasCodex)
|
|
164
|
+
available.push({ id: 'codex', mtime: getCodexActivityMtime() });
|
|
165
|
+
available.sort((a, b) => b.mtime - a.mtime);
|
|
166
|
+
const ranked = available.map((a) => a.id);
|
|
167
|
+
detectionCache = { at: now, ranked };
|
|
168
|
+
return ranked;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Returns all provider IDs whose data directories exist on the filesystem.
|
|
172
|
+
* Ordered by most-recent activity first.
|
|
173
|
+
*/
|
|
174
|
+
export function getAllDetectedProviders() {
|
|
175
|
+
return [...rankDetectedProviders()];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Detects the most appropriate provider based on filesystem presence and recency.
|
|
179
|
+
* Pass an explicit provider ID to override auto-detection.
|
|
180
|
+
*/
|
|
181
|
+
export function detectProvider(override) {
|
|
182
|
+
if (override && override !== 'auto')
|
|
183
|
+
return override;
|
|
184
|
+
return rankDetectedProviders()[0] ?? 'claude-code';
|
|
185
|
+
}
|