@phnx-labs/agents-cli 1.21.2 → 1.22.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 +101 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +35 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/perf.d.ts +16 -0
- package/dist/commands/perf.js +11 -1
- package/dist/commands/projects.d.ts +11 -1
- package/dist/commands/projects.js +38 -4
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +22 -1
- package/dist/commands/sessions.js +331 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/index.js +8 -0
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/exec.js +39 -8
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/profile.js +1 -14
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +1 -2
- package/dist/lib/perf/db.js +2 -14
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/routines.js +1 -14
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +21 -5
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +22 -1
- package/dist/lib/session/db.js +516 -24
- package/dist/lib/session/discover.d.ts +68 -7
- package/dist/lib/session/discover.js +186 -84
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +102 -35
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +49 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +31 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
|
@@ -23,7 +23,8 @@ import { walkForFilesWithStat } from '../fs-walk.js';
|
|
|
23
23
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
24
24
|
import { SESSION_AGENTS } from './types.js';
|
|
25
25
|
import { deriveShortId } from './short-id.js';
|
|
26
|
-
import { extractSessionTopic } from './prompt.js';
|
|
26
|
+
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
27
|
+
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
27
28
|
import { parseAntigravity, parseCursor } from './parse.js';
|
|
28
29
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
29
30
|
import { costOfUsage } from '../pricing/index.js';
|
|
@@ -33,6 +34,8 @@ import { mapBounded } from '../concurrency.js';
|
|
|
33
34
|
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, cacheLinearProject, } from './db.js';
|
|
34
35
|
import { buildRunNameMap } from './run-names.js';
|
|
35
36
|
import { resolveLinearApiKey } from '../auto-dispatch-linear.js';
|
|
37
|
+
import { ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, } from './tool-calls.js';
|
|
38
|
+
import { purgeMissingToolCallsInDirectory } from './tool-store.js';
|
|
36
39
|
const HOME = os.homedir();
|
|
37
40
|
// Versions can live under either repo: the user repo (current canonical
|
|
38
41
|
// location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
|
|
@@ -46,6 +49,73 @@ const HERMES_SESSIONS_DIR = path.join(HOME, '.hermes', 'sessions');
|
|
|
46
49
|
/** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
|
|
47
50
|
const OPENCLAW_TTL_MS = 60_000;
|
|
48
51
|
const ACTIVE_APPEND_RESCAN_DEBOUNCE_MS = 5_000;
|
|
52
|
+
/** One JSONL record may not force an unbounded string allocation. */
|
|
53
|
+
export const SESSION_JSONL_LINE_MAX_BYTES = 1024 * 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Stream one appended JSONL range, applying only newline-terminated records.
|
|
56
|
+
* Memory is bounded to one record. An ordinary unterminated tail remains for the
|
|
57
|
+
* next scan. Once a record exceeds the cap, its consumed offset and a one-bit
|
|
58
|
+
* dropping state advance together so later scans never reread the growing tail.
|
|
59
|
+
*/
|
|
60
|
+
async function applyJsonlAppend(filePath, fromOffset, wasDroppingOversizedLine, apply) {
|
|
61
|
+
let pending = Buffer.alloc(0);
|
|
62
|
+
let droppingOversizedLine = wasDroppingOversizedLine;
|
|
63
|
+
let bytesBeforeChunk = 0;
|
|
64
|
+
let consumedBytes = 0;
|
|
65
|
+
let skippedOversizedLine = false;
|
|
66
|
+
const stream = fs.createReadStream(filePath, { start: fromOffset });
|
|
67
|
+
try {
|
|
68
|
+
for await (const rawChunk of stream) {
|
|
69
|
+
const chunk = typeof rawChunk === 'string' ? Buffer.from(rawChunk, 'utf-8') : rawChunk;
|
|
70
|
+
let cursor = 0;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const newline = chunk.indexOf(0x0a, cursor);
|
|
73
|
+
if (newline === -1) {
|
|
74
|
+
if (!droppingOversizedLine) {
|
|
75
|
+
const tail = chunk.subarray(cursor);
|
|
76
|
+
if (pending.length + tail.length > SESSION_JSONL_LINE_MAX_BYTES) {
|
|
77
|
+
pending = Buffer.alloc(0);
|
|
78
|
+
droppingOversizedLine = true;
|
|
79
|
+
skippedOversizedLine = true;
|
|
80
|
+
}
|
|
81
|
+
else if (tail.length > 0) {
|
|
82
|
+
pending = pending.length > 0 ? Buffer.concat([pending, tail]) : Buffer.from(tail);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (droppingOversizedLine)
|
|
86
|
+
consumedBytes = bytesBeforeChunk + chunk.length;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (!droppingOversizedLine) {
|
|
90
|
+
const segment = chunk.subarray(cursor, newline);
|
|
91
|
+
if (pending.length + segment.length <= SESSION_JSONL_LINE_MAX_BYTES) {
|
|
92
|
+
const line = (pending.length > 0 ? Buffer.concat([pending, segment]) : segment).toString('utf-8');
|
|
93
|
+
if (line.trim()) {
|
|
94
|
+
try {
|
|
95
|
+
apply(JSON.parse(line));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// One malformed line never aborts the session scan.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
skippedOversizedLine = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
pending = Buffer.alloc(0);
|
|
107
|
+
droppingOversizedLine = false;
|
|
108
|
+
consumedBytes = bytesBeforeChunk + newline + 1;
|
|
109
|
+
cursor = newline + 1;
|
|
110
|
+
}
|
|
111
|
+
bytesBeforeChunk += chunk.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
stream.destroy();
|
|
116
|
+
}
|
|
117
|
+
return { consumedBytes, droppingOversizedLine, skippedOversizedLine };
|
|
118
|
+
}
|
|
49
119
|
/**
|
|
50
120
|
* How recently a file must have been scanned to be treated as "hot" — a
|
|
51
121
|
* candidate for an in-place append even when its parent dir's mtime hasn't
|
|
@@ -101,8 +171,17 @@ export async function discoverSessions(options) {
|
|
|
101
171
|
releaseScan(process.pid);
|
|
102
172
|
}
|
|
103
173
|
}
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
return queryIndexedSessions(options, {
|
|
175
|
+
skipExistenceCheck: options?.skipExistenceCheck ?? false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
|
|
179
|
+
export async function queryIndexedSessions(options, indexedOptions = {}) {
|
|
180
|
+
getDB();
|
|
181
|
+
const agents = options?.agent ? [options.agent] : SESSION_AGENTS;
|
|
182
|
+
const sessions = querySessions(buildQueryOptions({ ...options, skipExistenceCheck: indexedOptions.skipExistenceCheck ?? true }, agents, { includeLimit: true }));
|
|
183
|
+
if (indexedOptions.resolveLinear !== false)
|
|
184
|
+
await resolveLinearProjects(sessions);
|
|
106
185
|
for (const s of sessions)
|
|
107
186
|
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
108
187
|
return scopeToManaged(sessions, agents, options);
|
|
@@ -155,7 +234,7 @@ async function resolveLinearProjects(sessions) {
|
|
|
155
234
|
* An agent with no managed versions is left alone entirely — someone who has never
|
|
156
235
|
* run `agents add` sees exactly what they saw before.
|
|
157
236
|
*/
|
|
158
|
-
function scopeToManaged(sessions, agents, options) {
|
|
237
|
+
export function scopeToManaged(sessions, agents, options) {
|
|
159
238
|
if (options?.includeUnmanaged)
|
|
160
239
|
return sessions;
|
|
161
240
|
if (!anyManagedVersions())
|
|
@@ -305,11 +384,14 @@ function buildQueryOptions(options, agents, opts) {
|
|
|
305
384
|
project: projectQuery,
|
|
306
385
|
sinceMs,
|
|
307
386
|
untilMs: Number.isFinite(untilMs) ? untilMs : undefined,
|
|
308
|
-
limit: opts.includeLimit ? (options?.limit ?? 50) : undefined,
|
|
387
|
+
limit: opts.includeLimit && !options?.unbounded ? (options?.limit ?? 50) : undefined,
|
|
309
388
|
excludeTeamOrigin: options?.excludeTeamOrigin,
|
|
310
389
|
onlyTeamOrigin: options?.onlyTeamOrigin,
|
|
311
390
|
origin: options?.origin,
|
|
312
391
|
sortBy: options?.sortBy,
|
|
392
|
+
skipExistenceCheck: options?.skipExistenceCheck,
|
|
393
|
+
skill: options?.skill,
|
|
394
|
+
plugin: options?.plugin,
|
|
313
395
|
};
|
|
314
396
|
}
|
|
315
397
|
/**
|
|
@@ -579,6 +661,7 @@ export function collectChangedFilesInLeafDirs(leafDirs, ext) {
|
|
|
579
661
|
continue;
|
|
580
662
|
toCompare.push({ filePath, fileMtimeMs: stat.mtimeMs, fileSize: stat.size });
|
|
581
663
|
}
|
|
664
|
+
purgeMissingToolCallsInDirectory(getDB(), dirPath, files);
|
|
582
665
|
if (!disabled)
|
|
583
666
|
dirScansToRecord.push({ dirPath, dirMtimeMs, entryCount });
|
|
584
667
|
}
|
|
@@ -786,7 +869,13 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
|
|
|
786
869
|
try {
|
|
787
870
|
const result = await readRoutineArchiveMeta(agent, filePath);
|
|
788
871
|
if (result)
|
|
789
|
-
entries.push({
|
|
872
|
+
entries.push({
|
|
873
|
+
meta: result.meta,
|
|
874
|
+
content: result.content,
|
|
875
|
+
scan,
|
|
876
|
+
toolCalls: result.toolCalls,
|
|
877
|
+
toolIndexMode: result.toolIndexMode,
|
|
878
|
+
});
|
|
790
879
|
else
|
|
791
880
|
touched.push({ filePath, scan });
|
|
792
881
|
}
|
|
@@ -961,6 +1050,8 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
961
1050
|
scan,
|
|
962
1051
|
parserState: result.parserState,
|
|
963
1052
|
contentText: result.contentText,
|
|
1053
|
+
toolCalls: result.toolCalls,
|
|
1054
|
+
toolIndexMode: result.toolIndexMode,
|
|
964
1055
|
});
|
|
965
1056
|
}
|
|
966
1057
|
else {
|
|
@@ -990,7 +1081,7 @@ async function scanClaudeIncremental(onProgress) {
|
|
|
990
1081
|
*/
|
|
991
1082
|
async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account, label) {
|
|
992
1083
|
const prior = parsePriorClaudeState(priorRow);
|
|
993
|
-
const { scan, newState, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
1084
|
+
const { scan, newState, toolCalls, mode } = await scanClaudeSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
994
1085
|
if (mode === 'incremental')
|
|
995
1086
|
claudeIncrementalScanCount++;
|
|
996
1087
|
else
|
|
@@ -1030,6 +1121,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1030
1121
|
plan: scan.plan,
|
|
1031
1122
|
todos: scan.todos,
|
|
1032
1123
|
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1124
|
+
skillsUsed: scan.skillsUsed,
|
|
1125
|
+
slashCommandsUsed: scan.slashCommandsUsed,
|
|
1033
1126
|
};
|
|
1034
1127
|
}
|
|
1035
1128
|
else {
|
|
@@ -1061,6 +1154,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1061
1154
|
plan: scan.plan,
|
|
1062
1155
|
todos: scan.todos,
|
|
1063
1156
|
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1157
|
+
skillsUsed: scan.skillsUsed,
|
|
1158
|
+
slashCommandsUsed: scan.slashCommandsUsed,
|
|
1064
1159
|
};
|
|
1065
1160
|
}
|
|
1066
1161
|
return {
|
|
@@ -1071,6 +1166,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
1071
1166
|
// doc, cached so the resume can hydrate userTexts without re-reading the file.
|
|
1072
1167
|
parserState: JSON.stringify(newState),
|
|
1073
1168
|
contentText: newState.contentText,
|
|
1169
|
+
toolCalls,
|
|
1170
|
+
toolIndexMode: mode === 'full' ? 'replace' : 'append',
|
|
1074
1171
|
};
|
|
1075
1172
|
}
|
|
1076
1173
|
// ---------------------------------------------------------------------------
|
|
@@ -1208,6 +1305,8 @@ async function scanCodexIncremental(onProgress) {
|
|
|
1208
1305
|
scan,
|
|
1209
1306
|
parserState: result.parserState,
|
|
1210
1307
|
contentText: result.contentText,
|
|
1308
|
+
toolCalls: result.toolCalls,
|
|
1309
|
+
toolIndexMode: result.toolIndexMode,
|
|
1211
1310
|
});
|
|
1212
1311
|
}
|
|
1213
1312
|
else {
|
|
@@ -1310,6 +1409,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1310
1409
|
let scan;
|
|
1311
1410
|
let newState;
|
|
1312
1411
|
let newOffset = 0;
|
|
1412
|
+
let toolCalls;
|
|
1413
|
+
let toolIndexMode = 'replace';
|
|
1313
1414
|
if (scanStamp) {
|
|
1314
1415
|
const prior = parsePriorCodexState(priorRow);
|
|
1315
1416
|
const result = await scanCodexSessionResumable(filePath, prior, scanStamp.fileMtimeMs, scanStamp.fileSize, priorRow?.fileMtimeMs);
|
|
@@ -1320,6 +1421,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1320
1421
|
scan = result.scan;
|
|
1321
1422
|
newState = result.newState;
|
|
1322
1423
|
newOffset = result.newOffset;
|
|
1424
|
+
toolCalls = result.toolCalls;
|
|
1425
|
+
toolIndexMode = result.mode === 'full' ? 'replace' : 'append';
|
|
1323
1426
|
}
|
|
1324
1427
|
else {
|
|
1325
1428
|
scan = await scanCodexSession(filePath);
|
|
@@ -1366,6 +1469,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1366
1469
|
// doc for the resume's hydrate. Absent when no stamp was supplied.
|
|
1367
1470
|
parserState: newState ? JSON.stringify(newState) : undefined,
|
|
1368
1471
|
contentText: newState?.contentText,
|
|
1472
|
+
toolCalls,
|
|
1473
|
+
toolIndexMode,
|
|
1369
1474
|
};
|
|
1370
1475
|
}
|
|
1371
1476
|
/**
|
|
@@ -2446,6 +2551,9 @@ export function initClaudeParseState() {
|
|
|
2446
2551
|
plan: undefined,
|
|
2447
2552
|
checklistEvents: [],
|
|
2448
2553
|
recentDirectoriesTouched: [],
|
|
2554
|
+
toolCollector: new ToolCallCollector(),
|
|
2555
|
+
skillEvents: [],
|
|
2556
|
+
slashCommandEvents: [],
|
|
2449
2557
|
};
|
|
2450
2558
|
}
|
|
2451
2559
|
const CHECKLIST_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan', 'TaskCreate', 'TaskUpdate']);
|
|
@@ -2453,6 +2561,16 @@ const DIRECTORY_TOOLS = new Set(['Edit', 'Write', 'edit_file', 'write_file', 'cr
|
|
|
2453
2561
|
function foldDerivedToolState(state, event) {
|
|
2454
2562
|
if (CHECKLIST_TOOLS.has(event.tool ?? ''))
|
|
2455
2563
|
state.checklistEvents.push(event);
|
|
2564
|
+
// #12: skill/slash-command usage, held here instead of re-parsed later — the
|
|
2565
|
+
// same reason checklistEvents is folded incrementally rather than recomputed
|
|
2566
|
+
// from a full re-parse (see session/db.ts's writeResourceUsage doc comment).
|
|
2567
|
+
if (isSkillInvocation(event))
|
|
2568
|
+
state.skillEvents.push(event);
|
|
2569
|
+
if (event.tool === 'SlashCommand') {
|
|
2570
|
+
const slashCommand = extractSlashCommandFromToolInput(event.args);
|
|
2571
|
+
if (slashCommand)
|
|
2572
|
+
state.slashCommandEvents.push({ ...event, slashCommand });
|
|
2573
|
+
}
|
|
2456
2574
|
if (!DIRECTORY_TOOLS.has(event.tool ?? ''))
|
|
2457
2575
|
return;
|
|
2458
2576
|
const next = extractRecentDirectoriesTouched([event], state.cwd);
|
|
@@ -2472,6 +2590,7 @@ function foldDerivedToolState(state, event) {
|
|
|
2472
2590
|
* malformed-line skip happens in the caller, as before).
|
|
2473
2591
|
*/
|
|
2474
2592
|
export function applyClaudeLine(state, parsed) {
|
|
2593
|
+
collectClaudeToolCalls(state.toolCollector, parsed);
|
|
2475
2594
|
// entrypoint ships on the first envelope event (attachment/user/assistant)
|
|
2476
2595
|
// and is the clean structural signal for "was this a team spawn?"
|
|
2477
2596
|
if (!state.entrypoint && typeof parsed.entrypoint === 'string') {
|
|
@@ -2582,6 +2701,15 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2582
2701
|
state.userTexts.push(text);
|
|
2583
2702
|
if (!state.topic)
|
|
2584
2703
|
state.topic = extractSessionTopic(text);
|
|
2704
|
+
// #12: the USER typing a slash command — Claude injects a <command-name>
|
|
2705
|
+
// wrapper as the message content (extractClaudeUserText returns it
|
|
2706
|
+
// un-stripped; isLocalCommandMessage only filters bash-echo wrappers).
|
|
2707
|
+
const slashCommand = extractSlashCommandName(text);
|
|
2708
|
+
if (slashCommand) {
|
|
2709
|
+
state.slashCommandEvents.push({
|
|
2710
|
+
type: 'message', agent: 'claude', timestamp: parsed.timestamp || '', role: 'user', content: text, slashCommand,
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2585
2713
|
}
|
|
2586
2714
|
return;
|
|
2587
2715
|
}
|
|
@@ -2662,6 +2790,8 @@ export function finalizeClaudeScan(state) {
|
|
|
2662
2790
|
plan: state.plan,
|
|
2663
2791
|
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
2664
2792
|
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
2793
|
+
skillsUsed: state.skillEvents.length ? extractSkills(state.skillEvents) : undefined,
|
|
2794
|
+
slashCommandsUsed: state.slashCommandEvents.length ? extractSlashCommands(state.slashCommandEvents) : undefined,
|
|
2665
2795
|
};
|
|
2666
2796
|
}
|
|
2667
2797
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
@@ -2696,15 +2826,16 @@ const SEEN_IDS_RECENT_CAP = 256;
|
|
|
2696
2826
|
* `offset` bytes consumed. Round-trips through {@link hydrateClaudeParseState}
|
|
2697
2827
|
* so incremental replay equals a full parse.
|
|
2698
2828
|
*/
|
|
2699
|
-
export function serializeClaudeParserState(state, offset) {
|
|
2829
|
+
export function serializeClaudeParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
2700
2830
|
const allIds = [...state.seenAssistantIds];
|
|
2701
2831
|
const seenIdsRecent = allIds.length > SEEN_IDS_RECENT_CAP
|
|
2702
2832
|
? allIds.slice(allIds.length - SEEN_IDS_RECENT_CAP)
|
|
2703
2833
|
: allIds;
|
|
2704
2834
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
2705
2835
|
return {
|
|
2706
|
-
v:
|
|
2836
|
+
v: 2,
|
|
2707
2837
|
offset,
|
|
2838
|
+
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
2708
2839
|
timestamp: state.timestamp,
|
|
2709
2840
|
cwd: state.cwd,
|
|
2710
2841
|
gitBranch: state.gitBranch,
|
|
@@ -2740,6 +2871,9 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2740
2871
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
2741
2872
|
checklistEvents: state.checklistEvents,
|
|
2742
2873
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
2874
|
+
toolCalls: state.toolCollector.snapshot(),
|
|
2875
|
+
skillEvents: state.skillEvents,
|
|
2876
|
+
slashCommandEvents: state.slashCommandEvents,
|
|
2743
2877
|
};
|
|
2744
2878
|
}
|
|
2745
2879
|
/**
|
|
@@ -2796,6 +2930,9 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2796
2930
|
plan: prior.plan,
|
|
2797
2931
|
checklistEvents: prior.checklistEvents ?? [],
|
|
2798
2932
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
2933
|
+
toolCollector: new ToolCallCollector(prior.toolCalls),
|
|
2934
|
+
skillEvents: prior.skillEvents ?? [],
|
|
2935
|
+
slashCommandEvents: prior.slashCommandEvents ?? [],
|
|
2799
2936
|
};
|
|
2800
2937
|
}
|
|
2801
2938
|
/**
|
|
@@ -2819,45 +2956,20 @@ export async function scanClaudeSessionIncremental(filePath, fromOffset, prior)
|
|
|
2819
2956
|
// have no dedup, unlike assistant `seenAssistantIds`). A record written
|
|
2820
2957
|
// non-atomically — bytes first, then '\n' in a second write — is exactly this
|
|
2821
2958
|
// case. So we slice at the last '\n' ourselves: everything up to and including
|
|
2822
|
-
// it is a run of complete lines we apply and commit
|
|
2823
|
-
//
|
|
2824
|
-
//
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
}
|
|
2832
|
-
finally {
|
|
2833
|
-
stream.destroy();
|
|
2834
|
-
}
|
|
2835
|
-
const appended = Buffer.concat(chunks);
|
|
2836
|
-
// Bytes up to AND INCLUDING the last '\n' are the committed, complete-line run.
|
|
2837
|
-
const lastNl = appended.lastIndexOf(0x0a);
|
|
2838
|
-
const consumedBytes = lastNl === -1 ? 0 : lastNl + 1;
|
|
2839
|
-
if (consumedBytes > 0) {
|
|
2840
|
-
// split('\n') on the committed run: the element after the final '\n' is ''
|
|
2841
|
-
// (skipped by the trim guard). A stray '\r' from CRLF is tolerated by the
|
|
2842
|
-
// JSON.parse below exactly as the full parse tolerates it.
|
|
2843
|
-
for (const line of appended.subarray(0, consumedBytes).toString('utf-8').split('\n')) {
|
|
2844
|
-
if (!line.trim())
|
|
2845
|
-
continue;
|
|
2846
|
-
let parsed;
|
|
2847
|
-
try {
|
|
2848
|
-
parsed = JSON.parse(line);
|
|
2849
|
-
}
|
|
2850
|
-
catch {
|
|
2851
|
-
continue;
|
|
2852
|
-
}
|
|
2853
|
-
applyClaudeLine(state, parsed);
|
|
2854
|
-
}
|
|
2855
|
-
}
|
|
2856
|
-
const newOffset = fromOffset + consumedBytes;
|
|
2959
|
+
// it is a run of complete lines we apply and commit. An ordinary tail after
|
|
2960
|
+
// it is deferred until its '\n' lands; after a tail exceeds 1 MiB, its offset
|
|
2961
|
+
// advances with a persisted discard-until-newline bit.
|
|
2962
|
+
const append = await applyJsonlAppend(filePath, fromOffset, prior.jsonlDroppingOversizedLine === true, (parsed) => applyClaudeLine(state, parsed));
|
|
2963
|
+
if (append.skippedOversizedLine)
|
|
2964
|
+
state.toolCollector.recordIndexLimit();
|
|
2965
|
+
const newOffset = fromOffset + append.consumedBytes;
|
|
2966
|
+
const scan = finalizeClaudeScan(state);
|
|
2967
|
+
const toolCalls = state.toolCollector.drainChanged();
|
|
2857
2968
|
return {
|
|
2858
|
-
scan
|
|
2859
|
-
newState: serializeClaudeParserState(state, newOffset),
|
|
2969
|
+
scan,
|
|
2970
|
+
newState: serializeClaudeParserState(state, newOffset, append.droppingOversizedLine),
|
|
2860
2971
|
newOffset,
|
|
2972
|
+
toolCalls,
|
|
2861
2973
|
};
|
|
2862
2974
|
}
|
|
2863
2975
|
/** Serialized zero-value continuation: a fresh accumulator at offset 0, used to drive a FULL parse from the start through the same resumable path. */
|
|
@@ -2952,7 +3064,7 @@ function parsePriorClaudeState(row) {
|
|
|
2952
3064
|
return null;
|
|
2953
3065
|
try {
|
|
2954
3066
|
const parsed = JSON.parse(row.parserState);
|
|
2955
|
-
if (parsed?.v !==
|
|
3067
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
2956
3068
|
return null;
|
|
2957
3069
|
return parsed;
|
|
2958
3070
|
}
|
|
@@ -2997,6 +3109,9 @@ export function initCodexParseState() {
|
|
|
2997
3109
|
spawnedTeam: undefined,
|
|
2998
3110
|
checklistEvents: [],
|
|
2999
3111
|
recentDirectoriesTouched: [],
|
|
3112
|
+
toolCollector: new ToolCallCollector(),
|
|
3113
|
+
skillEvents: [],
|
|
3114
|
+
slashCommandEvents: [],
|
|
3000
3115
|
};
|
|
3001
3116
|
}
|
|
3002
3117
|
/**
|
|
@@ -3006,6 +3121,7 @@ export function initCodexParseState() {
|
|
|
3006
3121
|
* malformed-line skip happens in the caller, as before).
|
|
3007
3122
|
*/
|
|
3008
3123
|
export function applyCodexLine(state, parsed) {
|
|
3124
|
+
collectCodexToolCalls(state.toolCollector, parsed);
|
|
3009
3125
|
// PR signal, structurally: a Codex `function_call` whose command is
|
|
3010
3126
|
// `gh pr create`, then the pull URL from a `function_call_output`.
|
|
3011
3127
|
if (parsed.type === 'response_item') {
|
|
@@ -3178,11 +3294,12 @@ async function scanCodexSession(filePath) {
|
|
|
3178
3294
|
* bytes consumed. Round-trips through {@link hydrateCodexParseState} so
|
|
3179
3295
|
* incremental replay equals a full parse.
|
|
3180
3296
|
*/
|
|
3181
|
-
export function serializeCodexParserState(state, offset) {
|
|
3297
|
+
export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
3182
3298
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3183
3299
|
return {
|
|
3184
|
-
v:
|
|
3300
|
+
v: 2,
|
|
3185
3301
|
offset,
|
|
3302
|
+
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3186
3303
|
sessionId: state.sessionId,
|
|
3187
3304
|
timestamp: state.timestamp,
|
|
3188
3305
|
cwd: state.cwd,
|
|
@@ -3209,6 +3326,7 @@ export function serializeCodexParserState(state, offset) {
|
|
|
3209
3326
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3210
3327
|
checklistEvents: state.checklistEvents,
|
|
3211
3328
|
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3329
|
+
toolCalls: state.toolCollector.snapshot(),
|
|
3212
3330
|
};
|
|
3213
3331
|
}
|
|
3214
3332
|
/**
|
|
@@ -3244,6 +3362,12 @@ export function hydrateCodexParseState(prior) {
|
|
|
3244
3362
|
spawnedTeam: prior.spawnedTeam,
|
|
3245
3363
|
checklistEvents: prior.checklistEvents ?? [],
|
|
3246
3364
|
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
3365
|
+
toolCollector: new ToolCallCollector(prior.toolCalls),
|
|
3366
|
+
// Not persisted in CodexParserState (always empty for Codex today — see
|
|
3367
|
+
// CodexParseState.skillEvents) — a resume starts fresh rather than
|
|
3368
|
+
// round-tripping an always-empty array through the continuation blob.
|
|
3369
|
+
skillEvents: [],
|
|
3370
|
+
slashCommandEvents: [],
|
|
3247
3371
|
};
|
|
3248
3372
|
}
|
|
3249
3373
|
/**
|
|
@@ -3253,47 +3377,25 @@ export function hydrateCodexParseState(prior) {
|
|
|
3253
3377
|
*
|
|
3254
3378
|
* Same trailing-line discipline as {@link scanClaudeSessionIncremental}: apply
|
|
3255
3379
|
* ONLY the run of newline-terminated lines (slice at the last `'\n'`), and set
|
|
3256
|
-
* `newOffset = fromOffset + consumedBytes`.
|
|
3257
|
-
*
|
|
3258
|
-
*
|
|
3380
|
+
* `newOffset = fromOffset + consumedBytes`. An ordinary tail after the last
|
|
3381
|
+
* `'\n'` is deferred; an oversized one advances under a persisted discard bit.
|
|
3382
|
+
* Codex `messageCount` is additive with
|
|
3259
3383
|
* NO dedup, so re-reading a still-unterminated complete line would double-count
|
|
3260
3384
|
* it; deferring prevents that (the bug class prix-cloud caught for Claude).
|
|
3261
3385
|
*/
|
|
3262
3386
|
export async function scanCodexSessionIncremental(filePath, fromOffset, prior) {
|
|
3263
3387
|
const state = hydrateCodexParseState(prior);
|
|
3264
|
-
const
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
}
|
|
3271
|
-
finally {
|
|
3272
|
-
stream.destroy();
|
|
3273
|
-
}
|
|
3274
|
-
const appended = Buffer.concat(chunks);
|
|
3275
|
-
// Bytes up to AND INCLUDING the last '\n' are the committed, complete-line run.
|
|
3276
|
-
const lastNl = appended.lastIndexOf(0x0a);
|
|
3277
|
-
const consumedBytes = lastNl === -1 ? 0 : lastNl + 1;
|
|
3278
|
-
if (consumedBytes > 0) {
|
|
3279
|
-
for (const line of appended.subarray(0, consumedBytes).toString('utf-8').split('\n')) {
|
|
3280
|
-
if (!line.trim())
|
|
3281
|
-
continue;
|
|
3282
|
-
let parsed;
|
|
3283
|
-
try {
|
|
3284
|
-
parsed = JSON.parse(line);
|
|
3285
|
-
}
|
|
3286
|
-
catch {
|
|
3287
|
-
continue;
|
|
3288
|
-
}
|
|
3289
|
-
applyCodexLine(state, parsed);
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
const newOffset = fromOffset + consumedBytes;
|
|
3388
|
+
const append = await applyJsonlAppend(filePath, fromOffset, prior.jsonlDroppingOversizedLine === true, (parsed) => applyCodexLine(state, parsed));
|
|
3389
|
+
if (append.skippedOversizedLine)
|
|
3390
|
+
state.toolCollector.recordIndexLimit();
|
|
3391
|
+
const newOffset = fromOffset + append.consumedBytes;
|
|
3392
|
+
const scan = finalizeCodexScan(state);
|
|
3393
|
+
const toolCalls = state.toolCollector.drainChanged();
|
|
3293
3394
|
return {
|
|
3294
|
-
scan
|
|
3295
|
-
newState: serializeCodexParserState(state, newOffset),
|
|
3395
|
+
scan,
|
|
3396
|
+
newState: serializeCodexParserState(state, newOffset, append.droppingOversizedLine),
|
|
3296
3397
|
newOffset,
|
|
3398
|
+
toolCalls,
|
|
3297
3399
|
};
|
|
3298
3400
|
}
|
|
3299
3401
|
/** Serialized zero-value continuation: a fresh accumulator at offset 0, used to drive a FULL parse from the start through the same resumable path. */
|
|
@@ -3382,7 +3484,7 @@ function parsePriorCodexState(row) {
|
|
|
3382
3484
|
return null;
|
|
3383
3485
|
try {
|
|
3384
3486
|
const parsed = JSON.parse(row.parserState);
|
|
3385
|
-
if (parsed?.v !==
|
|
3487
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3386
3488
|
return null;
|
|
3387
3489
|
return parsed;
|
|
3388
3490
|
}
|
|
@@ -17,12 +17,32 @@ export interface SkillUse {
|
|
|
17
17
|
count: number;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* True for a tool_use event that is that harness's skill-invocation call (see
|
|
21
|
+
* {@link SKILL_TOOL_NAME_BY_AGENT}). Exported so an INCREMENTAL parser
|
|
22
|
+
* (discover.ts's ClaudeParseState/foldDerivedToolState) can select matching
|
|
23
|
+
* events into a small held array as it streams, then run {@link extractSkills}
|
|
24
|
+
* over just that subset at finalize — without re-parsing the whole transcript.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isSkillInvocation(e: SessionEvent): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Skills invoked during the session, from that harness's skill-invocation
|
|
29
|
+
* tool calls (see {@link SKILL_TOOL_NAME_BY_AGENT}). Carries the skill id in
|
|
30
|
+
* `args.skill` (or `args.name`). Sorted by count desc, then name.
|
|
24
31
|
*/
|
|
25
32
|
export declare function extractSkills(events: SessionEvent[]): SkillUse[];
|
|
33
|
+
export interface SlashCommandUse {
|
|
34
|
+
/** WITH the leading slash, e.g. `/recap`, `/code:commit` — matches SessionEvent.slashCommand's shape. */
|
|
35
|
+
name: string;
|
|
36
|
+
count: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Slash commands invoked during the session — either the user typing one
|
|
40
|
+
* (Claude's `<command-name>` wrapper) or the model invoking one via the
|
|
41
|
+
* `SlashCommand` tool (`SessionEvent.slashCommand`, populated by
|
|
42
|
+
* `parseClaudeContent` for both sources — see session/prompt.ts). Sorted by
|
|
43
|
+
* count desc, then name.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractSlashCommands(events: SessionEvent[]): SlashCommandUse[];
|
|
26
46
|
export interface HookUse {
|
|
27
47
|
/** Hook name as configured, e.g. `SessionStart:startup`. */
|
|
28
48
|
name: string;
|
|
@@ -15,17 +15,44 @@ import os from 'node:os';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { isNoisePath } from './digest.js';
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* The tool name a harness uses to invoke a skill, keyed by `SessionAgentId` —
|
|
19
|
+
* a registry, not a single hardcoded string, so a harness whose transcript
|
|
20
|
+
* names the tool differently is one table row away from support instead of a
|
|
21
|
+
* near-identical `if` arm (see CLAUDE.md's registry-over-if-chain convention).
|
|
22
|
+
*
|
|
23
|
+
* Verified entries only: Claude and Kimi both empirically name the tool
|
|
24
|
+
* `Skill` (a plugin-provided skill rides the same tool). No other harness has
|
|
25
|
+
* a confirmed skill-invocation tool name in this codebase's fixtures — an
|
|
26
|
+
* absent entry means "we don't know yet", not "this harness has no skills";
|
|
27
|
+
* extractSkills yields `[]` for it rather than guessing a name and silently
|
|
28
|
+
* mismatching (or worse, matching a coincidental tool with the same name).
|
|
29
|
+
*/
|
|
30
|
+
const SKILL_TOOL_NAME_BY_AGENT = {
|
|
31
|
+
claude: 'Skill',
|
|
32
|
+
kimi: 'Skill',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* True for a tool_use event that is that harness's skill-invocation call (see
|
|
36
|
+
* {@link SKILL_TOOL_NAME_BY_AGENT}). Exported so an INCREMENTAL parser
|
|
37
|
+
* (discover.ts's ClaudeParseState/foldDerivedToolState) can select matching
|
|
38
|
+
* events into a small held array as it streams, then run {@link extractSkills}
|
|
39
|
+
* over just that subset at finalize — without re-parsing the whole transcript.
|
|
40
|
+
*/
|
|
41
|
+
export function isSkillInvocation(e) {
|
|
42
|
+
if (e.type !== 'tool_use' || e._local)
|
|
43
|
+
return false;
|
|
44
|
+
const skillTool = SKILL_TOOL_NAME_BY_AGENT[e.agent];
|
|
45
|
+
return !!skillTool && e.tool === skillTool;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Skills invoked during the session, from that harness's skill-invocation
|
|
49
|
+
* tool calls (see {@link SKILL_TOOL_NAME_BY_AGENT}). Carries the skill id in
|
|
50
|
+
* `args.skill` (or `args.name`). Sorted by count desc, then name.
|
|
22
51
|
*/
|
|
23
52
|
export function extractSkills(events) {
|
|
24
53
|
const counts = new Map();
|
|
25
54
|
for (const e of events) {
|
|
26
|
-
if (e
|
|
27
|
-
continue;
|
|
28
|
-
if (e.tool !== 'Skill')
|
|
55
|
+
if (!isSkillInvocation(e))
|
|
29
56
|
continue;
|
|
30
57
|
const name = e.args?.skill ?? e.args?.name;
|
|
31
58
|
if (typeof name !== 'string' || !name.trim())
|
|
@@ -37,6 +64,24 @@ export function extractSkills(events) {
|
|
|
37
64
|
.map(([name, count]) => ({ name, count }))
|
|
38
65
|
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
39
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Slash commands invoked during the session — either the user typing one
|
|
69
|
+
* (Claude's `<command-name>` wrapper) or the model invoking one via the
|
|
70
|
+
* `SlashCommand` tool (`SessionEvent.slashCommand`, populated by
|
|
71
|
+
* `parseClaudeContent` for both sources — see session/prompt.ts). Sorted by
|
|
72
|
+
* count desc, then name.
|
|
73
|
+
*/
|
|
74
|
+
export function extractSlashCommands(events) {
|
|
75
|
+
const counts = new Map();
|
|
76
|
+
for (const e of events) {
|
|
77
|
+
if (!e.slashCommand)
|
|
78
|
+
continue;
|
|
79
|
+
counts.set(e.slashCommand, (counts.get(e.slashCommand) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
return [...counts.entries()]
|
|
82
|
+
.map(([name, count]) => ({ name, count }))
|
|
83
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
40
85
|
/**
|
|
41
86
|
* Hooks that fired during the session, folded from `hook` events (parsed from
|
|
42
87
|
* Claude's `hook_success`/`hook_error`/… attachment records; other harnesses
|
|
@@ -12,7 +12,14 @@ import type { SessionAgentId, SessionEvent } from './types.js';
|
|
|
12
12
|
* Node's ~512MB string ceiling with a healthy margin.
|
|
13
13
|
*/
|
|
14
14
|
export declare const SESSION_FILE_MAX_BYTES = 200000000;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Strip terminal control sequences that a malicious session file could use to
|
|
17
|
+
* hijack the user's terminal (clipboard via OSC 52, scrollback wipe, alt-screen
|
|
18
|
+
* takeover, cursor moves, etc.). Allowed through: tab (0x09), newline (0x0a),
|
|
19
|
+
* carriage return (0x0d). Everything else in the C0/C1 range and every CSI/OSC
|
|
20
|
+
* escape is dropped.
|
|
21
|
+
*/
|
|
22
|
+
export { sanitizeForTerminal } from '../redact.js';
|
|
16
23
|
/** In-place sanitize every user-visible string field on a list of events. */
|
|
17
24
|
export declare function sanitizeEvents(events: SessionEvent[]): void;
|
|
18
25
|
/**
|