sidekick-shared 0.28.2 → 0.29.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 +83 -0
- package/README.md +176 -30
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +14 -6
- package/dist/browser.js +26 -1
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/codexQuota.js +17 -4
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +6 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/codexQuota.js +18 -5
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +12 -9
- package/dist/esm/modelCatalog.js +2 -1
- package/dist/esm/modelInfo.js +422 -45
- package/dist/esm/multiProviderQuotaService.js +1 -1
- package/dist/esm/node.js +9 -3
- package/dist/esm/parsers/codexParser.js +175 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +83 -21
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +14 -7
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/codex.js +2 -24
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/quota.js +356 -24
- package/dist/esm/quotaHistory.js +33 -8
- package/dist/esm/quotaPoller.js +5 -2
- package/dist/esm/quotaResolver.js +31 -6
- package/dist/esm/quotaSnapshots.js +65 -10
- package/dist/esm/quotaThresholds.js +7 -2
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/index.js +2 -2
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/quota.js +30 -0
- package/dist/esm/schemas/quotaHistory.js +10 -2
- package/dist/esm/schemas/stateFile.js +9 -0
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/stateFile.js +18 -3
- package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/esm/statusline/formatter.js +57 -18
- package/dist/esm/statusline/index.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +42 -3
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/jsonlTail.js +378 -59
- package/dist/esm/zaiQuotaApi.js +4 -2
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -16
- package/dist/index.js +48 -12
- package/dist/modelCatalog.js +2 -1
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +431 -47
- package/dist/multiProviderQuotaService.js +1 -1
- package/dist/node.d.ts +16 -5
- package/dist/node.js +31 -2
- package/dist/parsers/codexParser.d.ts +55 -0
- package/dist/parsers/codexParser.js +175 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +86 -21
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +13 -6
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +3 -26
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/quota.d.ts +131 -1
- package/dist/quota.js +366 -24
- package/dist/quotaHistory.d.ts +10 -0
- package/dist/quotaHistory.js +34 -8
- package/dist/quotaPoller.d.ts +11 -3
- package/dist/quotaPoller.js +5 -2
- package/dist/quotaResolver.js +30 -5
- package/dist/quotaSnapshots.js +64 -9
- package/dist/quotaThresholds.js +7 -2
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +3 -3
- package/dist/schemas/index.js +5 -2
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/quota.d.ts +258 -0
- package/dist/schemas/quota.js +31 -1
- package/dist/schemas/quotaHistory.d.ts +2 -0
- package/dist/schemas/quotaHistory.js +10 -2
- package/dist/schemas/stateFile.d.ts +34 -0
- package/dist/schemas/stateFile.js +10 -1
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/stateFile.d.ts +18 -0
- package/dist/stateFile.js +17 -2
- package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
- package/dist/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/statusline/formatter.d.ts +5 -0
- package/dist/statusline/formatter.js +56 -17
- package/dist/statusline/index.d.ts +4 -4
- package/dist/statusline/index.js +5 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +42 -3
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/jsonlTail.d.ts +101 -4
- package/dist/watchers/jsonlTail.js +380 -60
- package/dist/zaiQuotaApi.js +3 -1
- package/package.json +41 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - `HOME` (and the Windows equivalents) point at a per-process temp dir,
|
|
11
11
|
* removed on exit, and the home overrides (`CLAUDE_CONFIG_DIR`,
|
|
12
12
|
* `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, `SIDEKICK_CONFIG_DIR`)
|
|
13
|
-
* are unset
|
|
13
|
+
* are unset (unless named in `preserveEnv` and pointed at a fixture), so
|
|
14
|
+
* every `os.homedir()`-derived path is isolated structurally;
|
|
14
15
|
* - the real `child_process` module object is patched, which reaches both ESM
|
|
15
16
|
* named imports (after `syncBuiltinESMExports`) and CommonJS code loaded
|
|
16
17
|
* through native `require`; per-file `vi.mock('child_process')` factories
|
|
@@ -28,6 +29,13 @@ import * as os from 'node:os';
|
|
|
28
29
|
import * as path from 'node:path';
|
|
29
30
|
const GUARD_KEY = '__sidekickCredentialGuard';
|
|
30
31
|
const DEFAULT_BLOCKED_BINARIES = ['security', 'claude', 'codex'];
|
|
32
|
+
/** Home overrides the guard unsets; only these can be preserved. */
|
|
33
|
+
const SCRUBBED_ENV = [
|
|
34
|
+
'CLAUDE_CONFIG_DIR',
|
|
35
|
+
'CLAUDE_SECURESTORAGE_CONFIG_DIR',
|
|
36
|
+
'CODEX_HOME',
|
|
37
|
+
'SIDEKICK_CONFIG_DIR',
|
|
38
|
+
];
|
|
31
39
|
const CHILD_PROCESS_FUNCTIONS = [
|
|
32
40
|
'spawn',
|
|
33
41
|
'spawnSync',
|
|
@@ -75,6 +83,8 @@ export function installCredentialGuard(options = {}) {
|
|
|
75
83
|
// os.userInfo() reads the passwd entry, so it still names the real home
|
|
76
84
|
// after HOME is redirected below.
|
|
77
85
|
const realHome = os.userInfo().homedir;
|
|
86
|
+
// Validated before any side effect, so a refused install changes nothing.
|
|
87
|
+
const preserved = checkPreservedEnv(options, realHome, realFs);
|
|
78
88
|
const testHome = realFs.mkdtempSync(path.join(os.tmpdir(), 'sidekick-test-home-'));
|
|
79
89
|
const removeTestHome = realFs.rmSync.bind(realFs);
|
|
80
90
|
process.once('exit', () => {
|
|
@@ -103,10 +113,9 @@ export function installCredentialGuard(options = {}) {
|
|
|
103
113
|
process.env.USERPROFILE = testHome;
|
|
104
114
|
process.env.APPDATA = path.join(testHome, 'AppData', 'Roaming');
|
|
105
115
|
process.env.LOCALAPPDATA = path.join(testHome, 'AppData', 'Local');
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
delete process.env.SIDEKICK_CONFIG_DIR;
|
|
116
|
+
for (const name of SCRUBBED_ENV)
|
|
117
|
+
if (!preserved.has(name))
|
|
118
|
+
delete process.env[name];
|
|
110
119
|
const protectedRoots = path.resolve(realHome) === path.resolve(testHome)
|
|
111
120
|
? []
|
|
112
121
|
: [
|
|
@@ -208,6 +217,79 @@ export function installCredentialGuard(options = {}) {
|
|
|
208
217
|
globalThis[GUARD_KEY] = state;
|
|
209
218
|
return state;
|
|
210
219
|
}
|
|
220
|
+
/** The `preserveEnv` names, after refusing unknown names and unsafe values. */
|
|
221
|
+
function checkPreservedEnv(options, realHome, realFs) {
|
|
222
|
+
const preserved = new Set();
|
|
223
|
+
for (const name of options.preserveEnv ?? []) {
|
|
224
|
+
if (!SCRUBBED_ENV.includes(name)) {
|
|
225
|
+
throw new Error(`installCredentialGuard: preserveEnv accepts only ${SCRUBBED_ENV.join(', ')}; got ${JSON.stringify(name)}.`);
|
|
226
|
+
}
|
|
227
|
+
preserved.add(name);
|
|
228
|
+
}
|
|
229
|
+
if (preserved.size === 0)
|
|
230
|
+
return preserved;
|
|
231
|
+
const roots = [
|
|
232
|
+
path.join(realHome, '.claude'),
|
|
233
|
+
path.join(realHome, '.claude.json'),
|
|
234
|
+
path.join(realHome, '.codex'),
|
|
235
|
+
path.join(realHome, '.config', 'sidekick'),
|
|
236
|
+
// The Windows default config dir, read before HOME and APPDATA move.
|
|
237
|
+
...(process.platform === 'win32' && process.env.APPDATA
|
|
238
|
+
? [path.join(process.env.APPDATA, 'sidekick')]
|
|
239
|
+
: []),
|
|
240
|
+
...(options.protectedPaths ?? []),
|
|
241
|
+
];
|
|
242
|
+
for (const name of preserved) {
|
|
243
|
+
const value = process.env[name]?.trim();
|
|
244
|
+
if (!value)
|
|
245
|
+
continue;
|
|
246
|
+
const hit = protectedLocation(value, roots, realFs);
|
|
247
|
+
if (hit) {
|
|
248
|
+
throw new Error(`installCredentialGuard: preserveEnv ${name}=${value} reaches the real protected location ${hit}. Point it at a temporary fixture instead.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return preserved;
|
|
252
|
+
}
|
|
253
|
+
/** The root `target` equals, contains, or lies inside, comparing plain and symlink-resolved paths. */
|
|
254
|
+
function protectedLocation(target, roots, realFs) {
|
|
255
|
+
// Case-insensitive by default on macOS and Windows; folding can only over-refuse.
|
|
256
|
+
const fold = process.platform === 'darwin' || process.platform === 'win32';
|
|
257
|
+
const normal = (value) => {
|
|
258
|
+
const resolved = path.resolve(value);
|
|
259
|
+
return fold ? resolved.toLowerCase() : resolved;
|
|
260
|
+
};
|
|
261
|
+
const within = (parent, child) => child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
|
262
|
+
const related = (a, b) => within(a, b) || within(b, a);
|
|
263
|
+
// Plain paths first, so an obviously unsafe value never reaches the filesystem.
|
|
264
|
+
const plain = normal(target);
|
|
265
|
+
for (const root of roots)
|
|
266
|
+
if (related(normal(root), plain))
|
|
267
|
+
return root;
|
|
268
|
+
const real = normal(realpathOfNearest(target, realFs));
|
|
269
|
+
for (const root of roots) {
|
|
270
|
+
const realRoot = normal(realpathOfNearest(root, realFs));
|
|
271
|
+
if (related(realRoot, real) || related(normal(root), real) || related(realRoot, plain))
|
|
272
|
+
return root;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
/** `realpath` of the nearest existing ancestor, with the missing tail re-appended. */
|
|
277
|
+
function realpathOfNearest(target, realFs) {
|
|
278
|
+
let current = path.resolve(target);
|
|
279
|
+
let tail = [];
|
|
280
|
+
for (;;) {
|
|
281
|
+
try {
|
|
282
|
+
return path.join(realFs.realpathSync.native(current), ...tail);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
const parent = path.dirname(current);
|
|
286
|
+
if (parent === current)
|
|
287
|
+
return path.resolve(target);
|
|
288
|
+
tail = [path.basename(current), ...tail];
|
|
289
|
+
current = parent;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
211
293
|
/**
|
|
212
294
|
* The real module object of a Node built-in: `process.getBuiltinModule`
|
|
213
295
|
* (Node 20.16+), or `require` in CommonJS on older Node.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates the session record that issued each of a set of tool calls, by id.
|
|
3
|
+
*
|
|
4
|
+
* Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
|
|
5
|
+
* the session file, session id and time of each call, plus the ordinal of the
|
|
6
|
+
* human prompt the call answered, so a downstream record (an edit, a commit)
|
|
7
|
+
* can be tied back to the prompt that caused it. Scope, listing, session keys
|
|
8
|
+
* and ordinals are the ones `collectPromptHistory()` uses, so results join
|
|
9
|
+
* directly with prompt-history entries.
|
|
10
|
+
*
|
|
11
|
+
* Cost: one listing of the workspace-scoped session files (`since` prunes by
|
|
12
|
+
* modification time), then a streaming pass over each candidate file in which
|
|
13
|
+
* a line is only parsed when it contains one of the wanted ids. Claude Code
|
|
14
|
+
* subagent transcripts of each session are searched too. Stops as soon as
|
|
15
|
+
* every id is found. Prompt ordinals are read only for files that matched.
|
|
16
|
+
*
|
|
17
|
+
* Node-only: reads session files.
|
|
18
|
+
*
|
|
19
|
+
* @module toolCallOrigins
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as readline from 'readline';
|
|
24
|
+
import { listCandidates, readPromptOrdinalMarks, resolveRoots, } from './promptHistory';
|
|
25
|
+
import { codexRolloutOrigin } from './parsers/codexRolloutOrigin';
|
|
26
|
+
import { realpathOrNull } from './workspaceRoots';
|
|
27
|
+
/**
|
|
28
|
+
* Finds where each tool call was issued. Ids that are not found are absent
|
|
29
|
+
* from the result. When an id appears more than once (a forked or resumed
|
|
30
|
+
* session can replay history), the earliest record wins, and a session's own
|
|
31
|
+
* record wins over a fork's copy at the same instant.
|
|
32
|
+
*/
|
|
33
|
+
export async function findToolCallOrigins(options) {
|
|
34
|
+
const wanted = new Set([...options.ids].filter((id) => typeof id === 'string' && id.length > 0));
|
|
35
|
+
const found = new Map();
|
|
36
|
+
if (wanted.size === 0)
|
|
37
|
+
return found;
|
|
38
|
+
throwIfAborted(options.signal);
|
|
39
|
+
const realpaths = new Map();
|
|
40
|
+
const { roots, listingRoots } = resolveRoots(options.workspacePaths, options.includeWorktrees !== false, realpaths);
|
|
41
|
+
if (roots.length === 0)
|
|
42
|
+
return found;
|
|
43
|
+
const providers = options.providers ?? ['claude-code', 'codex'];
|
|
44
|
+
const sinceMs = options.since ? options.since.getTime() : null;
|
|
45
|
+
const candidates = await listCandidates(providers, listingRoots, sinceMs);
|
|
46
|
+
const byProviderSession = new Map();
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
byProviderSession.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
|
|
49
|
+
}
|
|
50
|
+
const marksCache = new Map();
|
|
51
|
+
const marksFor = (candidate) => {
|
|
52
|
+
let marks = marksCache.get(candidate.key);
|
|
53
|
+
if (!marks) {
|
|
54
|
+
marks = readPromptOrdinalMarks(candidate.provider, candidate.sessionId, candidate.filePath).catch(() => []);
|
|
55
|
+
marksCache.set(candidate.key, marks);
|
|
56
|
+
}
|
|
57
|
+
return marks;
|
|
58
|
+
};
|
|
59
|
+
const remaining = new Set(wanted);
|
|
60
|
+
// Best timestamp seen per id, to prefer the earliest record across files.
|
|
61
|
+
const bestMs = new Map();
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (remaining.size === 0)
|
|
64
|
+
break;
|
|
65
|
+
throwIfAborted(options.signal);
|
|
66
|
+
const targets = [{ candidate, filePath: candidate.filePath }];
|
|
67
|
+
if (candidate.provider === 'claude-code') {
|
|
68
|
+
for (const sub of claudeSubagentTranscripts(candidate.filePath, candidate.sessionId)) {
|
|
69
|
+
targets.push({ candidate, filePath: sub.filePath, agentId: sub.agentId });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// A Codex fork or spawned subagent answers its parent's prompts.
|
|
73
|
+
let codexParent;
|
|
74
|
+
if (candidate.provider === 'codex') {
|
|
75
|
+
const origin = codexRolloutOrigin(await readFirstRecord(candidate.filePath));
|
|
76
|
+
if (origin.parentId)
|
|
77
|
+
codexParent = { id: origin.parentId, fork: !origin.isSubagent };
|
|
78
|
+
}
|
|
79
|
+
for (const target of targets) {
|
|
80
|
+
const hits = await scanForIds(target.filePath, candidate.provider, remaining, roots, realpaths, options.signal);
|
|
81
|
+
for (const hit of hits) {
|
|
82
|
+
const isFork = codexParent?.fork === true;
|
|
83
|
+
const previous = bestMs.get(hit.id);
|
|
84
|
+
if (previous &&
|
|
85
|
+
(previous.ms < hit.timestampMs ||
|
|
86
|
+
(previous.ms === hit.timestampMs && (!previous.fork || isFork)))) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
bestMs.set(hit.id, { ms: hit.timestampMs, fork: isFork });
|
|
90
|
+
let subagent;
|
|
91
|
+
if (target.agentId) {
|
|
92
|
+
subagent = { agentId: target.agentId, parentSessionId: candidate.sessionId };
|
|
93
|
+
}
|
|
94
|
+
else if (codexParent && !codexParent.fork) {
|
|
95
|
+
subagent = { agentId: candidate.sessionId, parentSessionId: codexParent.id };
|
|
96
|
+
}
|
|
97
|
+
// Ordinals come from the session whose prompts the call answered: a
|
|
98
|
+
// Codex fork or subagent has none of its own, so its parent's count.
|
|
99
|
+
const parentSession = codexParent
|
|
100
|
+
? byProviderSession.get(`codex:${codexParent.id}`)
|
|
101
|
+
: undefined;
|
|
102
|
+
const session = parentSession ?? candidate;
|
|
103
|
+
const afterOrdinal = !codexParent || parentSession
|
|
104
|
+
? ordinalAt(await marksFor(session), hit.timestampMs)
|
|
105
|
+
: null;
|
|
106
|
+
found.set(hit.id, {
|
|
107
|
+
provider: candidate.provider,
|
|
108
|
+
sessionId: session.sessionId,
|
|
109
|
+
sessionKey: session.key,
|
|
110
|
+
filePath: target.filePath,
|
|
111
|
+
timestamp: hit.timestamp,
|
|
112
|
+
afterOrdinal,
|
|
113
|
+
...(hit.toolName ? { toolName: hit.toolName } : {}),
|
|
114
|
+
...(subagent ? { subagent } : {}),
|
|
115
|
+
});
|
|
116
|
+
// A fork's copy might be replaced by the original later; keep looking.
|
|
117
|
+
if (!isFork)
|
|
118
|
+
remaining.delete(hit.id);
|
|
119
|
+
}
|
|
120
|
+
if (remaining.size === 0)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
/** Highest ordinal among prompts at or before `ms`; marks are in file order. */
|
|
127
|
+
function ordinalAt(marks, ms) {
|
|
128
|
+
let best = null;
|
|
129
|
+
for (const mark of marks) {
|
|
130
|
+
if (Number.isFinite(mark.timestampMs) && mark.timestampMs <= ms) {
|
|
131
|
+
if (best === null || mark.ordinal > best)
|
|
132
|
+
best = mark.ordinal;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return best;
|
|
136
|
+
}
|
|
137
|
+
function claudeSubagentTranscripts(sessionFile, sessionId) {
|
|
138
|
+
const dir = path.join(path.dirname(sessionFile), sessionId, 'subagents');
|
|
139
|
+
let names;
|
|
140
|
+
try {
|
|
141
|
+
names = fs.readdirSync(dir);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
return names
|
|
147
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
148
|
+
.sort()
|
|
149
|
+
.map((name) => ({
|
|
150
|
+
filePath: path.join(dir, name),
|
|
151
|
+
agentId: path.basename(name, '.jsonl').replace(/^agent-/, ''),
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
async function readFirstRecord(filePath) {
|
|
155
|
+
let handle;
|
|
156
|
+
try {
|
|
157
|
+
handle = await fs.promises.open(filePath, 'r');
|
|
158
|
+
const buffer = Buffer.alloc(256 * 1024);
|
|
159
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
160
|
+
const text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
161
|
+
const newline = text.indexOf('\n');
|
|
162
|
+
return JSON.parse(newline === -1 ? text : text.slice(0, newline));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await handle?.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Fail-closed scope check on a recorded working directory. */
|
|
172
|
+
function inScope(cwd, roots, realpaths) {
|
|
173
|
+
if (typeof cwd !== 'string' || cwd.length === 0)
|
|
174
|
+
return false;
|
|
175
|
+
const real = realpathOrNull(cwd, realpaths);
|
|
176
|
+
if (!real)
|
|
177
|
+
return false;
|
|
178
|
+
return roots.some((root) => {
|
|
179
|
+
if (real === root)
|
|
180
|
+
return true;
|
|
181
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
182
|
+
return real.startsWith(prefix);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const ID_PATTERN = /"(?:id|call_id)"\s*:\s*"([^"\\]{1,256})"/g;
|
|
186
|
+
async function scanForIds(filePath, provider, wanted, roots, realpaths, signal) {
|
|
187
|
+
const hits = [];
|
|
188
|
+
let stream;
|
|
189
|
+
try {
|
|
190
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return hits;
|
|
194
|
+
}
|
|
195
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
196
|
+
// Codex records a working directory per session/turn, not per line.
|
|
197
|
+
let codexCwd;
|
|
198
|
+
try {
|
|
199
|
+
for await (const line of lines) {
|
|
200
|
+
throwIfAborted(signal);
|
|
201
|
+
if (provider === 'codex' && line.includes('"cwd"')) {
|
|
202
|
+
const cwd = codexCwdOf(line);
|
|
203
|
+
if (cwd !== undefined)
|
|
204
|
+
codexCwd = cwd;
|
|
205
|
+
}
|
|
206
|
+
let match = false;
|
|
207
|
+
for (const found of line.matchAll(ID_PATTERN)) {
|
|
208
|
+
if (wanted.has(found[1])) {
|
|
209
|
+
match = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!match)
|
|
214
|
+
continue;
|
|
215
|
+
let record;
|
|
216
|
+
try {
|
|
217
|
+
record = JSON.parse(line);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const lineHits = provider === 'claude-code' ? claudeToolUses(record, wanted) : codexToolCall(record, wanted);
|
|
223
|
+
if (lineHits.length === 0)
|
|
224
|
+
continue;
|
|
225
|
+
const cwd = provider === 'claude-code' ? record.cwd : codexCwd;
|
|
226
|
+
if (!inScope(cwd, roots, realpaths))
|
|
227
|
+
continue;
|
|
228
|
+
hits.push(...lineHits);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (signal?.aborted)
|
|
233
|
+
throw error;
|
|
234
|
+
// Unreadable or vanished file: keep what was found.
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
lines.close();
|
|
238
|
+
stream.destroy();
|
|
239
|
+
}
|
|
240
|
+
return hits;
|
|
241
|
+
}
|
|
242
|
+
function codexCwdOf(line) {
|
|
243
|
+
try {
|
|
244
|
+
const record = JSON.parse(line);
|
|
245
|
+
if (record.type === 'session_meta' || record.type === 'turn_context') {
|
|
246
|
+
return record.payload?.cwd;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Not a complete record.
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
function timeOf(record) {
|
|
255
|
+
const timestamp = record.timestamp;
|
|
256
|
+
if (typeof timestamp !== 'string')
|
|
257
|
+
return null;
|
|
258
|
+
const timestampMs = Date.parse(timestamp);
|
|
259
|
+
return Number.isFinite(timestampMs) ? { timestamp, timestampMs } : null;
|
|
260
|
+
}
|
|
261
|
+
function claudeToolUses(record, wanted) {
|
|
262
|
+
const row = record;
|
|
263
|
+
if (row?.type !== 'assistant' || !Array.isArray(row.message?.content))
|
|
264
|
+
return [];
|
|
265
|
+
const time = timeOf(record);
|
|
266
|
+
if (!time)
|
|
267
|
+
return [];
|
|
268
|
+
const hits = [];
|
|
269
|
+
for (const block of row.message.content) {
|
|
270
|
+
const item = block;
|
|
271
|
+
if ((item?.type === 'tool_use' || item?.type === 'server_tool_use') &&
|
|
272
|
+
typeof item.id === 'string' &&
|
|
273
|
+
wanted.has(item.id)) {
|
|
274
|
+
hits.push({
|
|
275
|
+
id: item.id,
|
|
276
|
+
...time,
|
|
277
|
+
...(typeof item.name === 'string' ? { toolName: item.name } : {}),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return hits;
|
|
282
|
+
}
|
|
283
|
+
function codexToolCall(record, wanted) {
|
|
284
|
+
const row = record;
|
|
285
|
+
const payloadType = row?.payload?.type;
|
|
286
|
+
if (row?.type !== 'response_item' ||
|
|
287
|
+
typeof payloadType !== 'string' ||
|
|
288
|
+
!payloadType.endsWith('_call')) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const callId = row.payload?.call_id;
|
|
292
|
+
if (typeof callId !== 'string' || !wanted.has(callId))
|
|
293
|
+
return [];
|
|
294
|
+
const time = timeOf(record);
|
|
295
|
+
if (!time)
|
|
296
|
+
return [];
|
|
297
|
+
const name = row.payload?.name;
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
id: callId,
|
|
301
|
+
...time,
|
|
302
|
+
toolName: typeof name === 'string' ? name : payloadType.replace(/_call$/, ''),
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
function throwIfAborted(signal) {
|
|
307
|
+
if (signal?.aborted) {
|
|
308
|
+
throw (signal.reason ?? Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }));
|
|
309
|
+
}
|
|
310
|
+
}
|
package/dist/esm/transcript.js
CHANGED
|
@@ -63,10 +63,12 @@ export function projectSessionTranscript(events, options = {}) {
|
|
|
63
63
|
blocks.push({ type: 'text', ...boundedText(event.summary, fidelity, maxContentChars) });
|
|
64
64
|
}
|
|
65
65
|
if (event.type === 'tool_use') {
|
|
66
|
+
const projected = projectToolInput(event.tool.input);
|
|
66
67
|
blocks.push({
|
|
67
68
|
type: 'tool_use',
|
|
68
69
|
toolName: event.tool.name,
|
|
69
|
-
input: boundedValue(
|
|
70
|
+
input: boundedValue(projected.input, fidelity, maxContentChars),
|
|
71
|
+
...toolIdentity(projected),
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
if (event.type === 'tool_result') {
|
|
@@ -87,6 +89,7 @@ export function projectSessionTranscript(events, options = {}) {
|
|
|
87
89
|
timestamp: event.timestamp,
|
|
88
90
|
input: block.input,
|
|
89
91
|
command,
|
|
92
|
+
...toolIdentity(block),
|
|
90
93
|
source,
|
|
91
94
|
};
|
|
92
95
|
tools.push(call);
|
|
@@ -183,12 +186,14 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
|
|
|
183
186
|
});
|
|
184
187
|
}
|
|
185
188
|
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
189
|
+
const projected = projectToolInput(block.input);
|
|
186
190
|
result.push({
|
|
187
191
|
type: 'tool_use',
|
|
188
192
|
toolName: block.name,
|
|
189
193
|
toolUseId: typeof block.id === 'string' ? block.id : undefined,
|
|
190
|
-
input: boundedValue(
|
|
191
|
-
command: commandFromTool(block.name,
|
|
194
|
+
input: boundedValue(projected.input, fidelity, maxChars),
|
|
195
|
+
command: commandFromTool(block.name, projected.input),
|
|
196
|
+
...toolIdentity(projected),
|
|
192
197
|
});
|
|
193
198
|
}
|
|
194
199
|
else if (block.type === 'tool_result') {
|
|
@@ -212,6 +217,40 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
|
|
|
212
217
|
}
|
|
213
218
|
return result;
|
|
214
219
|
}
|
|
220
|
+
/** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
|
|
221
|
+
const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
|
|
222
|
+
/**
|
|
223
|
+
* Lifts the parsers' `_sidekickRawToolName` / `_sidekickMcpServerName` out of
|
|
224
|
+
* a tool input and drops every other top-level `_sidekick*` key. Returns a
|
|
225
|
+
* copy when anything is removed; the source event is never modified.
|
|
226
|
+
*/
|
|
227
|
+
function projectToolInput(input) {
|
|
228
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
229
|
+
return { input };
|
|
230
|
+
const source = input;
|
|
231
|
+
const keys = Object.keys(source);
|
|
232
|
+
if (!keys.some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX)))
|
|
233
|
+
return { input };
|
|
234
|
+
const clean = {};
|
|
235
|
+
for (const key of keys) {
|
|
236
|
+
if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
|
|
237
|
+
clean[key] = source[key];
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
input: clean,
|
|
241
|
+
rawToolName: nonEmptyString(source._sidekickRawToolName),
|
|
242
|
+
mcpServerName: nonEmptyString(source._sidekickMcpServerName),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function toolIdentity(value) {
|
|
246
|
+
return {
|
|
247
|
+
...(value.rawToolName ? { rawToolName: value.rawToolName } : {}),
|
|
248
|
+
...(value.mcpServerName ? { mcpServerName: value.mcpServerName } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function nonEmptyString(value) {
|
|
252
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
253
|
+
}
|
|
215
254
|
function boundedText(text, fidelity, maxChars) {
|
|
216
255
|
if (fidelity === 'full' || text.length <= maxChars)
|
|
217
256
|
return { text, truncated: false };
|