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,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code session provider for the shared package.
|
|
3
|
+
* Reads JSONL session files from ~/.claude/projects/.
|
|
4
|
+
*
|
|
5
|
+
* Implements the full SessionProviderBase interface with incremental
|
|
6
|
+
* reading via ClaudeCodeReader, subagent scanning, and cross-session search.
|
|
7
|
+
*
|
|
8
|
+
* Ported from sidekick-vscode/src/services/providers/ClaudeCodeSessionProvider.ts
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { StringDecoder } from 'string_decoder';
|
|
13
|
+
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
14
|
+
import { readSessionFileStats } from '../sessionStats';
|
|
15
|
+
import { JsonlParser } from '../parsers/jsonl';
|
|
16
|
+
import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
17
|
+
import { ProviderDiagnosticTracker } from './diagnostics';
|
|
18
|
+
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
19
|
+
import { encodeWorkspacePath as encodeWsPath, getSessionDirectory as getSessionDir, discoverSessionDirectory as discoverSessionDir, findActiveSession as findActiveSessionPath, findAllSessions as findAllSessionPaths, findSessionsInDirectory as findSessionsInDir, decodeEncodedPath, getAllProjectFolders as getAllProjectFoldersRaw, findSessionFilesWithStats, } from '../parsers/sessionPathResolver';
|
|
20
|
+
import { scanSubagentDir } from '../parsers/subagentScanner';
|
|
21
|
+
import { getModelContextWindowSize } from '../modelContext';
|
|
22
|
+
import { extractSessionEvents } from '../schemas/sessionEvent';
|
|
23
|
+
import { getClaudeProjectsDir } from '../claudePaths';
|
|
24
|
+
import { listClaudeSessionFilesAsync, workspaceDirectoryFilter } from './claudeListing';
|
|
25
|
+
/** Type guard for content blocks with a `type` string property */
|
|
26
|
+
function isTypedBlock(block) {
|
|
27
|
+
return (block !== null &&
|
|
28
|
+
typeof block === 'object' &&
|
|
29
|
+
typeof block.type === 'string');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extracts searchable text from a session event object.
|
|
33
|
+
*/
|
|
34
|
+
function extractSearchableText(event) {
|
|
35
|
+
const content = event.message?.content;
|
|
36
|
+
if (!content)
|
|
37
|
+
return '';
|
|
38
|
+
if (typeof content === 'string')
|
|
39
|
+
return content;
|
|
40
|
+
if (Array.isArray(content)) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const block of content) {
|
|
43
|
+
if (block && typeof block === 'object') {
|
|
44
|
+
const b = block;
|
|
45
|
+
if (typeof b.text === 'string')
|
|
46
|
+
parts.push(b.text);
|
|
47
|
+
if (typeof b.thinking === 'string')
|
|
48
|
+
parts.push(b.thinking);
|
|
49
|
+
if (typeof b.content === 'string')
|
|
50
|
+
parts.push(b.content);
|
|
51
|
+
if (b.input && typeof b.input === 'object')
|
|
52
|
+
parts.push(JSON.stringify(b.input));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return parts.join(' ');
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Project-directory filter for a workspace: the encoded workspace path, a
|
|
61
|
+
* worktree suffix of it, or a directory ending in the workspace's basename.
|
|
62
|
+
* Without a workspace every directory matches.
|
|
63
|
+
*/
|
|
64
|
+
function extractClaudeLabelFromPrefix(chunk) {
|
|
65
|
+
for (const line of chunk.split('\n')) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const event = JSON.parse(trimmed);
|
|
71
|
+
if (event.type !== 'user')
|
|
72
|
+
continue;
|
|
73
|
+
const content = event.message?.content;
|
|
74
|
+
if (!content)
|
|
75
|
+
continue;
|
|
76
|
+
let text = null;
|
|
77
|
+
if (typeof content === 'string') {
|
|
78
|
+
text = content.trim();
|
|
79
|
+
}
|
|
80
|
+
else if (Array.isArray(content)) {
|
|
81
|
+
const textBlock = content.find((block) => isTypedBlock(block) &&
|
|
82
|
+
block.type === 'text' &&
|
|
83
|
+
typeof block.text === 'string' &&
|
|
84
|
+
block.text.trim().length > 0);
|
|
85
|
+
if (textBlock && isTypedBlock(textBlock) && typeof textBlock.text === 'string') {
|
|
86
|
+
text = textBlock.text.trim();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (text) {
|
|
90
|
+
const compact = text.replace(/\s+/g, ' ');
|
|
91
|
+
return compact.length > 60 ? compact.substring(0, 57) + '...' : compact;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Skip malformed lines.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Incremental JSONL reader for Claude Code session files.
|
|
102
|
+
*
|
|
103
|
+
* Tracks byte position in the file and uses JsonlParser for
|
|
104
|
+
* streaming line-buffered parsing of new content.
|
|
105
|
+
*/
|
|
106
|
+
class ClaudeCodeReader {
|
|
107
|
+
sessionPath;
|
|
108
|
+
parser;
|
|
109
|
+
filePosition = 0;
|
|
110
|
+
/** Cursor after the last complete line, safe to persist across restarts. */
|
|
111
|
+
committedPosition = 0;
|
|
112
|
+
events = [];
|
|
113
|
+
_wasTruncated = false;
|
|
114
|
+
decoder = new StringDecoder('utf8');
|
|
115
|
+
/** Split lines of one response repeat its usage; count each response once. */
|
|
116
|
+
usageDeduper;
|
|
117
|
+
constructor(sessionPath, usageDeduper) {
|
|
118
|
+
this.sessionPath = sessionPath;
|
|
119
|
+
this.usageDeduper = usageDeduper ?? new ClaudeUsageDeduper();
|
|
120
|
+
this.parser = new JsonlParser({
|
|
121
|
+
onEvent: (raw) => {
|
|
122
|
+
for (const event of extractSessionEvents(raw)) {
|
|
123
|
+
this.events.push(this.usageDeduper.apply(normalizeClaudeUsage(event)));
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
onError: (_err, _line) => {
|
|
127
|
+
// Silently skip parse errors — no logging framework dependency
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
readNew() {
|
|
132
|
+
this.events = [];
|
|
133
|
+
this._wasTruncated = false;
|
|
134
|
+
try {
|
|
135
|
+
if (!fs.existsSync(this.sessionPath)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const stats = fs.statSync(this.sessionPath);
|
|
139
|
+
const currentSize = stats.size;
|
|
140
|
+
// Handle truncation
|
|
141
|
+
if (currentSize < this.filePosition) {
|
|
142
|
+
this._wasTruncated = true;
|
|
143
|
+
this.filePosition = 0;
|
|
144
|
+
this.committedPosition = 0;
|
|
145
|
+
this.parser.reset();
|
|
146
|
+
this.decoder = new StringDecoder('utf8');
|
|
147
|
+
this.usageDeduper.reset();
|
|
148
|
+
}
|
|
149
|
+
// No new content
|
|
150
|
+
if (currentSize <= this.filePosition) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
// Read new bytes from last position
|
|
154
|
+
const fd = fs.openSync(this.sessionPath, 'r');
|
|
155
|
+
const bufferSize = currentSize - this.filePosition;
|
|
156
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
157
|
+
let bytesRead = 0;
|
|
158
|
+
try {
|
|
159
|
+
bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
fs.closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
165
|
+
const newline = bytes.lastIndexOf(0x0a);
|
|
166
|
+
if (newline >= 0)
|
|
167
|
+
this.committedPosition = this.filePosition + newline + 1;
|
|
168
|
+
const chunk = this.decoder.write(bytes);
|
|
169
|
+
this.parser.processChunk(chunk);
|
|
170
|
+
this.filePosition += bytesRead;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
|
|
174
|
+
}
|
|
175
|
+
return this.events;
|
|
176
|
+
}
|
|
177
|
+
readAll() {
|
|
178
|
+
this.reset();
|
|
179
|
+
return this.readNew();
|
|
180
|
+
}
|
|
181
|
+
reset() {
|
|
182
|
+
this.filePosition = 0;
|
|
183
|
+
this.committedPosition = 0;
|
|
184
|
+
this.parser.reset();
|
|
185
|
+
this.decoder = new StringDecoder('utf8');
|
|
186
|
+
this.usageDeduper.reset();
|
|
187
|
+
this._wasTruncated = false;
|
|
188
|
+
}
|
|
189
|
+
exists() {
|
|
190
|
+
return fs.existsSync(this.sessionPath);
|
|
191
|
+
}
|
|
192
|
+
flush() {
|
|
193
|
+
const finalChunk = this.decoder.end();
|
|
194
|
+
if (finalChunk)
|
|
195
|
+
this.parser.processChunk(finalChunk);
|
|
196
|
+
this.parser.flush();
|
|
197
|
+
this.decoder = new StringDecoder('utf8');
|
|
198
|
+
}
|
|
199
|
+
getPosition() {
|
|
200
|
+
return this.committedPosition;
|
|
201
|
+
}
|
|
202
|
+
seekTo(position) {
|
|
203
|
+
this.filePosition = position;
|
|
204
|
+
this.committedPosition = position;
|
|
205
|
+
this.parser.reset();
|
|
206
|
+
this.decoder = new StringDecoder('utf8');
|
|
207
|
+
}
|
|
208
|
+
wasTruncated() {
|
|
209
|
+
return this._wasTruncated;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Session provider for Claude Code CLI.
|
|
214
|
+
*
|
|
215
|
+
* Implements the full SessionProviderBase interface, delegating path
|
|
216
|
+
* resolution to sessionPathResolver, parsing to JsonlParser, and
|
|
217
|
+
* subagent scanning to subagentScanner.
|
|
218
|
+
*/
|
|
219
|
+
export class ClaudeCodeProvider {
|
|
220
|
+
id = 'claude-code';
|
|
221
|
+
displayName = 'Claude Code';
|
|
222
|
+
diagnostics;
|
|
223
|
+
/** Runtime-reported context window limit (overrides static map when set). */
|
|
224
|
+
dynamicContextWindowLimit = null;
|
|
225
|
+
/** Project-directory listings reused across async enumerations. */
|
|
226
|
+
directoryCache = new DirectoryListingCache();
|
|
227
|
+
constructor(options = {}) {
|
|
228
|
+
this.diagnostics = new ProviderDiagnosticTracker(this.id, options);
|
|
229
|
+
}
|
|
230
|
+
// --- Path resolution ---
|
|
231
|
+
getSessionDirectory(workspacePath) {
|
|
232
|
+
return getSessionDir(workspacePath);
|
|
233
|
+
}
|
|
234
|
+
discoverSessionDirectory(workspacePath) {
|
|
235
|
+
return discoverSessionDir(workspacePath);
|
|
236
|
+
}
|
|
237
|
+
// --- Session discovery ---
|
|
238
|
+
findActiveSession(workspacePath) {
|
|
239
|
+
return findActiveSessionPath(workspacePath);
|
|
240
|
+
}
|
|
241
|
+
findAllSessions(workspacePath) {
|
|
242
|
+
return findAllSessionPaths(workspacePath);
|
|
243
|
+
}
|
|
244
|
+
findSessionById(workspacePath, sessionId) {
|
|
245
|
+
const normalizedId = sessionId.trim();
|
|
246
|
+
if (!normalizedId || path.basename(normalizedId) !== normalizedId)
|
|
247
|
+
return null;
|
|
248
|
+
try {
|
|
249
|
+
const directories = new Set();
|
|
250
|
+
directories.add(this.getSessionDirectory(workspacePath));
|
|
251
|
+
const discovered = this.discoverSessionDirectory(workspacePath);
|
|
252
|
+
if (discovered)
|
|
253
|
+
directories.add(discovered);
|
|
254
|
+
for (const folder of this.getAllProjectFolders(workspacePath))
|
|
255
|
+
directories.add(folder.dir);
|
|
256
|
+
for (const directory of directories) {
|
|
257
|
+
const direct = path.join(directory, `${normalizedId}.jsonl`);
|
|
258
|
+
try {
|
|
259
|
+
if (fs.statSync(direct).isFile()) {
|
|
260
|
+
this.diagnostics.available('findSessionById');
|
|
261
|
+
return direct;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Continue with directory-shaped lookup for worktree layouts.
|
|
266
|
+
}
|
|
267
|
+
const match = this.findSessionsInDirectory(directory).find((candidate) => this.getSessionId(candidate) === normalizedId);
|
|
268
|
+
if (match) {
|
|
269
|
+
this.diagnostics.available('findSessionById');
|
|
270
|
+
return match;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.recordHomeStatus('findSessionById');
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
this.recordHomeStatus('findSessionById');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Backward-compatible alias for findAllSessions. */
|
|
282
|
+
findSessionFiles(workspacePath) {
|
|
283
|
+
return this.findAllSessions(workspacePath);
|
|
284
|
+
}
|
|
285
|
+
findSessionsInDirectory(dir) {
|
|
286
|
+
return findSessionsInDir(dir);
|
|
287
|
+
}
|
|
288
|
+
getAllProjectFolders(workspacePath) {
|
|
289
|
+
return getAllProjectFoldersRaw(workspacePath);
|
|
290
|
+
}
|
|
291
|
+
listAllSessionFiles() {
|
|
292
|
+
const results = [];
|
|
293
|
+
const seen = new Set();
|
|
294
|
+
for (const folder of this.getAllProjectFolders()) {
|
|
295
|
+
// The directory listing already stat'ed each file; reuse it instead of
|
|
296
|
+
// stat'ing every session a second time.
|
|
297
|
+
for (const file of findSessionFilesWithStats(folder.dir)) {
|
|
298
|
+
if (seen.has(file.path))
|
|
299
|
+
continue;
|
|
300
|
+
seen.add(file.path);
|
|
301
|
+
results.push({ path: file.path, mtime: file.mtime, sizeBytes: file.sizeBytes });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
this.recordHomeStatus('listAllSessionFiles');
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Enumerate session files across project directories through the
|
|
309
|
+
* directory cache: one stat per project directory, a readdir only for
|
|
310
|
+
* directories whose mtime changed, and a stat only for files new to a
|
|
311
|
+
* changed directory (or recently modified, with `revalidateRecent`).
|
|
312
|
+
*
|
|
313
|
+
* With `limit`, directories are visited newest-mtime first and the walk
|
|
314
|
+
* stops once the remaining directories are older than the newest `limit`
|
|
315
|
+
* files found. A directory's mtime ignores appends, so a cold limited walk
|
|
316
|
+
* can miss a long-running session in an old directory until its next watch
|
|
317
|
+
* event; `since` filters files and shares the caveat.
|
|
318
|
+
*/
|
|
319
|
+
async listSessionFilesAsync(workspacePath, options = {}) {
|
|
320
|
+
try {
|
|
321
|
+
return await listClaudeSessionFilesAsync(workspacePath, options, this.directoryCache);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
this.recordHomeStatus('listSessionFilesAsync');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Resolve a recursive-watch event under the projects root with one stat.
|
|
329
|
+
* Sessions live exactly one directory below the root; deeper paths are
|
|
330
|
+
* subagent transcripts and are ignored. A path at the root itself is a
|
|
331
|
+
* project directory appearing or vanishing, which needs a full listing.
|
|
332
|
+
*/
|
|
333
|
+
async statWatchedSessionFile(root, relativePath, workspacePath) {
|
|
334
|
+
const segments = splitWatchedPath(relativePath);
|
|
335
|
+
if (!segments)
|
|
336
|
+
return { status: 'unknown' };
|
|
337
|
+
if (segments.length > 2)
|
|
338
|
+
return { status: 'ignored' };
|
|
339
|
+
const fullPath = path.join(root, ...segments);
|
|
340
|
+
if (segments.length === 1) {
|
|
341
|
+
this.directoryCache.invalidate(fullPath);
|
|
342
|
+
try {
|
|
343
|
+
const stat = await fs.promises.stat(fullPath);
|
|
344
|
+
return stat.isDirectory() ? { status: 'unknown' } : { status: 'ignored' };
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return { status: 'unknown' };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const [directoryName, fileName] = segments;
|
|
351
|
+
if (!this.isSessionFile(fileName))
|
|
352
|
+
return { status: 'ignored' };
|
|
353
|
+
if (!workspaceDirectoryFilter(workspacePath)(directoryName))
|
|
354
|
+
return { status: 'ignored' };
|
|
355
|
+
const sessionId = this.getSessionId(fullPath);
|
|
356
|
+
const stat = await this.directoryCache.refreshFile(fullPath);
|
|
357
|
+
if (!stat || stat.sizeBytes <= 0)
|
|
358
|
+
return { status: 'missing', path: fullPath, sessionId };
|
|
359
|
+
return {
|
|
360
|
+
status: 'present',
|
|
361
|
+
file: {
|
|
362
|
+
path: fullPath,
|
|
363
|
+
mtime: new Date(stat.mtimeMs),
|
|
364
|
+
sizeBytes: stat.sizeBytes,
|
|
365
|
+
sessionId,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// --- File identification ---
|
|
370
|
+
isSessionFile(filename) {
|
|
371
|
+
return filename.endsWith('.jsonl');
|
|
372
|
+
}
|
|
373
|
+
getSessionId(sessionPath) {
|
|
374
|
+
return path.basename(sessionPath, '.jsonl');
|
|
375
|
+
}
|
|
376
|
+
encodeWorkspacePath(workspacePath) {
|
|
377
|
+
return encodeWsPath(workspacePath);
|
|
378
|
+
}
|
|
379
|
+
extractSessionLabel(sessionPath) {
|
|
380
|
+
try {
|
|
381
|
+
const fd = fs.openSync(sessionPath, 'r');
|
|
382
|
+
const buffer = Buffer.alloc(8192);
|
|
383
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 8192, 0);
|
|
384
|
+
fs.closeSync(fd);
|
|
385
|
+
if (bytesRead === 0)
|
|
386
|
+
return null;
|
|
387
|
+
return extractClaudeLabelFromPrefix(buffer.toString('utf-8', 0, bytesRead));
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async extractSessionLabelsAsync(sessionPaths) {
|
|
394
|
+
const results = new Map();
|
|
395
|
+
for (const sessionPath of sessionPaths) {
|
|
396
|
+
let handle = null;
|
|
397
|
+
try {
|
|
398
|
+
handle = await fs.promises.open(sessionPath, 'r');
|
|
399
|
+
const buffer = Buffer.alloc(8192);
|
|
400
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
401
|
+
results.set(sessionPath, extractClaudeLabelFromPrefix(buffer.toString('utf8', 0, bytesRead)));
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
results.set(sessionPath, null);
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
await handle?.close().catch(() => undefined);
|
|
408
|
+
}
|
|
409
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
410
|
+
}
|
|
411
|
+
return results;
|
|
412
|
+
}
|
|
413
|
+
// --- Data reading ---
|
|
414
|
+
createReader(sessionPath) {
|
|
415
|
+
return new ClaudeCodeReader(sessionPath);
|
|
416
|
+
}
|
|
417
|
+
// --- Subagent support ---
|
|
418
|
+
scanSubagents(sessionDir, sessionId) {
|
|
419
|
+
return scanSubagentDir(sessionDir, sessionId);
|
|
420
|
+
}
|
|
421
|
+
// --- Cross-session search ---
|
|
422
|
+
searchInSession(sessionPath, query, maxResults) {
|
|
423
|
+
const results = [];
|
|
424
|
+
const queryLower = query.toLowerCase();
|
|
425
|
+
try {
|
|
426
|
+
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
427
|
+
const lines = content.split('\n');
|
|
428
|
+
const projectDir = path.basename(path.dirname(sessionPath));
|
|
429
|
+
const projectPath = decodeEncodedPath(projectDir);
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
if (results.length >= maxResults)
|
|
432
|
+
break;
|
|
433
|
+
if (!line.trim() || !line.toLowerCase().includes(queryLower))
|
|
434
|
+
continue;
|
|
435
|
+
try {
|
|
436
|
+
const event = JSON.parse(line);
|
|
437
|
+
const text = extractSearchableText(event);
|
|
438
|
+
if (!text)
|
|
439
|
+
continue;
|
|
440
|
+
const textLower = text.toLowerCase();
|
|
441
|
+
const matchIdx = textLower.indexOf(queryLower);
|
|
442
|
+
if (matchIdx < 0)
|
|
443
|
+
continue;
|
|
444
|
+
const start = Math.max(0, matchIdx - 40);
|
|
445
|
+
const end = Math.min(text.length, matchIdx + query.length + 40);
|
|
446
|
+
const snippet = (start > 0 ? '...' : '') +
|
|
447
|
+
text.substring(start, end) +
|
|
448
|
+
(end < text.length ? '...' : '');
|
|
449
|
+
results.push({
|
|
450
|
+
sessionPath,
|
|
451
|
+
line: snippet.replace(/\n/g, ' '),
|
|
452
|
+
eventType: event.type || 'unknown',
|
|
453
|
+
timestamp: event.timestamp || '',
|
|
454
|
+
projectPath,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Skip malformed JSON
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Skip unreadable files
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
}
|
|
467
|
+
getProjectsBaseDir() {
|
|
468
|
+
return getClaudeProjectsDir();
|
|
469
|
+
}
|
|
470
|
+
getWatchRoots() {
|
|
471
|
+
return [this.getProjectsBaseDir()];
|
|
472
|
+
}
|
|
473
|
+
// --- Stats ---
|
|
474
|
+
readSessionStats(sessionPath) {
|
|
475
|
+
// One reader pass through the shared aggregator: cache-inclusive per-model
|
|
476
|
+
// totals, cost with provenance, a tool success/failure split, and the label
|
|
477
|
+
// from the first user prompt in the events already read.
|
|
478
|
+
return readSessionFileStats(this, sessionPath);
|
|
479
|
+
}
|
|
480
|
+
readSessionContextSnapshot(sessionPath, options = {}) {
|
|
481
|
+
return readSessionContextSnapshot(this, sessionPath, options);
|
|
482
|
+
}
|
|
483
|
+
// --- Optional methods ---
|
|
484
|
+
getRuntimeStatus() {
|
|
485
|
+
try {
|
|
486
|
+
fs.accessSync(this.getProjectsBaseDir(), fs.constants.R_OK);
|
|
487
|
+
return { available: true, kind: 'available' };
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return {
|
|
491
|
+
available: false,
|
|
492
|
+
kind: 'home_unavailable',
|
|
493
|
+
message: 'Claude Code session home is unavailable.',
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
getLastOperationStatus() {
|
|
498
|
+
return this.diagnostics.getLastOperationStatus();
|
|
499
|
+
}
|
|
500
|
+
getContextWindowLimit(modelId) {
|
|
501
|
+
if (this.dynamicContextWindowLimit)
|
|
502
|
+
return this.dynamicContextWindowLimit;
|
|
503
|
+
return getModelContextWindowSize(modelId);
|
|
504
|
+
}
|
|
505
|
+
/** Set a runtime-reported context window limit (overrides static map). */
|
|
506
|
+
setDynamicContextWindowLimit(limit) {
|
|
507
|
+
this.dynamicContextWindowLimit = limit;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Returns the latest assistant message's token usage snapshot.
|
|
511
|
+
*
|
|
512
|
+
* Reads the session JSONL file backwards to find the most recent assistant
|
|
513
|
+
* message with usage data, avoiding the need to parse the entire file.
|
|
514
|
+
*/
|
|
515
|
+
getCurrentUsageSnapshot(sessionPath) {
|
|
516
|
+
try {
|
|
517
|
+
if (!fs.existsSync(sessionPath))
|
|
518
|
+
return null;
|
|
519
|
+
// Read the last portion of the file to find the most recent assistant message
|
|
520
|
+
const stats = fs.statSync(sessionPath);
|
|
521
|
+
const readSize = Math.min(stats.size, 64 * 1024); // Last 64KB
|
|
522
|
+
const fd = fs.openSync(sessionPath, 'r');
|
|
523
|
+
const buffer = Buffer.alloc(readSize);
|
|
524
|
+
fs.readSync(fd, buffer, 0, readSize, stats.size - readSize);
|
|
525
|
+
fs.closeSync(fd);
|
|
526
|
+
const chunk = buffer.toString('utf-8');
|
|
527
|
+
const lines = chunk.split('\n').reverse();
|
|
528
|
+
for (const line of lines) {
|
|
529
|
+
const trimmed = line.trim();
|
|
530
|
+
if (!trimmed || !trimmed.startsWith('{'))
|
|
531
|
+
continue;
|
|
532
|
+
try {
|
|
533
|
+
const event = JSON.parse(trimmed);
|
|
534
|
+
if (event.type === 'assistant' && event.message?.usage) {
|
|
535
|
+
const u = event.message.usage;
|
|
536
|
+
return {
|
|
537
|
+
inputTokens: u.input_tokens || 0,
|
|
538
|
+
outputTokens: u.output_tokens || 0,
|
|
539
|
+
cacheWriteTokens: u.cache_creation_input_tokens || 0,
|
|
540
|
+
cacheReadTokens: u.cache_read_input_tokens || 0,
|
|
541
|
+
model: event.message.model || 'unknown',
|
|
542
|
+
timestamp: new Date(event.timestamp || Date.now()),
|
|
543
|
+
reportedCost: u.reported_cost,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// Skip malformed lines
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// --- Lifecycle ---
|
|
558
|
+
dispose() {
|
|
559
|
+
this.dynamicContextWindowLimit = null;
|
|
560
|
+
this.directoryCache.invalidate();
|
|
561
|
+
}
|
|
562
|
+
recordHomeStatus(operation) {
|
|
563
|
+
const status = this.getRuntimeStatus();
|
|
564
|
+
if (status.available) {
|
|
565
|
+
this.diagnostics.available(operation);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.diagnostics.degraded(operation, status, {
|
|
569
|
+
kind: 'home_unavailable',
|
|
570
|
+
severity: 'info',
|
|
571
|
+
phase: 'enumerate',
|
|
572
|
+
message: status.message ?? 'Claude Code session home is unavailable.',
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code session-file listing, kept apart from the provider class so a
|
|
3
|
+
* caller that only lists files does not bundle the provider's stats,
|
|
4
|
+
* context and transcript code.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getClaudeProjectsDir } from '../claudePaths';
|
|
9
|
+
import { encodeWorkspacePath as encodeWsPath } from '../parsers/sessionPathResolver';
|
|
10
|
+
import { DirectoryListingCache } from './directoryListingCache';
|
|
11
|
+
/** Matches project directory names that may hold sessions for `workspacePath`. */
|
|
12
|
+
export function workspaceDirectoryFilter(workspacePath) {
|
|
13
|
+
if (!workspacePath)
|
|
14
|
+
return () => true;
|
|
15
|
+
const encodedWorkspace = encodeWsPath(workspacePath).toLowerCase();
|
|
16
|
+
const workspaceBasename = path
|
|
17
|
+
.basename(workspacePath)
|
|
18
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
return (directoryName) => {
|
|
21
|
+
const name = directoryName.toLowerCase();
|
|
22
|
+
return (name === encodedWorkspace ||
|
|
23
|
+
name.startsWith(`${encodedWorkspace}-`) ||
|
|
24
|
+
name === workspaceBasename ||
|
|
25
|
+
name.endsWith(`-${workspaceBasename}`));
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Enumerate session files across project directories through the
|
|
30
|
+
* directory cache: one stat per project directory, a readdir only for
|
|
31
|
+
* directories whose mtime changed, and a stat only for files new to a
|
|
32
|
+
* changed directory (or recently modified, with `revalidateRecent`).
|
|
33
|
+
*
|
|
34
|
+
* With `limit`, directories are visited newest-mtime first and the walk
|
|
35
|
+
* stops once the remaining directories are older than the newest `limit`
|
|
36
|
+
* files found. A directory's mtime ignores appends, so a cold limited walk
|
|
37
|
+
* can miss a long-running session in an old directory until its next watch
|
|
38
|
+
* event; `since` filters files and shares the caveat.
|
|
39
|
+
*/
|
|
40
|
+
export async function listClaudeSessionFilesAsync(workspacePath, options = {}, cache = new DirectoryListingCache()) {
|
|
41
|
+
const projectsRoot = getClaudeProjectsDir();
|
|
42
|
+
if (options.stats) {
|
|
43
|
+
options.stats.directoriesListed ??= 0;
|
|
44
|
+
options.stats.directoriesStatted ??= 0;
|
|
45
|
+
options.stats.filesStatted ??= 0;
|
|
46
|
+
}
|
|
47
|
+
const root = await cache.listDirectory(projectsRoot, () => false, {
|
|
48
|
+
revalidateRecent: options.revalidateRecent,
|
|
49
|
+
stats: options.stats,
|
|
50
|
+
});
|
|
51
|
+
if (!root) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const limit = options.limit !== undefined && Number.isFinite(options.limit)
|
|
55
|
+
? Math.max(0, Math.floor(options.limit))
|
|
56
|
+
: null;
|
|
57
|
+
if (limit === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const since = options.since !== undefined && Number.isFinite(options.since) ? options.since : null;
|
|
61
|
+
const matchesWorkspace = workspaceDirectoryFilter(workspacePath);
|
|
62
|
+
const candidates = [];
|
|
63
|
+
let sinceYield = 0;
|
|
64
|
+
for (const name of root.subdirectories) {
|
|
65
|
+
if (!matchesWorkspace(name))
|
|
66
|
+
continue;
|
|
67
|
+
const directory = path.join(projectsRoot, name);
|
|
68
|
+
try {
|
|
69
|
+
const stat = await fs.promises.stat(directory);
|
|
70
|
+
if (options.stats) {
|
|
71
|
+
options.stats.directoriesStatted = (options.stats.directoriesStatted ?? 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
if (stat.isDirectory())
|
|
74
|
+
candidates.push({ directory, stat });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (++sinceYield >= 50) {
|
|
80
|
+
sinceYield = 0;
|
|
81
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (limit !== null)
|
|
85
|
+
candidates.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
|
|
86
|
+
const byNewest = (left, right) => right.mtime.getTime() - left.mtime.getTime();
|
|
87
|
+
const results = [];
|
|
88
|
+
let cutoffMs = null;
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (limit !== null && cutoffMs !== null && candidate.stat.mtimeMs < cutoffMs)
|
|
91
|
+
break;
|
|
92
|
+
const listing = await cache.listDirectory(candidate.directory, (name) => name.endsWith('.jsonl'), {
|
|
93
|
+
dirStat: candidate.stat,
|
|
94
|
+
revalidateRecent: options.revalidateRecent,
|
|
95
|
+
stats: options.stats,
|
|
96
|
+
});
|
|
97
|
+
if (!listing)
|
|
98
|
+
continue;
|
|
99
|
+
for (const [name, stat] of listing.files) {
|
|
100
|
+
if (stat.sizeBytes <= 0)
|
|
101
|
+
continue;
|
|
102
|
+
if (since !== null && stat.mtimeMs < since)
|
|
103
|
+
continue;
|
|
104
|
+
const sessionPath = path.join(candidate.directory, name);
|
|
105
|
+
results.push({
|
|
106
|
+
path: sessionPath,
|
|
107
|
+
mtime: new Date(stat.mtimeMs),
|
|
108
|
+
sizeBytes: stat.sizeBytes,
|
|
109
|
+
sessionId: path.basename(sessionPath, '.jsonl'),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (limit !== null && results.length >= limit) {
|
|
113
|
+
results.sort(byNewest);
|
|
114
|
+
results.length = limit;
|
|
115
|
+
cutoffMs = results[limit - 1].mtime.getTime();
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
118
|
+
}
|
|
119
|
+
results.sort(byNewest);
|
|
120
|
+
return limit !== null && results.length > limit ? results.slice(0, limit) : results;
|
|
121
|
+
}
|