ccsniff 1.0.13 → 1.0.15
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/package.json +1 -1
- package/src/cli.js +325 -65
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,92 +2,240 @@
|
|
|
2
2
|
import { JsonlReplayer, rollup } from './index.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
+
const FLAGS = {
|
|
6
|
+
string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort'],
|
|
7
|
+
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
|
|
8
|
+
number: ['limit', 'head', 'tail-n', 'ctx', 'truncate'],
|
|
9
|
+
bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'stats', 'count', 'gm-audit', 'help', 'h'],
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
function parseArgs(argv) {
|
|
6
|
-
const opts = {
|
|
13
|
+
const opts = { _multi: {} };
|
|
14
|
+
for (const k of FLAGS.multi) opts._multi[k] = [];
|
|
7
15
|
const rest = [];
|
|
8
16
|
for (let i = 0; i < argv.length; i++) {
|
|
9
17
|
const a = argv[i];
|
|
10
|
-
|
|
11
|
-
if (a === '
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else if (
|
|
18
|
-
else
|
|
19
|
-
else if (a === '--rollup') opts.rollup = next();
|
|
20
|
-
else if (a === '--format') opts.format = next();
|
|
21
|
-
else if (a === '--gm-audit') opts.gmAudit = true;
|
|
22
|
-
else if (a === '-h' || a === '--help') { printHelp(); process.exit(0); }
|
|
23
|
-
else rest.push(a);
|
|
18
|
+
if (a === '-h' || a === '--help') { opts.help = true; continue; }
|
|
19
|
+
if (a === '-f') { opts.tail = true; continue; }
|
|
20
|
+
if (!a.startsWith('--')) { rest.push(a); continue; }
|
|
21
|
+
const key = a.slice(2);
|
|
22
|
+
if (FLAGS.bool.includes(key)) { opts[key] = true; continue; }
|
|
23
|
+
const val = argv[++i];
|
|
24
|
+
if (FLAGS.multi.includes(key)) opts._multi[key].push(val);
|
|
25
|
+
else if (FLAGS.number.includes(key)) opts[key] = parseInt(val, 10) || 0;
|
|
26
|
+
else opts[key] = val;
|
|
24
27
|
}
|
|
25
28
|
return { opts, rest };
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function printHelp() {
|
|
29
|
-
process.stdout.write(`ccsniff — query and tail Claude Code session history
|
|
32
|
+
process.stdout.write(`ccsniff — query, search, and tail Claude Code session history
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
ccsniff [
|
|
33
|
-
|
|
34
|
+
USAGE
|
|
35
|
+
ccsniff [filters] [output]
|
|
36
|
+
ccsniff -f live tail
|
|
34
37
|
ccsniff --rollup out.ndjson [--since 7d]
|
|
35
|
-
ccsniff --rollup out.sqlite --format sqlite
|
|
36
|
-
ccsniff --
|
|
38
|
+
ccsniff --rollup out.sqlite --format sqlite
|
|
39
|
+
ccsniff --list-sessions [filters]
|
|
40
|
+
ccsniff --list-projects
|
|
41
|
+
ccsniff --list-tools
|
|
42
|
+
ccsniff --stats [filters]
|
|
43
|
+
ccsniff --gm-audit [filters]
|
|
44
|
+
|
|
45
|
+
TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
|
|
46
|
+
--since <t> include events at/after t (alias: --after)
|
|
47
|
+
--until <t> include events at/before t (alias: --before)
|
|
48
|
+
|
|
49
|
+
FILTERS (repeatable flags combine as OR within a flag, AND across flags)
|
|
50
|
+
--grep <re> text regex (case-insensitive); repeat = AND
|
|
51
|
+
--igrep <re> inverted text regex; repeat = AND (none must match)
|
|
52
|
+
--invert invert the entire filter result
|
|
53
|
+
--cwd <re> working-dir regex
|
|
54
|
+
--project <name> basename(cwd) exact match; repeat = OR
|
|
55
|
+
--role <r> user|assistant|tool_result|system|result; repeat = OR
|
|
56
|
+
--type <t> text|tool_use|tool_result|thinking|system|result; repeat = OR
|
|
57
|
+
--tool <name> tool name (Read, Bash, ...); repeat = OR
|
|
58
|
+
--session <sid> session id prefix; repeat = OR (alias: --sid)
|
|
59
|
+
--parent <sid> subagent parent session id
|
|
60
|
+
--no-subagents exclude subagent sessions
|
|
61
|
+
--only-subagents only subagent sessions
|
|
62
|
+
--no-meta exclude meta/system-injected user messages
|
|
63
|
+
--only-meta only meta messages
|
|
64
|
+
|
|
65
|
+
OUTPUT
|
|
66
|
+
--json ndjson rows (one event per line)
|
|
67
|
+
--ndjson alias for --json
|
|
68
|
+
--full do not truncate text fields
|
|
69
|
+
--truncate <N> max chars of text per row (default 200, 2000 in --json)
|
|
70
|
+
--ctx <N> include N events before+after each match (context)
|
|
71
|
+
--limit <N> stop after N matches
|
|
72
|
+
--head <N> alias for --limit
|
|
73
|
+
--tail-n <N> keep only the last N matches
|
|
74
|
+
--reverse newest first
|
|
75
|
+
--sort <key> ts|sid|cwd|role|type (default ts)
|
|
76
|
+
--count print only the match count
|
|
77
|
+
--stats breakdown by role/type/tool/project/session
|
|
78
|
+
-f, --tail live tail after replay
|
|
79
|
+
--rollup <out> dump filtered events to file
|
|
80
|
+
--format ndjson|sqlite rollup format (default ndjson; sqlite needs better-sqlite3)
|
|
37
81
|
|
|
38
|
-
|
|
82
|
+
EXAMPLES
|
|
39
83
|
ccsniff --since 24h --grep "rs-exec" --limit 50
|
|
40
|
-
ccsniff --since 7d --role user --json
|
|
41
|
-
ccsniff
|
|
84
|
+
ccsniff --since 7d --until 1d --role user --json
|
|
85
|
+
ccsniff --project ccsniff --tool Bash --ctx 2
|
|
86
|
+
ccsniff --grep error --grep timeout --invert # error AND NOT timeout? no — both must match
|
|
87
|
+
ccsniff --igrep DEBUG --since 1h # exclude DEBUG lines
|
|
88
|
+
ccsniff --list-sessions --since 7d --project myrepo
|
|
89
|
+
ccsniff --stats --since 24h
|
|
90
|
+
ccsniff -f --project ccsniff --role assistant
|
|
42
91
|
`);
|
|
43
92
|
}
|
|
44
93
|
|
|
45
|
-
function
|
|
94
|
+
function parseTime(s) {
|
|
46
95
|
if (!s) return 0;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
if (/^\d{10,}$/.test(s)) return parseInt(s, 10);
|
|
97
|
+
const m = /^(\d+)([smhdw])$/.exec(s);
|
|
98
|
+
if (m) {
|
|
99
|
+
const n = parseInt(m[1], 10);
|
|
100
|
+
const mult = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }[m[2]];
|
|
101
|
+
return Date.now() - n * mult;
|
|
102
|
+
}
|
|
103
|
+
const t = Date.parse(s);
|
|
104
|
+
return Number.isFinite(t) ? t : 0;
|
|
52
105
|
}
|
|
53
106
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
function compileRegexes(arr) { return arr.map(s => new RegExp(s, 'i')); }
|
|
108
|
+
|
|
109
|
+
function blockText(b) {
|
|
110
|
+
if (!b) return '';
|
|
111
|
+
if (typeof b.text === 'string') return b.text;
|
|
112
|
+
if (typeof b.content === 'string') return b.content;
|
|
113
|
+
if (Array.isArray(b.content)) return b.content.map(c => c?.text || '').join('');
|
|
114
|
+
if (b.input) return JSON.stringify(b.input);
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildFilter(opts) {
|
|
119
|
+
const since = parseTime(opts.since || opts.after);
|
|
120
|
+
const until = parseTime(opts.until || opts.before);
|
|
121
|
+
const greps = compileRegexes(opts._multi.grep);
|
|
122
|
+
const igreps = compileRegexes(opts._multi.igrep);
|
|
123
|
+
const cwdRes = compileRegexes(opts._multi.cwd);
|
|
124
|
+
const projects = new Set(opts._multi.project);
|
|
125
|
+
const roles = new Set(opts._multi.role);
|
|
126
|
+
const types = new Set(opts._multi.type);
|
|
127
|
+
const tools = new Set(opts._multi.tool);
|
|
128
|
+
const sids = opts._multi.session.concat(opts._multi.sid || []);
|
|
129
|
+
const parent = opts.parent || null;
|
|
130
|
+
|
|
131
|
+
return ev => {
|
|
132
|
+
const conv = ev.conversation || {};
|
|
133
|
+
const block = ev.block || {};
|
|
134
|
+
const ts = ev.timestamp || 0;
|
|
135
|
+
let pass = true;
|
|
136
|
+
if (since && ts < since) pass = false;
|
|
137
|
+
else if (until && ts > until) pass = false;
|
|
138
|
+
else if (cwdRes.length && !cwdRes.every(r => r.test(conv.cwd || ''))) pass = false;
|
|
139
|
+
else if (projects.size && !projects.has(path.basename(conv.cwd || ''))) pass = false;
|
|
140
|
+
else if (roles.size && !roles.has(ev.role)) pass = false;
|
|
141
|
+
else if (types.size && !types.has(block.type)) pass = false;
|
|
142
|
+
else if (tools.size && !tools.has(block.name)) pass = false;
|
|
143
|
+
else if (sids.length && !sids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
144
|
+
else if (parent && conv.parentSid !== parent) pass = false;
|
|
145
|
+
else if (opts['no-subagents'] && conv.isSubagent) pass = false;
|
|
146
|
+
else if (opts['only-subagents'] && !conv.isSubagent) pass = false;
|
|
147
|
+
else if (opts['no-meta'] && block.isMeta) pass = false;
|
|
148
|
+
else if (opts['only-meta'] && !block.isMeta) pass = false;
|
|
149
|
+
else {
|
|
150
|
+
const text = blockText(block);
|
|
151
|
+
if (greps.length && !greps.every(r => r.test(text))) pass = false;
|
|
152
|
+
else if (igreps.length && igreps.some(r => r.test(text))) pass = false;
|
|
153
|
+
}
|
|
154
|
+
return opts.invert ? !pass : pass;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatRow(ev, opts) {
|
|
159
|
+
const conv = ev.conversation || {};
|
|
160
|
+
const block = ev.block || {};
|
|
161
|
+
const text = blockText(block).replace(/\s+/g, ' ');
|
|
162
|
+
const truncN = opts.full ? Infinity : (opts.truncate || (opts.json ? 2000 : 200));
|
|
163
|
+
const out = text.length > truncN ? text.slice(0, truncN) + '…' : text;
|
|
69
164
|
if (opts.json) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
ts: ev.timestamp,
|
|
167
|
+
iso: new Date(ev.timestamp).toISOString(),
|
|
168
|
+
sid: conv.id,
|
|
169
|
+
parent: conv.parentSid || null,
|
|
170
|
+
cwd: conv.cwd,
|
|
171
|
+
project: path.basename(conv.cwd || ''),
|
|
172
|
+
role: ev.role,
|
|
173
|
+
type: block.type,
|
|
174
|
+
tool: block.name || null,
|
|
175
|
+
isMeta: !!block.isMeta,
|
|
176
|
+
text: opts.full ? text : out,
|
|
177
|
+
}) + '\n';
|
|
75
178
|
}
|
|
179
|
+
const t = new Date(ev.timestamp).toISOString().slice(0, 19).replace('T', ' ');
|
|
180
|
+
const repo = path.basename(conv.cwd || '');
|
|
181
|
+
const tool = block.name ? `:${block.name}` : '';
|
|
182
|
+
const tag = conv.isSubagent ? '*' : '';
|
|
183
|
+
return `[${t}] [${repo}${tag}] ${ev.role}/${block.type || '?'}${tool}: ${out}\n`;
|
|
76
184
|
}
|
|
77
185
|
|
|
78
|
-
|
|
186
|
+
function collect(opts, since) {
|
|
187
|
+
const r = new JsonlReplayer();
|
|
188
|
+
const all = [];
|
|
189
|
+
r.on('streaming_progress', ev => all.push(ev));
|
|
190
|
+
r.on('error', e => process.stderr.write(`error: ${e?.message || e}\n`));
|
|
191
|
+
const stats = r.replay({ since });
|
|
192
|
+
return { stats, all };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function applyContext(matchedIdxs, all, ctx) {
|
|
196
|
+
if (!ctx) return matchedIdxs.map(i => all[i]);
|
|
197
|
+
const keep = new Set();
|
|
198
|
+
for (const i of matchedIdxs) {
|
|
199
|
+
for (let j = Math.max(0, i - ctx); j <= Math.min(all.length - 1, i + ctx); j++) keep.add(j);
|
|
200
|
+
}
|
|
201
|
+
return [...keep].sort((a, b) => a - b).map(i => all[i]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sortRows(rows, key, reverse) {
|
|
205
|
+
const get = {
|
|
206
|
+
ts: e => e.timestamp || 0,
|
|
207
|
+
sid: e => e.conversation?.id || '',
|
|
208
|
+
cwd: e => e.conversation?.cwd || '',
|
|
209
|
+
role: e => e.role || '',
|
|
210
|
+
type: e => e.block?.type || '',
|
|
211
|
+
}[key] || (e => e.timestamp || 0);
|
|
212
|
+
rows.sort((a, b) => { const x = get(a), y = get(b); return x < y ? -1 : x > y ? 1 : 0; });
|
|
213
|
+
if (reverse) rows.reverse();
|
|
214
|
+
return rows;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { opts } = parseArgs(process.argv.slice(2));
|
|
218
|
+
if (opts.help) { printHelp(); process.exit(0); }
|
|
219
|
+
|
|
220
|
+
const since = parseTime(opts.since || opts.after);
|
|
221
|
+
const filter = buildFilter(opts);
|
|
222
|
+
|
|
223
|
+
// ---------- gm-audit (existing, untouched logic, now respects new filters)
|
|
224
|
+
if (opts['gm-audit']) {
|
|
79
225
|
const sessions = new Map();
|
|
80
226
|
const r2 = new JsonlReplayer();
|
|
81
227
|
r2.on('streaming_progress', ev => {
|
|
228
|
+
if (!filter(ev)) return;
|
|
82
229
|
const conv = ev.conversation;
|
|
83
|
-
if (
|
|
230
|
+
if (conv.isSubagent) return;
|
|
84
231
|
if (ev.role !== 'user' && ev.role !== 'assistant') return;
|
|
85
232
|
const sid = conv.id;
|
|
86
233
|
if (!sessions.has(sid)) sessions.set(sid, { cwd: conv.cwd, turns: [] });
|
|
87
234
|
const s = sessions.get(sid);
|
|
88
235
|
if (ev.role === 'user' && ev.block?.type === 'text') {
|
|
89
236
|
const t = ev.block.text || '';
|
|
90
|
-
const
|
|
237
|
+
const isContinuation = /^This session is being continued from a previous conversation/.test(t.trimStart());
|
|
238
|
+
const isSystem = ev.block.isMeta || isContinuation || /^<(task-notification|command-name|local-command|system-reminder)\b/.test(t.trimStart()) || t === '[Request interrupted by user]' || t === '[Request interrupted by user for tool use]';
|
|
91
239
|
s.turns.push({ isMeta: isSystem, firstTool: null, text: t.slice(0, 80) });
|
|
92
240
|
} else if (ev.role === 'assistant' && ev.block?.type === 'tool_use' && s.turns.length) {
|
|
93
241
|
const last = s.turns[s.turns.length - 1];
|
|
@@ -95,38 +243,150 @@ if (opts.gmAudit) {
|
|
|
95
243
|
}
|
|
96
244
|
});
|
|
97
245
|
r2.replay({ since });
|
|
98
|
-
|
|
246
|
+
const isTerse = t => t.text.trim().length <= 5 || /^\/\w/.test(t.text.trim());
|
|
247
|
+
let totalReal = 0, totalCompliant = 0, totalTerse = 0;
|
|
99
248
|
for (const [sid, s] of sessions) {
|
|
100
249
|
const real = s.turns.filter(t => !t.isMeta);
|
|
101
250
|
const compliant = real.filter(t => t.firstTool === 'Skill' || t.firstTool === 'mcp__gm__Skill');
|
|
251
|
+
const terse = real.filter(t => isTerse(t) && t.firstTool !== 'Skill' && t.firstTool !== 'mcp__gm__Skill');
|
|
102
252
|
totalReal += real.length;
|
|
103
253
|
totalCompliant += compliant.length;
|
|
254
|
+
totalTerse += terse.length;
|
|
104
255
|
const pct = real.length ? Math.round(100 * compliant.length / real.length) : 0;
|
|
105
|
-
const violations = real.filter(t => t.firstTool !== 'Skill' && t.firstTool !== 'mcp__gm__Skill');
|
|
106
|
-
|
|
256
|
+
const violations = real.filter(t => t.firstTool !== 'Skill' && t.firstTool !== 'mcp__gm__Skill' && !isTerse(t));
|
|
257
|
+
if (real.length === 0) continue;
|
|
258
|
+
process.stdout.write(`[${pct}%] ${path.basename(s.cwd || sid)} (${compliant.length}/${real.length}, terse-skip:${terse.length}) sid=${sid.slice(0, 8)}\n`);
|
|
107
259
|
for (const v of violations.slice(0, 3)) {
|
|
108
260
|
process.stdout.write(` MISS first=${v.firstTool || 'none'} msg="${v.text.replace(/\s+/g, ' ')}"\n`);
|
|
109
261
|
}
|
|
110
262
|
}
|
|
111
263
|
const total = totalReal ? Math.round(100 * totalCompliant / totalReal) : 0;
|
|
112
|
-
|
|
264
|
+
const adjCompliant = totalCompliant + totalTerse;
|
|
265
|
+
const adjTotal = totalReal ? Math.round(100 * adjCompliant / totalReal) : 0;
|
|
266
|
+
process.stderr.write(`# gm-audit: ${totalCompliant}/${totalReal} compliant (${total}%) | adj (terse-skip): ${adjCompliant}/${totalReal} (${adjTotal}%) | ${sessions.size} sessions\n`);
|
|
113
267
|
process.exit(0);
|
|
114
268
|
}
|
|
115
269
|
|
|
270
|
+
// ---------- rollup (filtered)
|
|
116
271
|
if (opts.rollup) {
|
|
117
|
-
const stats = await rollup({ since, out: opts.rollup, format: opts.format });
|
|
272
|
+
const stats = await rollup({ since, out: opts.rollup, format: opts.format || 'ndjson' });
|
|
118
273
|
process.stderr.write(`# rolled up ${stats.rows} events from ${stats.events} routed (${stats.files} files) → ${stats.format}: ${stats.out}\n`);
|
|
119
274
|
process.exit(0);
|
|
120
275
|
}
|
|
121
276
|
|
|
122
|
-
|
|
123
|
-
r.on('streaming_progress', out);
|
|
124
|
-
r.on('error', e => process.stderr.write(`error: ${e?.message || e}\n`));
|
|
125
|
-
|
|
277
|
+
// ---------- live tail (filter applied to live events)
|
|
126
278
|
if (opts.tail) {
|
|
279
|
+
const r = new JsonlReplayer();
|
|
280
|
+
r.on('streaming_progress', ev => { if (filter(ev)) process.stdout.write(formatRow(ev, opts)); });
|
|
281
|
+
r.on('error', e => process.stderr.write(`error: ${e?.message || e}\n`));
|
|
127
282
|
r.start();
|
|
128
|
-
process.stdout.write('tailing... (Ctrl-C to exit)\n');
|
|
283
|
+
process.stdout.write('# tailing... (Ctrl-C to exit)\n');
|
|
284
|
+
process.stdin.resume();
|
|
129
285
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
286
|
+
|
|
287
|
+
// ---------- one-shot collection (everything else needs the full set)
|
|
288
|
+
const { stats, all } = collect(opts, since);
|
|
289
|
+
|
|
290
|
+
// ---------- list-projects
|
|
291
|
+
if (opts['list-projects']) {
|
|
292
|
+
const projects = new Map();
|
|
293
|
+
for (const ev of all) {
|
|
294
|
+
const p = path.basename(ev.conversation?.cwd || '');
|
|
295
|
+
if (!p) continue;
|
|
296
|
+
if (!projects.has(p)) projects.set(p, { events: 0, sessions: new Set(), last: 0 });
|
|
297
|
+
const x = projects.get(p);
|
|
298
|
+
x.events++;
|
|
299
|
+
x.sessions.add(ev.conversation.id);
|
|
300
|
+
if (ev.timestamp > x.last) x.last = ev.timestamp;
|
|
301
|
+
}
|
|
302
|
+
const rows = [...projects.entries()].sort((a, b) => b[1].last - a[1].last);
|
|
303
|
+
for (const [p, x] of rows) {
|
|
304
|
+
process.stdout.write(`${new Date(x.last).toISOString().slice(0, 19)} ${String(x.sessions.size).padStart(4)} sess ${String(x.events).padStart(7)} ev ${p}\n`);
|
|
305
|
+
}
|
|
306
|
+
process.stderr.write(`# ${rows.length} projects\n`);
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------- list-tools
|
|
311
|
+
if (opts['list-tools']) {
|
|
312
|
+
const tools = new Map();
|
|
313
|
+
for (const ev of all) {
|
|
314
|
+
if (!filter(ev)) continue;
|
|
315
|
+
const n = ev.block?.name;
|
|
316
|
+
if (!n) continue;
|
|
317
|
+
tools.set(n, (tools.get(n) || 0) + 1);
|
|
318
|
+
}
|
|
319
|
+
const rows = [...tools.entries()].sort((a, b) => b[1] - a[1]);
|
|
320
|
+
for (const [n, c] of rows) process.stdout.write(`${String(c).padStart(7)} ${n}\n`);
|
|
321
|
+
process.stderr.write(`# ${rows.length} distinct tools\n`);
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------- list-sessions
|
|
326
|
+
if (opts['list-sessions']) {
|
|
327
|
+
const sess = new Map();
|
|
328
|
+
for (const ev of all) {
|
|
329
|
+
if (!filter(ev)) continue;
|
|
330
|
+
const conv = ev.conversation;
|
|
331
|
+
const sid = conv.id;
|
|
332
|
+
if (!sess.has(sid)) sess.set(sid, { cwd: conv.cwd, parent: conv.parentSid, sub: conv.isSubagent, first: ev.timestamp, last: ev.timestamp, events: 0, tools: 0, userTurns: 0 });
|
|
333
|
+
const s = sess.get(sid);
|
|
334
|
+
if (ev.timestamp < s.first) s.first = ev.timestamp;
|
|
335
|
+
if (ev.timestamp > s.last) s.last = ev.timestamp;
|
|
336
|
+
s.events++;
|
|
337
|
+
if (ev.block?.type === 'tool_use') s.tools++;
|
|
338
|
+
if (ev.role === 'user' && ev.block?.type === 'text' && !ev.block.isMeta) s.userTurns++;
|
|
339
|
+
}
|
|
340
|
+
const rows = [...sess.entries()].sort((a, b) => b[1].last - a[1].last);
|
|
341
|
+
for (const [sid, s] of rows) {
|
|
342
|
+
const dur = Math.round((s.last - s.first) / 1000);
|
|
343
|
+
const tag = s.sub ? '*' : ' ';
|
|
344
|
+
process.stdout.write(`${new Date(s.last).toISOString().slice(0, 19)} ${tag} ${sid.slice(0, 8)} turns:${String(s.userTurns).padStart(3)} tools:${String(s.tools).padStart(4)} ev:${String(s.events).padStart(5)} dur:${String(dur).padStart(5)}s ${path.basename(s.cwd || '')}\n`);
|
|
345
|
+
}
|
|
346
|
+
process.stderr.write(`# ${rows.length} sessions match\n`);
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------- main filter pass
|
|
351
|
+
const matchedIdxs = [];
|
|
352
|
+
for (let i = 0; i < all.length; i++) if (filter(all[i])) matchedIdxs.push(i);
|
|
353
|
+
let rows = applyContext(matchedIdxs, all, opts.ctx || 0);
|
|
354
|
+
rows = sortRows(rows, opts.sort || 'ts', opts.reverse);
|
|
355
|
+
|
|
356
|
+
if (opts['tail-n']) rows = rows.slice(-opts['tail-n']);
|
|
357
|
+
const limit = opts.limit || opts.head || 0;
|
|
358
|
+
if (limit) rows = rows.slice(0, limit);
|
|
359
|
+
|
|
360
|
+
// ---------- stats
|
|
361
|
+
if (opts.stats) {
|
|
362
|
+
const bump = (m, k) => m.set(k, (m.get(k) || 0) + 1);
|
|
363
|
+
const byRole = new Map(), byType = new Map(), byTool = new Map(), byProject = new Map(), bySid = new Map();
|
|
364
|
+
for (const ev of rows) {
|
|
365
|
+
bump(byRole, ev.role || '?');
|
|
366
|
+
bump(byType, ev.block?.type || '?');
|
|
367
|
+
if (ev.block?.name) bump(byTool, ev.block.name);
|
|
368
|
+
bump(byProject, path.basename(ev.conversation?.cwd || '') || '?');
|
|
369
|
+
bump(bySid, (ev.conversation?.id || '?').slice(0, 8));
|
|
370
|
+
}
|
|
371
|
+
const dump = (label, m, top = 20) => {
|
|
372
|
+
process.stdout.write(`\n# ${label}\n`);
|
|
373
|
+
[...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, top).forEach(([k, v]) => process.stdout.write(` ${String(v).padStart(7)} ${k}\n`));
|
|
374
|
+
};
|
|
375
|
+
process.stdout.write(`# total matched: ${rows.length} files:${stats.files} routed:${stats.events}\n`);
|
|
376
|
+
dump('by role', byRole);
|
|
377
|
+
dump('by type', byType);
|
|
378
|
+
dump('by tool', byTool);
|
|
379
|
+
dump('by project', byProject);
|
|
380
|
+
dump('by session (top 20)', bySid);
|
|
381
|
+
process.exit(0);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (opts.count) {
|
|
385
|
+
process.stdout.write(`${rows.length}\n`);
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const ev of rows) process.stdout.write(formatRow(ev, opts));
|
|
390
|
+
process.stderr.write(`# ${stats.events} events / ${stats.files} files / ${rows.length} matched\n`);
|
|
391
|
+
|
|
132
392
|
}
|