sidekick-shared 0.26.6 → 0.27.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 +14 -0
- package/README.md +54 -20
- package/dist/humanPrompt.d.ts +27 -0
- package/dist/humanPrompt.js +93 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -10
- package/dist/modelContext.js +4 -0
- package/dist/modelInfo.js +26 -1
- package/dist/node.d.ts +4 -0
- package/dist/node.js +9 -2
- package/dist/promptHistory.d.ts +108 -0
- package/dist/promptHistory.js +616 -0
- package/dist/schemas/sessionEvent.d.ts +25 -0
- package/dist/schemas/sessionEvent.js +3 -0
- package/dist/transcript.d.ts +6 -0
- package/dist/transcript.js +3 -0
- package/dist/types/sessionEvent.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node-only collection of human-typed prompts from Claude Code and Codex
|
|
4
|
+
* session logs, scoped to a set of workspace roots.
|
|
5
|
+
*
|
|
6
|
+
* Parse-only: no redaction and no network I/O. Scope fails closed — a prompt
|
|
7
|
+
* is returned only when the working directory recorded on its log line (or,
|
|
8
|
+
* for Codex, its session/turn context) realpath-resolves to an allowed root
|
|
9
|
+
* or somewhere inside one. Every bound stops cleanly and is reported in
|
|
10
|
+
* `boundsHit`; nothing is silently truncated and no bound throws.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.collectPromptHistory = collectPromptHistory;
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const humanPrompt_1 = require("./humanPrompt");
|
|
51
|
+
const sessionPathResolver_1 = require("./parsers/sessionPathResolver");
|
|
52
|
+
const claudeCode_1 = require("./providers/claudeCode");
|
|
53
|
+
const codex_1 = require("./providers/codex");
|
|
54
|
+
const DEFAULT_BOUNDS = {
|
|
55
|
+
maxSessions: 1000,
|
|
56
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
57
|
+
maxTotalBytes: 256 * 1024 * 1024,
|
|
58
|
+
deadlineMs: 30_000,
|
|
59
|
+
};
|
|
60
|
+
const CHECK_EVERY_LINES = 500;
|
|
61
|
+
const ALL_PROVIDERS = ['claude-code', 'codex'];
|
|
62
|
+
/** Collect human-typed prompts from Claude Code and Codex logs under the given roots. */
|
|
63
|
+
async function collectPromptHistory(options) {
|
|
64
|
+
const bounds = { ...DEFAULT_BOUNDS, ...definedBounds(options.bounds) };
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
const boundsHit = new Set();
|
|
67
|
+
const stats = {
|
|
68
|
+
sessionsScanned: 0,
|
|
69
|
+
sessionsSkippedUnchanged: 0,
|
|
70
|
+
sessionsOutOfScope: 0,
|
|
71
|
+
filesOverSizeLimit: 0,
|
|
72
|
+
sessionsNotInteractive: 0,
|
|
73
|
+
promptsMissingTimestamp: 0,
|
|
74
|
+
promptsOutOfScope: 0,
|
|
75
|
+
};
|
|
76
|
+
const previous = options.cursor?.version === 1 ? options.cursor.sessions : {};
|
|
77
|
+
const cursor = { version: 1, sessions: { ...previous } };
|
|
78
|
+
const entries = [];
|
|
79
|
+
const shouldStop = () => {
|
|
80
|
+
if (options.signal?.aborted)
|
|
81
|
+
return 'aborted';
|
|
82
|
+
if (Date.now() - startedAt >= bounds.deadlineMs)
|
|
83
|
+
return 'deadline';
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
const finish = () => ({
|
|
87
|
+
entries: entries.sort(compareEntries),
|
|
88
|
+
cursor,
|
|
89
|
+
stats,
|
|
90
|
+
boundsHit: [...boundsHit],
|
|
91
|
+
});
|
|
92
|
+
const realpaths = new Map();
|
|
93
|
+
const { roots, listingRoots } = resolveRoots(options.workspacePaths, options.includeWorktrees !== false, realpaths);
|
|
94
|
+
if (roots.length === 0)
|
|
95
|
+
return finish();
|
|
96
|
+
const sinceMs = options.since ? options.since.getTime() : null;
|
|
97
|
+
const providers = options.providers ?? ALL_PROVIDERS;
|
|
98
|
+
const candidates = await listCandidates(providers, listingRoots, sinceMs);
|
|
99
|
+
const context = { roots, sinceMs, stats, realpaths, shouldStop };
|
|
100
|
+
let totalBytes = 0;
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
const stop = shouldStop();
|
|
103
|
+
if (stop) {
|
|
104
|
+
boundsHit.add(stop);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
let stat;
|
|
108
|
+
try {
|
|
109
|
+
stat = await fs.promises.stat(candidate.filePath);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const prior = previous[candidate.key];
|
|
115
|
+
if (prior && prior.size === stat.size && prior.mtimeMs === stat.mtimeMs) {
|
|
116
|
+
stats.sessionsSkippedUnchanged++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// A file that shrank was rewritten; its old offsets and ordinals are meaningless.
|
|
120
|
+
const resume = prior && prior.offset <= stat.size ? prior : undefined;
|
|
121
|
+
const offset = resume?.offset ?? 0;
|
|
122
|
+
const bytesToRead = stat.size - offset;
|
|
123
|
+
if (resume?.state?.excluded) {
|
|
124
|
+
cursor.sessions[candidate.key] = {
|
|
125
|
+
...resume,
|
|
126
|
+
size: stat.size,
|
|
127
|
+
mtimeMs: stat.mtimeMs,
|
|
128
|
+
offset: stat.size,
|
|
129
|
+
};
|
|
130
|
+
stats.sessionsNotInteractive++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (bytesToRead > bounds.maxFileBytes) {
|
|
134
|
+
stats.filesOverSizeLimit++;
|
|
135
|
+
boundsHit.add('maxFileBytes');
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (stats.sessionsScanned >= bounds.maxSessions) {
|
|
139
|
+
boundsHit.add('maxSessions');
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (totalBytes + bytesToRead > bounds.maxTotalBytes) {
|
|
143
|
+
boundsHit.add('maxTotalBytes');
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
let buffer;
|
|
147
|
+
try {
|
|
148
|
+
buffer = await readRange(candidate.filePath, offset, bytesToRead);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
totalBytes += buffer.length;
|
|
154
|
+
stats.sessionsScanned++;
|
|
155
|
+
const outcome = await scanBuffer(candidate, buffer, offset, resume, context);
|
|
156
|
+
if (outcome.stoppedBy) {
|
|
157
|
+
// Partially processed file: drop its entries and leave its cursor untouched.
|
|
158
|
+
boundsHit.add(outcome.stoppedBy);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (outcome.notInteractive)
|
|
162
|
+
stats.sessionsNotInteractive++;
|
|
163
|
+
else if (outcome.inScope === 0 && outcome.outOfScope > 0)
|
|
164
|
+
stats.sessionsOutOfScope++;
|
|
165
|
+
entries.push(...outcome.entries);
|
|
166
|
+
cursor.sessions[candidate.key] = {
|
|
167
|
+
provider: candidate.provider,
|
|
168
|
+
sessionId: candidate.sessionId,
|
|
169
|
+
size: stat.size,
|
|
170
|
+
mtimeMs: stat.mtimeMs,
|
|
171
|
+
lastOrdinal: outcome.lastOrdinal,
|
|
172
|
+
offset: outcome.offset,
|
|
173
|
+
...(outcome.state ? { state: outcome.state } : {}),
|
|
174
|
+
};
|
|
175
|
+
await yieldToEventLoop();
|
|
176
|
+
}
|
|
177
|
+
return finish();
|
|
178
|
+
}
|
|
179
|
+
function definedBounds(bounds) {
|
|
180
|
+
const result = {};
|
|
181
|
+
if (!bounds)
|
|
182
|
+
return result;
|
|
183
|
+
for (const key of Object.keys(DEFAULT_BOUNDS)) {
|
|
184
|
+
const value = bounds[key];
|
|
185
|
+
if (typeof value === 'number' && !Number.isNaN(value))
|
|
186
|
+
result[key] = Math.max(0, value);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
function compareEntries(left, right) {
|
|
191
|
+
return (compareStrings(left.provider, right.provider) ||
|
|
192
|
+
compareStrings(left.sessionId, right.sessionId) ||
|
|
193
|
+
left.ordinal - right.ordinal);
|
|
194
|
+
}
|
|
195
|
+
function compareStrings(left, right) {
|
|
196
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
197
|
+
}
|
|
198
|
+
function realpathOrNull(input, cache) {
|
|
199
|
+
const cached = cache.get(input);
|
|
200
|
+
if (cached !== undefined)
|
|
201
|
+
return cached;
|
|
202
|
+
let resolved = null;
|
|
203
|
+
try {
|
|
204
|
+
resolved = path.isAbsolute(input) ? fs.realpathSync.native(input) : null;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
resolved = null;
|
|
208
|
+
}
|
|
209
|
+
cache.set(input, resolved);
|
|
210
|
+
return resolved;
|
|
211
|
+
}
|
|
212
|
+
function resolveRoots(workspacePaths, includeWorktrees, realpaths) {
|
|
213
|
+
const roots = new Set();
|
|
214
|
+
const listingRoots = new Set();
|
|
215
|
+
const add = (candidate) => {
|
|
216
|
+
const real = realpathOrNull(candidate, realpaths);
|
|
217
|
+
if (!real)
|
|
218
|
+
return;
|
|
219
|
+
roots.add(real);
|
|
220
|
+
listingRoots.add(real);
|
|
221
|
+
listingRoots.add(path.resolve(candidate));
|
|
222
|
+
};
|
|
223
|
+
for (const workspacePath of workspacePaths) {
|
|
224
|
+
if (typeof workspacePath !== 'string' || !path.isAbsolute(workspacePath))
|
|
225
|
+
continue;
|
|
226
|
+
add(workspacePath);
|
|
227
|
+
if (!includeWorktrees)
|
|
228
|
+
continue;
|
|
229
|
+
const real = realpathOrNull(workspacePath, realpaths);
|
|
230
|
+
if (!real)
|
|
231
|
+
continue;
|
|
232
|
+
const mainRepo = (0, sessionPathResolver_1.resolveWorktreeMainRepo)(real) ?? real;
|
|
233
|
+
add(mainRepo);
|
|
234
|
+
for (const sibling of (0, sessionPathResolver_1.discoverWorktreeSiblings)(mainRepo))
|
|
235
|
+
add(sibling);
|
|
236
|
+
}
|
|
237
|
+
return { roots: [...roots], listingRoots: [...listingRoots] };
|
|
238
|
+
}
|
|
239
|
+
/** Fail-closed scope check on a recorded working directory. */
|
|
240
|
+
function scopedCwd(cwd, roots, realpaths) {
|
|
241
|
+
if (typeof cwd !== 'string' || cwd.length === 0)
|
|
242
|
+
return null;
|
|
243
|
+
const real = realpathOrNull(cwd, realpaths);
|
|
244
|
+
if (!real)
|
|
245
|
+
return null;
|
|
246
|
+
for (const root of roots) {
|
|
247
|
+
if (real === root)
|
|
248
|
+
return real;
|
|
249
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
250
|
+
if (real.startsWith(prefix))
|
|
251
|
+
return real;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
async function listCandidates(providers, listingRoots, sinceMs) {
|
|
256
|
+
const byPath = new Map();
|
|
257
|
+
const listOptions = sinceMs !== null ? { since: sinceMs } : {};
|
|
258
|
+
for (const provider of [...new Set(providers)]) {
|
|
259
|
+
if (!ALL_PROVIDERS.includes(provider))
|
|
260
|
+
continue;
|
|
261
|
+
// Listings are only candidate supersets; scope is re-checked per prompt.
|
|
262
|
+
const source = provider === 'claude-code' ? new claudeCode_1.ClaudeCodeProvider() : new codex_1.CodexProvider();
|
|
263
|
+
for (const root of listingRoots) {
|
|
264
|
+
let files;
|
|
265
|
+
try {
|
|
266
|
+
files = (await source.listSessionFilesAsync?.(root, listOptions)) ?? [];
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
files = [];
|
|
270
|
+
}
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
if (byPath.has(file.path))
|
|
273
|
+
continue;
|
|
274
|
+
const sessionId = file.sessionId ?? path.basename(file.path, '.jsonl');
|
|
275
|
+
byPath.set(file.path, {
|
|
276
|
+
provider,
|
|
277
|
+
sessionId,
|
|
278
|
+
filePath: file.path,
|
|
279
|
+
key: `${provider}:${sessionId}:${hashPath(file.path)}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
source.dispose();
|
|
284
|
+
}
|
|
285
|
+
return [...byPath.values()].sort((left, right) => compareStrings(left.provider, right.provider) ||
|
|
286
|
+
compareStrings(left.sessionId, right.sessionId) ||
|
|
287
|
+
compareStrings(left.key, right.key));
|
|
288
|
+
}
|
|
289
|
+
function hashPath(filePath) {
|
|
290
|
+
return (0, crypto_1.createHash)('sha256').update(filePath).digest('hex').slice(0, 12);
|
|
291
|
+
}
|
|
292
|
+
async function readRange(filePath, offset, length) {
|
|
293
|
+
const buffer = Buffer.alloc(length);
|
|
294
|
+
if (length === 0)
|
|
295
|
+
return buffer;
|
|
296
|
+
const handle = await fs.promises.open(filePath, 'r');
|
|
297
|
+
try {
|
|
298
|
+
let read = 0;
|
|
299
|
+
while (read < length) {
|
|
300
|
+
const { bytesRead } = await handle.read(buffer, read, length - read, offset + read);
|
|
301
|
+
if (bytesRead === 0)
|
|
302
|
+
break;
|
|
303
|
+
read += bytesRead;
|
|
304
|
+
}
|
|
305
|
+
return read === length ? buffer : buffer.subarray(0, read);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await handle.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function yieldToEventLoop() {
|
|
312
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
313
|
+
}
|
|
314
|
+
async function scanBuffer(candidate, buffer, startOffset, resume, context) {
|
|
315
|
+
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
316
|
+
const complete = lastNewline >= 0 ? buffer.subarray(0, lastNewline + 1) : Buffer.alloc(0);
|
|
317
|
+
const lines = complete.toString('utf8').split('\n');
|
|
318
|
+
const scanner = candidate.provider === 'claude-code'
|
|
319
|
+
? new ClaudeLineScanner(candidate, resume, context)
|
|
320
|
+
: new CodexLineScanner(candidate, resume, context);
|
|
321
|
+
for (let index = 0; index < lines.length; index++) {
|
|
322
|
+
if (index > 0 && index % CHECK_EVERY_LINES === 0) {
|
|
323
|
+
await yieldToEventLoop();
|
|
324
|
+
const stop = context.shouldStop();
|
|
325
|
+
if (stop)
|
|
326
|
+
return { ...scanner.outcome(startOffset), stoppedBy: stop };
|
|
327
|
+
}
|
|
328
|
+
const line = lines[index];
|
|
329
|
+
if (!line.trim())
|
|
330
|
+
continue;
|
|
331
|
+
let record;
|
|
332
|
+
try {
|
|
333
|
+
record = JSON.parse(line);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (typeof record !== 'object' || record === null)
|
|
339
|
+
continue;
|
|
340
|
+
if (scanner.accept(record) === 'excluded') {
|
|
341
|
+
// Non-interactive Codex session: nothing in it will ever count.
|
|
342
|
+
return {
|
|
343
|
+
...scanner.outcome(startOffset + buffer.length),
|
|
344
|
+
entries: [],
|
|
345
|
+
notInteractive: true,
|
|
346
|
+
stoppedBy: null,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { ...scanner.outcome(startOffset + complete.length), stoppedBy: null };
|
|
351
|
+
}
|
|
352
|
+
/** Shared per-file prompt bookkeeping for both providers. */
|
|
353
|
+
class LineScanner {
|
|
354
|
+
candidate;
|
|
355
|
+
context;
|
|
356
|
+
entries = [];
|
|
357
|
+
nextOrdinal;
|
|
358
|
+
inScope = 0;
|
|
359
|
+
outOfScope = 0;
|
|
360
|
+
pending = null;
|
|
361
|
+
constructor(candidate, resume, context) {
|
|
362
|
+
this.candidate = candidate;
|
|
363
|
+
this.context = context;
|
|
364
|
+
this.nextOrdinal = (resume?.lastOrdinal ?? -1) + 1;
|
|
365
|
+
}
|
|
366
|
+
state() {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
outcome(offset) {
|
|
370
|
+
const state = this.state();
|
|
371
|
+
return {
|
|
372
|
+
entries: this.entries,
|
|
373
|
+
lastOrdinal: this.nextOrdinal - 1,
|
|
374
|
+
offset,
|
|
375
|
+
...(state ? { state } : {}),
|
|
376
|
+
inScope: this.inScope,
|
|
377
|
+
outOfScope: this.outOfScope,
|
|
378
|
+
notInteractive: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Assign an ordinal to a human prompt and emit it when in scope. */
|
|
382
|
+
recordPrompt(text, timestamp, cwd, gitBranch) {
|
|
383
|
+
const ordinal = this.nextOrdinal++;
|
|
384
|
+
this.pending = null;
|
|
385
|
+
if (typeof timestamp !== 'string' || Number.isNaN(Date.parse(timestamp))) {
|
|
386
|
+
this.context.stats.promptsMissingTimestamp++;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const scoped = scopedCwd(cwd, this.context.roots, this.context.realpaths);
|
|
390
|
+
if (!scoped) {
|
|
391
|
+
this.outOfScope++;
|
|
392
|
+
this.context.stats.promptsOutOfScope++;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.inScope++;
|
|
396
|
+
if (this.context.sinceMs !== null && Date.parse(timestamp) < this.context.sinceMs)
|
|
397
|
+
return;
|
|
398
|
+
const entry = {
|
|
399
|
+
provider: this.candidate.provider,
|
|
400
|
+
sessionId: this.candidate.sessionId,
|
|
401
|
+
ordinal,
|
|
402
|
+
timestamp,
|
|
403
|
+
text,
|
|
404
|
+
cwd: scoped,
|
|
405
|
+
...(gitBranch ? { gitBranch } : {}),
|
|
406
|
+
};
|
|
407
|
+
this.entries.push(entry);
|
|
408
|
+
this.pending = entry;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function transcriptMessage(candidate, record, role, content, extra, eventIndex) {
|
|
412
|
+
return {
|
|
413
|
+
role: 'user',
|
|
414
|
+
timestamp: typeof record.timestamp === 'string' ? record.timestamp : '',
|
|
415
|
+
content,
|
|
416
|
+
text: content
|
|
417
|
+
.filter((block) => block.type === 'text')
|
|
418
|
+
.map((block) => block.text ?? '')
|
|
419
|
+
.join('\n'),
|
|
420
|
+
tools: [],
|
|
421
|
+
commands: [],
|
|
422
|
+
source: {
|
|
423
|
+
provider: candidate.provider,
|
|
424
|
+
sessionId: candidate.sessionId,
|
|
425
|
+
originalRole: role,
|
|
426
|
+
eventIndex,
|
|
427
|
+
eventType: 'user',
|
|
428
|
+
...extra,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function stringField(value) {
|
|
433
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
434
|
+
}
|
|
435
|
+
function tokenCount(value) {
|
|
436
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
437
|
+
}
|
|
438
|
+
function compactUsage(usage) {
|
|
439
|
+
const result = {};
|
|
440
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
441
|
+
if (typeof value === 'number')
|
|
442
|
+
result[key] = value;
|
|
443
|
+
}
|
|
444
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
445
|
+
}
|
|
446
|
+
class ClaudeLineScanner extends LineScanner {
|
|
447
|
+
lineIndex = 0;
|
|
448
|
+
answerId = null;
|
|
449
|
+
accept(record) {
|
|
450
|
+
const index = this.lineIndex++;
|
|
451
|
+
const message = record.message;
|
|
452
|
+
if (typeof message !== 'object' || message === null)
|
|
453
|
+
return 'ok';
|
|
454
|
+
if (record.type === 'user') {
|
|
455
|
+
const origin = record.origin;
|
|
456
|
+
const canonical = transcriptMessage(this.candidate, record, typeof message.role === 'string' ? message.role : 'user', claudeBlocks(message.content), {
|
|
457
|
+
entrypoint: stringField(record.entrypoint),
|
|
458
|
+
isMeta: record.isMeta === true ? true : undefined,
|
|
459
|
+
isSidechain: record.isSidechain === true ? true : undefined,
|
|
460
|
+
isCompactSummary: record.isCompactSummary === true ? true : undefined,
|
|
461
|
+
originKind: origin && typeof origin.kind === 'string' ? origin.kind : undefined,
|
|
462
|
+
promptSource: stringField(record.promptSource),
|
|
463
|
+
}, index);
|
|
464
|
+
const text = (0, humanPrompt_1.humanPromptText)(canonical, 'claude-code');
|
|
465
|
+
if (text === null)
|
|
466
|
+
return 'ok';
|
|
467
|
+
this.answerId = null;
|
|
468
|
+
this.recordPrompt(text, record.timestamp, record.cwd, stringField(record.gitBranch));
|
|
469
|
+
return 'ok';
|
|
470
|
+
}
|
|
471
|
+
if (record.type === 'assistant' && this.pending && record.isSidechain !== true) {
|
|
472
|
+
const id = stringField(message.id) ?? `line:${index}`;
|
|
473
|
+
if (this.answerId === null)
|
|
474
|
+
this.answerId = id;
|
|
475
|
+
if (id !== this.answerId) {
|
|
476
|
+
this.pending = null;
|
|
477
|
+
return 'ok';
|
|
478
|
+
}
|
|
479
|
+
const model = stringField(message.model);
|
|
480
|
+
if (model && model !== '<synthetic>')
|
|
481
|
+
this.pending.model = model;
|
|
482
|
+
const usage = message.usage;
|
|
483
|
+
if (usage && typeof usage === 'object') {
|
|
484
|
+
const mapped = compactUsage({
|
|
485
|
+
inputTokens: tokenCount(usage.input_tokens),
|
|
486
|
+
outputTokens: tokenCount(usage.output_tokens),
|
|
487
|
+
cacheReadTokens: tokenCount(usage.cache_read_input_tokens),
|
|
488
|
+
cacheWriteTokens: tokenCount(usage.cache_creation_input_tokens),
|
|
489
|
+
});
|
|
490
|
+
if (mapped)
|
|
491
|
+
this.pending.usage = mapped;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return 'ok';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function claudeBlocks(content) {
|
|
498
|
+
if (typeof content === 'string')
|
|
499
|
+
return [{ type: 'text', text: content }];
|
|
500
|
+
if (!Array.isArray(content))
|
|
501
|
+
return [];
|
|
502
|
+
const blocks = [];
|
|
503
|
+
for (const item of content) {
|
|
504
|
+
if (typeof item !== 'object' || item === null)
|
|
505
|
+
continue;
|
|
506
|
+
const block = item;
|
|
507
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
508
|
+
blocks.push({ type: 'text', text: block.text });
|
|
509
|
+
}
|
|
510
|
+
else if (block.type === 'tool_result') {
|
|
511
|
+
blocks.push({ type: 'tool_result' });
|
|
512
|
+
}
|
|
513
|
+
else if (block.type === 'image') {
|
|
514
|
+
blocks.push({ type: 'image' });
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
blocks.push({ type: 'unknown' });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return blocks;
|
|
521
|
+
}
|
|
522
|
+
class CodexLineScanner extends LineScanner {
|
|
523
|
+
lineIndex = 0;
|
|
524
|
+
resumeState;
|
|
525
|
+
constructor(candidate, resume, context) {
|
|
526
|
+
super(candidate, resume, context);
|
|
527
|
+
this.resumeState = { ...(resume?.state ?? {}) };
|
|
528
|
+
}
|
|
529
|
+
state() {
|
|
530
|
+
return { ...this.resumeState };
|
|
531
|
+
}
|
|
532
|
+
accept(record) {
|
|
533
|
+
const index = this.lineIndex++;
|
|
534
|
+
const payload = record.payload;
|
|
535
|
+
if (typeof payload !== 'object' || payload === null)
|
|
536
|
+
return 'ok';
|
|
537
|
+
const state = this.resumeState;
|
|
538
|
+
if (record.type === 'session_meta') {
|
|
539
|
+
// Forked sessions embed their parent's session_meta after their own; only the first counts.
|
|
540
|
+
if (state.meta)
|
|
541
|
+
return 'ok';
|
|
542
|
+
state.meta = true;
|
|
543
|
+
const source = typeof payload.source === 'string' ? payload.source : null;
|
|
544
|
+
if (!source || !humanPrompt_1.HUMAN_CODEX_SOURCES.includes(source) || stringField(payload.forked_from_id)) {
|
|
545
|
+
state.excluded = true;
|
|
546
|
+
return 'excluded';
|
|
547
|
+
}
|
|
548
|
+
state.cwd = stringField(payload.cwd);
|
|
549
|
+
const git = payload.git;
|
|
550
|
+
state.gitBranch = git && typeof git === 'object' ? stringField(git.branch) : undefined;
|
|
551
|
+
return 'ok';
|
|
552
|
+
}
|
|
553
|
+
if (record.type === 'turn_context') {
|
|
554
|
+
const cwd = stringField(payload.cwd);
|
|
555
|
+
if (cwd)
|
|
556
|
+
state.cwd = cwd;
|
|
557
|
+
const model = stringField(payload.model);
|
|
558
|
+
if (model)
|
|
559
|
+
state.model = model;
|
|
560
|
+
return 'ok';
|
|
561
|
+
}
|
|
562
|
+
if (record.type === 'response_item' && payload.type === 'message' && payload.role === 'user') {
|
|
563
|
+
// Without session metadata the source (and thus interactivity) is unknown: fail closed.
|
|
564
|
+
if (!state.meta) {
|
|
565
|
+
state.excluded = true;
|
|
566
|
+
return 'excluded';
|
|
567
|
+
}
|
|
568
|
+
const canonical = transcriptMessage(this.candidate, record, 'user', codexBlocks(payload.content), {}, index);
|
|
569
|
+
const text = (0, humanPrompt_1.humanPromptText)(canonical, 'codex');
|
|
570
|
+
if (text === null)
|
|
571
|
+
return 'ok';
|
|
572
|
+
this.recordPrompt(text, record.timestamp, state.cwd, state.gitBranch);
|
|
573
|
+
return 'ok';
|
|
574
|
+
}
|
|
575
|
+
if (record.type === 'event_msg' && payload.type === 'token_count' && this.pending) {
|
|
576
|
+
const info = payload.info;
|
|
577
|
+
const last = info && typeof info === 'object' ? info.last_token_usage : undefined;
|
|
578
|
+
if (!last || typeof last !== 'object')
|
|
579
|
+
return 'ok';
|
|
580
|
+
const input = tokenCount(last.input_tokens);
|
|
581
|
+
const cached = tokenCount(last.cached_input_tokens);
|
|
582
|
+
const mapped = compactUsage({
|
|
583
|
+
inputTokens: input !== undefined ? Math.max(0, input - (cached ?? 0)) : undefined,
|
|
584
|
+
outputTokens: tokenCount(last.output_tokens),
|
|
585
|
+
cacheReadTokens: cached,
|
|
586
|
+
cacheWriteTokens: tokenCount(last.cache_write_input_tokens),
|
|
587
|
+
});
|
|
588
|
+
if (mapped)
|
|
589
|
+
this.pending.usage = mapped;
|
|
590
|
+
if (state.model)
|
|
591
|
+
this.pending.model = state.model;
|
|
592
|
+
this.pending = null;
|
|
593
|
+
}
|
|
594
|
+
return 'ok';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function codexBlocks(content) {
|
|
598
|
+
if (!Array.isArray(content))
|
|
599
|
+
return [];
|
|
600
|
+
const blocks = [];
|
|
601
|
+
for (const item of content) {
|
|
602
|
+
if (typeof item !== 'object' || item === null)
|
|
603
|
+
continue;
|
|
604
|
+
const block = item;
|
|
605
|
+
if ((block.type === 'input_text' || block.type === 'text') && typeof block.text === 'string') {
|
|
606
|
+
blocks.push({ type: 'text', text: block.text });
|
|
607
|
+
}
|
|
608
|
+
else if (block.type === 'input_image') {
|
|
609
|
+
blocks.push({ type: 'image' });
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
blocks.push({ type: 'unknown' });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return blocks;
|
|
616
|
+
}
|
|
@@ -65,6 +65,11 @@ export declare const userSessionEventSchema: z.ZodObject<{
|
|
|
65
65
|
entrypoint: z.ZodOptional<z.ZodString>;
|
|
66
66
|
isMeta: z.ZodOptional<z.ZodBoolean>;
|
|
67
67
|
isSidechain: z.ZodOptional<z.ZodBoolean>;
|
|
68
|
+
isCompactSummary: z.ZodOptional<z.ZodBoolean>;
|
|
69
|
+
origin: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
promptSource: z.ZodOptional<z.ZodString>;
|
|
68
73
|
cwd: z.ZodOptional<z.ZodString>;
|
|
69
74
|
gitBranch: z.ZodOptional<z.ZodString>;
|
|
70
75
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -145,6 +150,11 @@ export declare const assistantSessionEventSchema: z.ZodObject<{
|
|
|
145
150
|
entrypoint: z.ZodOptional<z.ZodString>;
|
|
146
151
|
isMeta: z.ZodOptional<z.ZodBoolean>;
|
|
147
152
|
isSidechain: z.ZodOptional<z.ZodBoolean>;
|
|
153
|
+
isCompactSummary: z.ZodOptional<z.ZodBoolean>;
|
|
154
|
+
origin: z.ZodOptional<z.ZodObject<{
|
|
155
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
promptSource: z.ZodOptional<z.ZodString>;
|
|
148
158
|
cwd: z.ZodOptional<z.ZodString>;
|
|
149
159
|
gitBranch: z.ZodOptional<z.ZodString>;
|
|
150
160
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -226,6 +236,11 @@ export declare const systemSessionEventSchema: z.ZodObject<{
|
|
|
226
236
|
entrypoint: z.ZodOptional<z.ZodString>;
|
|
227
237
|
isMeta: z.ZodOptional<z.ZodBoolean>;
|
|
228
238
|
isSidechain: z.ZodOptional<z.ZodBoolean>;
|
|
239
|
+
isCompactSummary: z.ZodOptional<z.ZodBoolean>;
|
|
240
|
+
origin: z.ZodOptional<z.ZodObject<{
|
|
241
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
242
|
+
}, z.core.$strip>>;
|
|
243
|
+
promptSource: z.ZodOptional<z.ZodString>;
|
|
229
244
|
cwd: z.ZodOptional<z.ZodString>;
|
|
230
245
|
gitBranch: z.ZodOptional<z.ZodString>;
|
|
231
246
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -310,6 +325,11 @@ export declare const toolUseSessionEventSchema: z.ZodObject<{
|
|
|
310
325
|
entrypoint: z.ZodOptional<z.ZodString>;
|
|
311
326
|
isMeta: z.ZodOptional<z.ZodBoolean>;
|
|
312
327
|
isSidechain: z.ZodOptional<z.ZodBoolean>;
|
|
328
|
+
isCompactSummary: z.ZodOptional<z.ZodBoolean>;
|
|
329
|
+
origin: z.ZodOptional<z.ZodObject<{
|
|
330
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
331
|
+
}, z.core.$strip>>;
|
|
332
|
+
promptSource: z.ZodOptional<z.ZodString>;
|
|
313
333
|
cwd: z.ZodOptional<z.ZodString>;
|
|
314
334
|
gitBranch: z.ZodOptional<z.ZodString>;
|
|
315
335
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -395,6 +415,11 @@ export declare const toolResultSessionEventSchema: z.ZodObject<{
|
|
|
395
415
|
entrypoint: z.ZodOptional<z.ZodString>;
|
|
396
416
|
isMeta: z.ZodOptional<z.ZodBoolean>;
|
|
397
417
|
isSidechain: z.ZodOptional<z.ZodBoolean>;
|
|
418
|
+
isCompactSummary: z.ZodOptional<z.ZodBoolean>;
|
|
419
|
+
origin: z.ZodOptional<z.ZodObject<{
|
|
420
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
421
|
+
}, z.core.$strip>>;
|
|
422
|
+
promptSource: z.ZodOptional<z.ZodString>;
|
|
398
423
|
cwd: z.ZodOptional<z.ZodString>;
|
|
399
424
|
gitBranch: z.ZodOptional<z.ZodString>;
|
|
400
425
|
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
@@ -45,6 +45,9 @@ const eventBaseShape = {
|
|
|
45
45
|
entrypoint: zod_1.z.string().optional(),
|
|
46
46
|
isMeta: zod_1.z.boolean().optional(),
|
|
47
47
|
isSidechain: zod_1.z.boolean().optional(),
|
|
48
|
+
isCompactSummary: zod_1.z.boolean().optional(),
|
|
49
|
+
origin: zod_1.z.object({ kind: zod_1.z.string().optional() }).optional(),
|
|
50
|
+
promptSource: zod_1.z.string().optional(),
|
|
48
51
|
cwd: zod_1.z.string().optional(),
|
|
49
52
|
gitBranch: zod_1.z.string().optional(),
|
|
50
53
|
permissionMode: exports.permissionModeSchema.optional(),
|
package/dist/transcript.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface TranscriptSourceProvenance {
|
|
|
10
10
|
entrypoint?: string;
|
|
11
11
|
isMeta?: boolean;
|
|
12
12
|
isSidechain?: boolean;
|
|
13
|
+
/** Claude Code compaction/continuation summary flag. */
|
|
14
|
+
isCompactSummary?: boolean;
|
|
15
|
+
/** Claude Code `origin.kind` (e.g. `human`, `task-notification`, `coordinator`). */
|
|
16
|
+
originKind?: string;
|
|
17
|
+
/** Claude Code `promptSource` (e.g. `typed`, `queued`, `system`). */
|
|
18
|
+
promptSource?: string;
|
|
13
19
|
originalRole?: string;
|
|
14
20
|
eventIndex: number;
|
|
15
21
|
eventType: SessionEvent['type'];
|