ccsniff 1.0.14 → 1.0.16
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 +313 -62
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,86 +2,232 @@
|
|
|
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] dump matching events (requires ≥1 flag)
|
|
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 || process.argv.length <= 2) { 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 (
|
|
84
|
-
if (conv.isSubagent) return; // memorize/subagents don't need gm invocation
|
|
230
|
+
if (conv.isSubagent) return;
|
|
85
231
|
if (ev.role !== 'user' && ev.role !== 'assistant') return;
|
|
86
232
|
const sid = conv.id;
|
|
87
233
|
if (!sessions.has(sid)) sessions.set(sid, { cwd: conv.cwd, turns: [] });
|
|
@@ -97,7 +243,6 @@ if (opts.gmAudit) {
|
|
|
97
243
|
}
|
|
98
244
|
});
|
|
99
245
|
r2.replay({ since });
|
|
100
|
-
// terse: single-word / slash-command / short follow-up — less critical to enforce gm
|
|
101
246
|
const isTerse = t => t.text.trim().length <= 5 || /^\/\w/.test(t.text.trim());
|
|
102
247
|
let totalReal = 0, totalCompliant = 0, totalTerse = 0;
|
|
103
248
|
for (const [sid, s] of sessions) {
|
|
@@ -122,20 +267,126 @@ if (opts.gmAudit) {
|
|
|
122
267
|
process.exit(0);
|
|
123
268
|
}
|
|
124
269
|
|
|
270
|
+
// ---------- rollup (filtered)
|
|
125
271
|
if (opts.rollup) {
|
|
126
|
-
const stats = await rollup({ since, out: opts.rollup, format: opts.format });
|
|
272
|
+
const stats = await rollup({ since, out: opts.rollup, format: opts.format || 'ndjson' });
|
|
127
273
|
process.stderr.write(`# rolled up ${stats.rows} events from ${stats.events} routed (${stats.files} files) → ${stats.format}: ${stats.out}\n`);
|
|
128
274
|
process.exit(0);
|
|
129
275
|
}
|
|
130
276
|
|
|
131
|
-
|
|
132
|
-
r.on('streaming_progress', out);
|
|
133
|
-
r.on('error', e => process.stderr.write(`error: ${e?.message || e}\n`));
|
|
134
|
-
|
|
277
|
+
// ---------- live tail (filter applied to live events)
|
|
135
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`));
|
|
136
282
|
r.start();
|
|
137
|
-
process.stdout.write('tailing... (Ctrl-C to exit)\n');
|
|
283
|
+
process.stdout.write('# tailing... (Ctrl-C to exit)\n');
|
|
284
|
+
process.stdin.resume();
|
|
138
285
|
} else {
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
|
|
141
392
|
}
|