sidekick-shared 0.26.4 → 0.26.6
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 +52 -0
- package/README.md +59 -51
- package/dist/accountChanges.js +21 -2
- package/dist/accountHealth.d.ts +72 -0
- package/dist/accountHealth.js +286 -0
- package/dist/accountLaunch.d.ts +30 -0
- package/dist/accountLaunch.js +124 -0
- package/dist/accountManager.d.ts +33 -8
- package/dist/accountManager.js +183 -50
- package/dist/accountRegistry.d.ts +10 -0
- package/dist/accountRegistry.js +42 -22
- package/dist/accountSwitch.d.ts +43 -0
- package/dist/accountSwitch.js +115 -0
- package/dist/accountSync.d.ts +30 -0
- package/dist/accountSync.js +180 -0
- package/dist/accountSyncTypes.d.ts +25 -0
- package/dist/accountSyncTypes.js +7 -0
- package/dist/accounts.d.ts +58 -6
- package/dist/accounts.js +408 -189
- package/dist/claudeCredentials.d.ts +38 -0
- package/dist/claudeCredentials.js +74 -0
- package/dist/claudeProfiles.d.ts +40 -0
- package/dist/claudeProfiles.js +102 -10
- package/dist/codexAuth.d.ts +32 -0
- package/dist/codexAuth.js +127 -0
- package/dist/codexPaths.d.ts +21 -0
- package/dist/codexPaths.js +117 -0
- package/dist/codexProfiles.d.ts +58 -5
- package/dist/codexProfiles.js +574 -289
- package/dist/credentialIO.d.ts +12 -5
- package/dist/credentialIO.js +63 -29
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.js +28 -32
- package/dist/ensureDefaultAccounts.d.ts +8 -0
- package/dist/ensureDefaultAccounts.js +42 -82
- package/dist/index.d.ts +31 -12
- package/dist/index.js +68 -13
- package/dist/keepAlive.d.ts +44 -0
- package/dist/keepAlive.js +106 -0
- package/dist/node.d.ts +4 -1
- package/dist/node.js +4 -1
- package/dist/observedSessionCollector.d.ts +117 -5
- package/dist/observedSessionCollector.js +483 -76
- package/dist/processDetection.d.ts +32 -0
- package/dist/processDetection.js +210 -0
- package/dist/providers/claudeCode.d.ts +23 -2
- package/dist/providers/claudeCode.js +143 -51
- package/dist/providers/codex.d.ts +10 -2
- package/dist/providers/codex.js +95 -12
- package/dist/providers/directoryListingCache.d.ts +85 -0
- package/dist/providers/directoryListingCache.js +244 -0
- package/dist/providers/rolloutWalker.d.ts +28 -5
- package/dist/providers/rolloutWalker.js +100 -16
- package/dist/providers/types.d.ts +48 -1
- package/dist/schemas/accountManager.d.ts +283 -1
- package/dist/schemas/accountManager.js +122 -1
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/schemas/index.d.ts +6 -1
- package/dist/schemas/index.js +14 -1
- package/dist/schemas/observedSessionV1.d.ts +21 -21
- package/dist/schemas/quota.d.ts +18 -18
- package/dist/schemas/quotaHistory.d.ts +1 -1
- package/dist/schemas/stateFile.d.ts +5 -5
- package/dist/sessionMonitor.js +24 -2
- package/dist/terminalSync.d.ts +5 -5
- package/dist/terminalSync.js +7 -112
- package/dist/writers/atomic.d.ts +2 -0
- package/dist/writers/atomic.js +49 -2
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RunningProcess {
|
|
2
|
+
pid: number;
|
|
3
|
+
/** The matched name (as passed to the finder). */
|
|
4
|
+
name: string;
|
|
5
|
+
/** The executable as reported by the OS (basename or image name). */
|
|
6
|
+
command: string;
|
|
7
|
+
}
|
|
8
|
+
export type AccountConsumerKind = 'claude-cli' | 'claude-desktop' | 'codex-cli' | 'codex-app' | 'vscode-extension-host';
|
|
9
|
+
export interface RunningAccountConsumer {
|
|
10
|
+
kind: AccountConsumerKind;
|
|
11
|
+
pids: number[];
|
|
12
|
+
/** Whether a live-store switch reaches this consumer once it restarts. */
|
|
13
|
+
switched: boolean;
|
|
14
|
+
/** The user-facing sentence both UIs print for this consumer. */
|
|
15
|
+
reachability: string;
|
|
16
|
+
}
|
|
17
|
+
/** Sync sibling of {@link findRunningProcesses}; blocks up to 4 s. */
|
|
18
|
+
export declare function findRunningProcessesSync(names: string[]): RunningProcess[];
|
|
19
|
+
export declare function findRunningProcesses(names: string[]): Promise<RunningProcess[]>;
|
|
20
|
+
export declare const ACCOUNT_CONSUMER_REACHABILITY: Record<AccountConsumerKind, {
|
|
21
|
+
switched: boolean;
|
|
22
|
+
reachability: string;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Which credential consumers are running right now, with the reachability
|
|
26
|
+
* sentence to show for each. Optionally scoped to one provider.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectRunningAccountConsumers(provider?: 'claude-code' | 'codex'): Promise<RunningAccountConsumer[]>;
|
|
29
|
+
/** Sync sibling of {@link detectRunningAccountConsumers}; blocks up to 4 s. */
|
|
30
|
+
export declare function detectRunningAccountConsumersSync(provider?: 'claude-code' | 'codex'): RunningAccountConsumer[];
|
|
31
|
+
/** Build a consumer entry for a kind the host detects itself (e.g. its own extension host). */
|
|
32
|
+
export declare function describeAccountConsumer(kind: AccountConsumerKind, pids?: number[]): RunningAccountConsumer;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ACCOUNT_CONSUMER_REACHABILITY = void 0;
|
|
37
|
+
exports.findRunningProcessesSync = findRunningProcessesSync;
|
|
38
|
+
exports.findRunningProcesses = findRunningProcesses;
|
|
39
|
+
exports.detectRunningAccountConsumers = detectRunningAccountConsumers;
|
|
40
|
+
exports.detectRunningAccountConsumersSync = detectRunningAccountConsumersSync;
|
|
41
|
+
exports.describeAccountConsumer = describeAccountConsumer;
|
|
42
|
+
/**
|
|
43
|
+
* Cross-platform detection of running CLI/app processes that hold provider
|
|
44
|
+
* credentials in memory. Bounded, never throws, never spawns a shell.
|
|
45
|
+
*/
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const PROBE_TIMEOUT_MS = 4000;
|
|
49
|
+
const CLAUDE_NPM_ENTRY = /claude-code[\\/]cli\.js/;
|
|
50
|
+
function parseUnixTable(stdout) {
|
|
51
|
+
const rows = [];
|
|
52
|
+
for (const line of stdout.split('\n')) {
|
|
53
|
+
const match = line.match(/^\s*(\d+)\s+(\S+)\s*(.*)$/);
|
|
54
|
+
if (!match)
|
|
55
|
+
continue;
|
|
56
|
+
rows.push({ pid: Number(match[1]), command: match[2], args: match[3] ?? '' });
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
function parseWindowsCsv(stdout) {
|
|
61
|
+
const rows = [];
|
|
62
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
63
|
+
const match = line.match(/^"([^"]+)","(\d+)"/);
|
|
64
|
+
if (!match)
|
|
65
|
+
continue;
|
|
66
|
+
rows.push({ pid: Number(match[2]), command: match[1], args: '' });
|
|
67
|
+
}
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
function matchRows(rows, names) {
|
|
71
|
+
const wanted = new Set(names);
|
|
72
|
+
const found = [];
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
if (!Number.isFinite(row.pid) || row.pid === process.pid)
|
|
75
|
+
continue;
|
|
76
|
+
const base = path.basename(row.command);
|
|
77
|
+
// The native Claude Code binary is invoked through a `claude` symlink but
|
|
78
|
+
// its executable is the versioned file (`.../versions/2.1.273`), so `comm`
|
|
79
|
+
// never says `claude`; argv[0] does.
|
|
80
|
+
const argv0 = row.args.trim().split(/\s+/)[0] ?? '';
|
|
81
|
+
const candidates = [base, base.replace(/\.exe$/i, ''), path.basename(argv0)];
|
|
82
|
+
let name = candidates.find((candidate) => candidate && wanted.has(candidate));
|
|
83
|
+
// An npm-installed `claude` runs under `node`; recognise it by its entry file.
|
|
84
|
+
if (!name &&
|
|
85
|
+
wanted.has('claude') &&
|
|
86
|
+
/^node(\.exe)?$/i.test(base) &&
|
|
87
|
+
CLAUDE_NPM_ENTRY.test(row.args)) {
|
|
88
|
+
name = 'claude';
|
|
89
|
+
}
|
|
90
|
+
if (name)
|
|
91
|
+
found.push({ pid: row.pid, name, command: row.command });
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
function probeCommand() {
|
|
96
|
+
return process.platform === 'win32'
|
|
97
|
+
? { command: 'tasklist', args: ['/FO', 'CSV', '/NH'] }
|
|
98
|
+
: { command: 'ps', args: ['-axo', 'pid=,comm=,args='] };
|
|
99
|
+
}
|
|
100
|
+
function parseOutput(stdout) {
|
|
101
|
+
return process.platform === 'win32' ? parseWindowsCsv(stdout) : parseUnixTable(stdout);
|
|
102
|
+
}
|
|
103
|
+
/** Sync sibling of {@link findRunningProcesses}; blocks up to 4 s. */
|
|
104
|
+
function findRunningProcessesSync(names) {
|
|
105
|
+
if (names.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
try {
|
|
108
|
+
const { command, args } = probeCommand();
|
|
109
|
+
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
112
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
113
|
+
killSignal: 'SIGKILL',
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
if (result.error || result.status !== 0)
|
|
117
|
+
return [];
|
|
118
|
+
return matchRows(parseOutput(String(result.stdout ?? '')), names);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function findRunningProcesses(names) {
|
|
125
|
+
if (names.length === 0)
|
|
126
|
+
return Promise.resolve([]);
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
try {
|
|
129
|
+
const { command, args } = probeCommand();
|
|
130
|
+
(0, child_process_1.execFile)(command, args, {
|
|
131
|
+
encoding: 'utf8',
|
|
132
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
133
|
+
killSignal: 'SIGKILL',
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
136
|
+
}, (error, stdout) => {
|
|
137
|
+
resolve(error ? [] : matchRows(parseOutput(String(stdout ?? '')), names));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
resolve([]);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const CONSUMER_PROCESS_NAMES = {
|
|
146
|
+
'claude-cli': 'claude',
|
|
147
|
+
'claude-desktop': 'Claude',
|
|
148
|
+
'codex-cli': 'codex',
|
|
149
|
+
'codex-app': 'Codex',
|
|
150
|
+
};
|
|
151
|
+
exports.ACCOUNT_CONSUMER_REACHABILITY = {
|
|
152
|
+
'claude-cli': {
|
|
153
|
+
switched: true,
|
|
154
|
+
reachability: 'Running claude sessions keep the previous account until restarted; a running session can rotate the refresh token of the account you just left.',
|
|
155
|
+
},
|
|
156
|
+
'claude-desktop': {
|
|
157
|
+
switched: false,
|
|
158
|
+
reachability: 'Claude Desktop is not switched by sidekick (it keeps its own web session) and its embedded engine can rotate the shared CLI token.',
|
|
159
|
+
},
|
|
160
|
+
'codex-cli': {
|
|
161
|
+
switched: true,
|
|
162
|
+
reachability: 'Running codex sessions keep the previous account until restarted.',
|
|
163
|
+
},
|
|
164
|
+
'codex-app': {
|
|
165
|
+
switched: true,
|
|
166
|
+
reachability: 'The Codex app shares the CLI login; restart it to pick up the switch. While running it can refresh and rotate the live token.',
|
|
167
|
+
},
|
|
168
|
+
'vscode-extension-host': {
|
|
169
|
+
switched: true,
|
|
170
|
+
reachability: 'The Claude Code extension host keeps the previous account until the window reloads.',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
function toConsumer(kind, pids) {
|
|
174
|
+
return { kind, pids, ...exports.ACCOUNT_CONSUMER_REACHABILITY[kind] };
|
|
175
|
+
}
|
|
176
|
+
function groupConsumers(processes, kinds) {
|
|
177
|
+
const consumers = [];
|
|
178
|
+
for (const kind of kinds) {
|
|
179
|
+
const pids = processes.filter((p) => p.name === CONSUMER_PROCESS_NAMES[kind]).map((p) => p.pid);
|
|
180
|
+
if (pids.length > 0)
|
|
181
|
+
consumers.push(toConsumer(kind, pids));
|
|
182
|
+
}
|
|
183
|
+
return consumers;
|
|
184
|
+
}
|
|
185
|
+
function kindsForProvider(provider) {
|
|
186
|
+
if (provider === 'claude-code')
|
|
187
|
+
return ['claude-cli', 'claude-desktop'];
|
|
188
|
+
if (provider === 'codex')
|
|
189
|
+
return ['codex-cli', 'codex-app'];
|
|
190
|
+
return ['claude-cli', 'claude-desktop', 'codex-cli', 'codex-app'];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Which credential consumers are running right now, with the reachability
|
|
194
|
+
* sentence to show for each. Optionally scoped to one provider.
|
|
195
|
+
*/
|
|
196
|
+
async function detectRunningAccountConsumers(provider) {
|
|
197
|
+
const kinds = kindsForProvider(provider);
|
|
198
|
+
const processes = await findRunningProcesses(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
|
|
199
|
+
return groupConsumers(processes, kinds);
|
|
200
|
+
}
|
|
201
|
+
/** Sync sibling of {@link detectRunningAccountConsumers}; blocks up to 4 s. */
|
|
202
|
+
function detectRunningAccountConsumersSync(provider) {
|
|
203
|
+
const kinds = kindsForProvider(provider);
|
|
204
|
+
const processes = findRunningProcessesSync(kinds.map((kind) => CONSUMER_PROCESS_NAMES[kind]));
|
|
205
|
+
return groupConsumers(processes, kinds);
|
|
206
|
+
}
|
|
207
|
+
/** Build a consumer entry for a kind the host detects itself (e.g. its own extension host). */
|
|
208
|
+
function describeAccountConsumer(kind, pids = []) {
|
|
209
|
+
return toConsumer(kind, pids);
|
|
210
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
|
|
11
11
|
import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
|
|
12
|
-
import type { SessionProviderBase, SessionReader, SessionFileInfo, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions } from './types';
|
|
12
|
+
import type { ListSessionFilesOptions, SessionProviderBase, SessionReader, SessionFileInfo, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions, WatchedSessionFile } from './types';
|
|
13
13
|
/**
|
|
14
14
|
* Session provider for Claude Code CLI.
|
|
15
15
|
*
|
|
@@ -23,6 +23,8 @@ export declare class ClaudeCodeProvider implements SessionProviderBase {
|
|
|
23
23
|
private readonly diagnostics;
|
|
24
24
|
/** Runtime-reported context window limit (overrides static map when set). */
|
|
25
25
|
private dynamicContextWindowLimit;
|
|
26
|
+
/** Project-directory listings reused across async enumerations. */
|
|
27
|
+
private readonly directoryCache;
|
|
26
28
|
constructor(options?: SessionProviderOptions);
|
|
27
29
|
getSessionDirectory(workspacePath: string): string;
|
|
28
30
|
discoverSessionDirectory(workspacePath: string): string | null;
|
|
@@ -34,7 +36,26 @@ export declare class ClaudeCodeProvider implements SessionProviderBase {
|
|
|
34
36
|
findSessionsInDirectory(dir: string): string[];
|
|
35
37
|
getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
|
|
36
38
|
listAllSessionFiles(): SessionFileInfo[];
|
|
37
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Enumerate session files across project directories through the
|
|
41
|
+
* directory cache: one stat per project directory, a readdir only for
|
|
42
|
+
* directories whose mtime changed, and a stat only for files new to a
|
|
43
|
+
* changed directory (or recently modified, with `revalidateRecent`).
|
|
44
|
+
*
|
|
45
|
+
* With `limit`, directories are visited newest-mtime first and the walk
|
|
46
|
+
* stops once the remaining directories are older than the newest `limit`
|
|
47
|
+
* files found. A directory's mtime ignores appends, so a cold limited walk
|
|
48
|
+
* can miss a long-running session in an old directory until its next watch
|
|
49
|
+
* event; `since` filters files and shares the caveat.
|
|
50
|
+
*/
|
|
51
|
+
listSessionFilesAsync(workspacePath?: string, options?: ListSessionFilesOptions): Promise<SessionFileInfo[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a recursive-watch event under the projects root with one stat.
|
|
54
|
+
* Sessions live exactly one directory below the root; deeper paths are
|
|
55
|
+
* subagent transcripts and are ignored. A path at the root itself is a
|
|
56
|
+
* project directory appearing or vanishing, which needs a full listing.
|
|
57
|
+
*/
|
|
58
|
+
statWatchedSessionFile(root: string, relativePath: string, workspacePath?: string): Promise<WatchedSessionFile>;
|
|
38
59
|
isSessionFile(filename: string): boolean;
|
|
39
60
|
getSessionId(sessionPath: string): string;
|
|
40
61
|
encodeWorkspacePath(workspacePath: string): string;
|
|
@@ -51,6 +51,7 @@ const sessionContext_1 = require("../context/sessionContext");
|
|
|
51
51
|
const sessionStats_1 = require("../sessionStats");
|
|
52
52
|
const jsonl_1 = require("../parsers/jsonl");
|
|
53
53
|
const diagnostics_1 = require("./diagnostics");
|
|
54
|
+
const directoryListingCache_1 = require("./directoryListingCache");
|
|
54
55
|
const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
|
|
55
56
|
const subagentScanner_1 = require("../parsers/subagentScanner");
|
|
56
57
|
const modelContext_1 = require("../modelContext");
|
|
@@ -90,6 +91,27 @@ function extractSearchableText(event) {
|
|
|
90
91
|
}
|
|
91
92
|
return '';
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Project-directory filter for a workspace: the encoded workspace path, a
|
|
96
|
+
* worktree suffix of it, or a directory ending in the workspace's basename.
|
|
97
|
+
* Without a workspace every directory matches.
|
|
98
|
+
*/
|
|
99
|
+
function workspaceDirectoryFilter(workspacePath) {
|
|
100
|
+
if (!workspacePath)
|
|
101
|
+
return () => true;
|
|
102
|
+
const encodedWorkspace = (0, sessionPathResolver_1.encodeWorkspacePath)(workspacePath).toLowerCase();
|
|
103
|
+
const workspaceBasename = path
|
|
104
|
+
.basename(workspacePath)
|
|
105
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
return (directoryName) => {
|
|
108
|
+
const name = directoryName.toLowerCase();
|
|
109
|
+
return (name === encodedWorkspace ||
|
|
110
|
+
name.startsWith(`${encodedWorkspace}-`) ||
|
|
111
|
+
name === workspaceBasename ||
|
|
112
|
+
name.endsWith(`-${workspaceBasename}`));
|
|
113
|
+
};
|
|
114
|
+
}
|
|
93
115
|
function extractClaudeLabelFromPrefix(chunk) {
|
|
94
116
|
for (const line of chunk.split('\n')) {
|
|
95
117
|
const trimmed = line.trim();
|
|
@@ -284,6 +306,8 @@ class ClaudeCodeProvider {
|
|
|
284
306
|
diagnostics;
|
|
285
307
|
/** Runtime-reported context window limit (overrides static map when set). */
|
|
286
308
|
dynamicContextWindowLimit = null;
|
|
309
|
+
/** Project-directory listings reused across async enumerations. */
|
|
310
|
+
directoryCache = new directoryListingCache_1.DirectoryListingCache();
|
|
287
311
|
constructor(options = {}) {
|
|
288
312
|
this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
|
|
289
313
|
}
|
|
@@ -364,77 +388,144 @@ class ClaudeCodeProvider {
|
|
|
364
388
|
this.recordHomeStatus('listAllSessionFiles');
|
|
365
389
|
return results;
|
|
366
390
|
}
|
|
367
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Enumerate session files across project directories through the
|
|
393
|
+
* directory cache: one stat per project directory, a readdir only for
|
|
394
|
+
* directories whose mtime changed, and a stat only for files new to a
|
|
395
|
+
* changed directory (or recently modified, with `revalidateRecent`).
|
|
396
|
+
*
|
|
397
|
+
* With `limit`, directories are visited newest-mtime first and the walk
|
|
398
|
+
* stops once the remaining directories are older than the newest `limit`
|
|
399
|
+
* files found. A directory's mtime ignores appends, so a cold limited walk
|
|
400
|
+
* can miss a long-running session in an old directory until its next watch
|
|
401
|
+
* event; `since` filters files and shares the caveat.
|
|
402
|
+
*/
|
|
403
|
+
async listSessionFilesAsync(workspacePath, options = {}) {
|
|
368
404
|
const projectsRoot = this.getProjectsBaseDir();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
if (options.stats) {
|
|
406
|
+
options.stats.directoriesListed ??= 0;
|
|
407
|
+
options.stats.directoriesStatted ??= 0;
|
|
408
|
+
options.stats.filesStatted ??= 0;
|
|
372
409
|
}
|
|
373
|
-
|
|
410
|
+
const root = await this.directoryCache.listDirectory(projectsRoot, () => false, {
|
|
411
|
+
revalidateRecent: options.revalidateRecent,
|
|
412
|
+
stats: options.stats,
|
|
413
|
+
});
|
|
414
|
+
if (!root) {
|
|
374
415
|
this.recordHomeStatus('listSessionFilesAsync');
|
|
375
416
|
return [];
|
|
376
417
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
? path
|
|
380
|
-
.basename(workspacePath)
|
|
381
|
-
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
382
|
-
.toLowerCase()
|
|
418
|
+
const limit = options.limit !== undefined && Number.isFinite(options.limit)
|
|
419
|
+
? Math.max(0, Math.floor(options.limit))
|
|
383
420
|
: null;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
421
|
+
if (limit === 0) {
|
|
422
|
+
this.recordHomeStatus('listSessionFilesAsync');
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
const since = options.since !== undefined && Number.isFinite(options.since) ? options.since : null;
|
|
426
|
+
const matchesWorkspace = workspaceDirectoryFilter(workspacePath);
|
|
427
|
+
const candidates = [];
|
|
428
|
+
let sinceYield = 0;
|
|
429
|
+
for (const name of root.subdirectories) {
|
|
430
|
+
if (!matchesWorkspace(name))
|
|
431
|
+
continue;
|
|
432
|
+
const directory = path.join(projectsRoot, name);
|
|
387
433
|
try {
|
|
388
|
-
const stat =
|
|
389
|
-
if (
|
|
390
|
-
|
|
434
|
+
const stat = await fs.promises.stat(directory);
|
|
435
|
+
if (options.stats) {
|
|
436
|
+
options.stats.directoriesStatted = (options.stats.directoriesStatted ?? 0) + 1;
|
|
437
|
+
}
|
|
438
|
+
if (stat.isDirectory())
|
|
439
|
+
candidates.push({ directory, stat });
|
|
391
440
|
}
|
|
392
441
|
catch {
|
|
393
442
|
continue;
|
|
394
443
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
444
|
+
if (++sinceYield >= 50) {
|
|
445
|
+
sinceYield = 0;
|
|
446
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (limit !== null)
|
|
450
|
+
candidates.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
|
|
451
|
+
const byNewest = (left, right) => right.mtime.getTime() - left.mtime.getTime();
|
|
452
|
+
const results = [];
|
|
453
|
+
let cutoffMs = null;
|
|
454
|
+
for (const candidate of candidates) {
|
|
455
|
+
if (limit !== null && cutoffMs !== null && candidate.stat.mtimeMs < cutoffMs)
|
|
456
|
+
break;
|
|
457
|
+
const listing = await this.directoryCache.listDirectory(candidate.directory, (name) => this.isSessionFile(name), {
|
|
458
|
+
dirStat: candidate.stat,
|
|
459
|
+
revalidateRecent: options.revalidateRecent,
|
|
460
|
+
stats: options.stats,
|
|
461
|
+
});
|
|
462
|
+
if (!listing)
|
|
401
463
|
continue;
|
|
464
|
+
for (const [name, stat] of listing.files) {
|
|
465
|
+
if (stat.sizeBytes <= 0)
|
|
466
|
+
continue;
|
|
467
|
+
if (since !== null && stat.mtimeMs < since)
|
|
468
|
+
continue;
|
|
469
|
+
const sessionPath = path.join(candidate.directory, name);
|
|
470
|
+
results.push({
|
|
471
|
+
path: sessionPath,
|
|
472
|
+
mtime: new Date(stat.mtimeMs),
|
|
473
|
+
sizeBytes: stat.sizeBytes,
|
|
474
|
+
sessionId: this.getSessionId(sessionPath),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (limit !== null && results.length >= limit) {
|
|
478
|
+
results.sort(byNewest);
|
|
479
|
+
results.length = limit;
|
|
480
|
+
cutoffMs = results[limit - 1].mtime.getTime();
|
|
402
481
|
}
|
|
403
|
-
directories.push(directory);
|
|
404
482
|
await new Promise((resolve) => setImmediate(resolve));
|
|
405
483
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
484
|
+
this.recordHomeStatus('listSessionFilesAsync');
|
|
485
|
+
results.sort(byNewest);
|
|
486
|
+
return limit !== null && results.length > limit ? results.slice(0, limit) : results;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a recursive-watch event under the projects root with one stat.
|
|
490
|
+
* Sessions live exactly one directory below the root; deeper paths are
|
|
491
|
+
* subagent transcripts and are ignored. A path at the root itself is a
|
|
492
|
+
* project directory appearing or vanishing, which needs a full listing.
|
|
493
|
+
*/
|
|
494
|
+
async statWatchedSessionFile(root, relativePath, workspacePath) {
|
|
495
|
+
const segments = (0, directoryListingCache_1.splitWatchedPath)(relativePath);
|
|
496
|
+
if (!segments)
|
|
497
|
+
return { status: 'unknown' };
|
|
498
|
+
if (segments.length > 2)
|
|
499
|
+
return { status: 'ignored' };
|
|
500
|
+
const fullPath = path.join(root, ...segments);
|
|
501
|
+
if (segments.length === 1) {
|
|
502
|
+
this.directoryCache.invalidate(fullPath);
|
|
409
503
|
try {
|
|
410
|
-
|
|
504
|
+
const stat = await fs.promises.stat(fullPath);
|
|
505
|
+
return stat.isDirectory() ? { status: 'unknown' } : { status: 'ignored' };
|
|
411
506
|
}
|
|
412
507
|
catch {
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
for (const entry of entries) {
|
|
416
|
-
if (!entry.isFile() || !this.isSessionFile(entry.name))
|
|
417
|
-
continue;
|
|
418
|
-
const sessionPath = path.join(directory, entry.name);
|
|
419
|
-
try {
|
|
420
|
-
const stat = await fs.promises.stat(sessionPath);
|
|
421
|
-
if (stat.size > 0) {
|
|
422
|
-
results.push({
|
|
423
|
-
path: sessionPath,
|
|
424
|
-
mtime: stat.mtime,
|
|
425
|
-
sizeBytes: stat.size,
|
|
426
|
-
sessionId: this.getSessionId(sessionPath),
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
// Skip files that vanish between discovery and stat.
|
|
432
|
-
}
|
|
433
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
508
|
+
return { status: 'unknown' };
|
|
434
509
|
}
|
|
435
510
|
}
|
|
436
|
-
|
|
437
|
-
|
|
511
|
+
const [directoryName, fileName] = segments;
|
|
512
|
+
if (!this.isSessionFile(fileName))
|
|
513
|
+
return { status: 'ignored' };
|
|
514
|
+
if (!workspaceDirectoryFilter(workspacePath)(directoryName))
|
|
515
|
+
return { status: 'ignored' };
|
|
516
|
+
const sessionId = this.getSessionId(fullPath);
|
|
517
|
+
const stat = await this.directoryCache.refreshFile(fullPath);
|
|
518
|
+
if (!stat || stat.sizeBytes <= 0)
|
|
519
|
+
return { status: 'missing', path: fullPath, sessionId };
|
|
520
|
+
return {
|
|
521
|
+
status: 'present',
|
|
522
|
+
file: {
|
|
523
|
+
path: fullPath,
|
|
524
|
+
mtime: new Date(stat.mtimeMs),
|
|
525
|
+
sizeBytes: stat.sizeBytes,
|
|
526
|
+
sessionId,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
438
529
|
}
|
|
439
530
|
// --- File identification ---
|
|
440
531
|
isSessionFile(filename) {
|
|
@@ -627,6 +718,7 @@ class ClaudeCodeProvider {
|
|
|
627
718
|
// --- Lifecycle ---
|
|
628
719
|
dispose() {
|
|
629
720
|
this.dynamicContextWindowLimit = null;
|
|
721
|
+
this.directoryCache.invalidate();
|
|
630
722
|
}
|
|
631
723
|
recordHomeStatus(operation) {
|
|
632
724
|
const status = this.getRuntimeStatus();
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
|
|
10
10
|
*/
|
|
11
11
|
import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
|
|
12
|
-
import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileInfo, SessionFileStats, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions } from './types';
|
|
12
|
+
import type { ListSessionFilesOptions, SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileInfo, SessionFileStats, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions, WatchedSessionFile } from './types';
|
|
13
13
|
import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
|
|
14
14
|
import type { CodexRateLimits } from '../types/codex';
|
|
15
15
|
export interface FindCodexRolloutFileOptions {
|
|
@@ -48,6 +48,8 @@ export declare class CodexProvider implements SessionProviderBase {
|
|
|
48
48
|
private dynamicContextWindowLimit;
|
|
49
49
|
private lastRateLimitsData;
|
|
50
50
|
private lastTokenUsageData;
|
|
51
|
+
/** Dated rollout-directory listings reused across async enumerations. */
|
|
52
|
+
private readonly directoryCache;
|
|
51
53
|
constructor(options?: SessionProviderOptions);
|
|
52
54
|
/** Lazy-initialize database connection. */
|
|
53
55
|
private ensureDb;
|
|
@@ -69,7 +71,13 @@ export declare class CodexProvider implements SessionProviderBase {
|
|
|
69
71
|
findSessionFiles(workspacePath: string): string[];
|
|
70
72
|
findSessionsInDirectory(dir: string): string[];
|
|
71
73
|
listAllSessionFiles(): SessionFileInfo[];
|
|
72
|
-
listSessionFilesAsync(workspacePath?: string): Promise<SessionFileInfo[]>;
|
|
74
|
+
listSessionFilesAsync(workspacePath?: string, options?: ListSessionFilesOptions): Promise<SessionFileInfo[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a recursive-watch event under a sessions root with one stat.
|
|
77
|
+
* Rollouts are matched by name at any depth the walker would visit; a
|
|
78
|
+
* dated directory appearing or vanishing needs a full walk.
|
|
79
|
+
*/
|
|
80
|
+
statWatchedSessionFile(root: string, relativePath: string, workspacePath?: string): Promise<WatchedSessionFile>;
|
|
73
81
|
getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
|
|
74
82
|
isSessionFile(filename: string): boolean;
|
|
75
83
|
getSessionId(sessionPath: string): string;
|