ccsniff 1.0.11 → 1.0.13
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 +41 -1
- package/src/index.js +13 -4
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { JsonlReplayer, rollup } from './index.js';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
function parseArgs(argv) {
|
|
6
|
-
const opts = { since: null, grep: null, cwd: null, role: null, type: null, limit: 0, json: false, tail: false, rollup: null, format: 'ndjson' };
|
|
6
|
+
const opts = { since: null, grep: null, cwd: null, role: null, type: null, limit: 0, json: false, tail: false, rollup: null, format: 'ndjson', gmAudit: false };
|
|
7
7
|
const rest = [];
|
|
8
8
|
for (let i = 0; i < argv.length; i++) {
|
|
9
9
|
const a = argv[i];
|
|
@@ -18,6 +18,7 @@ function parseArgs(argv) {
|
|
|
18
18
|
else if (a === '--tail' || a === '-f') opts.tail = true;
|
|
19
19
|
else if (a === '--rollup') opts.rollup = next();
|
|
20
20
|
else if (a === '--format') opts.format = next();
|
|
21
|
+
else if (a === '--gm-audit') opts.gmAudit = true;
|
|
21
22
|
else if (a === '-h' || a === '--help') { printHelp(); process.exit(0); }
|
|
22
23
|
else rest.push(a);
|
|
23
24
|
}
|
|
@@ -32,6 +33,7 @@ Usage:
|
|
|
32
33
|
[--type text|tool_use|tool_result] [--limit N] [--json] [-f]
|
|
33
34
|
ccsniff --rollup out.ndjson [--since 7d]
|
|
34
35
|
ccsniff --rollup out.sqlite --format sqlite [--since 7d] # requires better-sqlite3
|
|
36
|
+
ccsniff --gm-audit [--since 24h] [--cwd repo]
|
|
35
37
|
|
|
36
38
|
Examples:
|
|
37
39
|
ccsniff --since 24h --grep "rs-exec" --limit 50
|
|
@@ -73,6 +75,44 @@ function out(ev) {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
if (opts.gmAudit) {
|
|
79
|
+
const sessions = new Map();
|
|
80
|
+
const r2 = new JsonlReplayer();
|
|
81
|
+
r2.on('streaming_progress', ev => {
|
|
82
|
+
const conv = ev.conversation;
|
|
83
|
+
if (cwdRe && !cwdRe.test(conv.cwd || '')) return;
|
|
84
|
+
if (ev.role !== 'user' && ev.role !== 'assistant') return;
|
|
85
|
+
const sid = conv.id;
|
|
86
|
+
if (!sessions.has(sid)) sessions.set(sid, { cwd: conv.cwd, turns: [] });
|
|
87
|
+
const s = sessions.get(sid);
|
|
88
|
+
if (ev.role === 'user' && ev.block?.type === 'text') {
|
|
89
|
+
const t = ev.block.text || '';
|
|
90
|
+
const isSystem = ev.block.isMeta || /^<(task-notification|command-name|local-command|system-reminder)\b/.test(t.trimStart()) || t === '[Request interrupted by user]';
|
|
91
|
+
s.turns.push({ isMeta: isSystem, firstTool: null, text: t.slice(0, 80) });
|
|
92
|
+
} else if (ev.role === 'assistant' && ev.block?.type === 'tool_use' && s.turns.length) {
|
|
93
|
+
const last = s.turns[s.turns.length - 1];
|
|
94
|
+
if (last.firstTool === null) last.firstTool = ev.block.name || '';
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
r2.replay({ since });
|
|
98
|
+
let totalReal = 0, totalCompliant = 0;
|
|
99
|
+
for (const [sid, s] of sessions) {
|
|
100
|
+
const real = s.turns.filter(t => !t.isMeta);
|
|
101
|
+
const compliant = real.filter(t => t.firstTool === 'Skill' || t.firstTool === 'mcp__gm__Skill');
|
|
102
|
+
totalReal += real.length;
|
|
103
|
+
totalCompliant += compliant.length;
|
|
104
|
+
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
|
+
process.stdout.write(`[${pct}%] ${path.basename(s.cwd || sid)} (${compliant.length}/${real.length}) sid=${sid.slice(0, 8)}\n`);
|
|
107
|
+
for (const v of violations.slice(0, 3)) {
|
|
108
|
+
process.stdout.write(` MISS first=${v.firstTool || 'none'} msg="${v.text.replace(/\s+/g, ' ')}"\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const total = totalReal ? Math.round(100 * totalCompliant / totalReal) : 0;
|
|
112
|
+
process.stderr.write(`# gm-audit: ${totalCompliant}/${totalReal} compliant (${total}%) across ${sessions.size} sessions\n`);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
if (opts.rollup) {
|
|
77
117
|
const stats = await rollup({ since, out: opts.rollup, format: opts.format });
|
|
78
118
|
process.stderr.write(`# rolled up ${stats.rows} events from ${stats.events} routed (${stats.files} files) → ${stats.format}: ${stats.out}\n`);
|
package/src/index.js
CHANGED
|
@@ -128,7 +128,14 @@ export class JsonlWatcher extends EventEmitter {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
_route(conv, sid, e) {
|
|
131
|
-
if (e.type === 'queue-operation' || e.type === 'last-prompt'
|
|
131
|
+
if (e.type === 'queue-operation' || e.type === 'last-prompt') return;
|
|
132
|
+
if (e.type === 'user' && e.isMeta) {
|
|
133
|
+
this._startStreaming(conv, sid);
|
|
134
|
+
const content = e.message?.content;
|
|
135
|
+
const text = typeof content === 'string' ? content : (Array.isArray(content) ? content.filter(b => b?.type === 'text').map(b => b.text).join('') : '');
|
|
136
|
+
if (text.trim()) this._push(conv, sid, { type: 'text', text, isMeta: true }, 'user');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
132
139
|
|
|
133
140
|
if (e.isApiErrorMessage && e.error === 'rate_limit') {
|
|
134
141
|
this.emit('streaming_error', { conversationId: conv.id, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
|
|
@@ -249,6 +256,7 @@ function rollupNdjson(r, { since, out }) {
|
|
|
249
256
|
type: b.type || null,
|
|
250
257
|
text: text.slice(0, 4000),
|
|
251
258
|
tool: b.name || null,
|
|
259
|
+
isMeta: b.isMeta || false,
|
|
252
260
|
}) + '\n');
|
|
253
261
|
rows++;
|
|
254
262
|
});
|
|
@@ -266,15 +274,15 @@ async function rollupSqlite(r, { since, out }) {
|
|
|
266
274
|
db.exec(`
|
|
267
275
|
CREATE TABLE IF NOT EXISTS events (
|
|
268
276
|
ts INTEGER, sid TEXT, parent TEXT, cwd TEXT,
|
|
269
|
-
role TEXT, type TEXT, tool TEXT, text TEXT
|
|
277
|
+
role TEXT, type TEXT, tool TEXT, text TEXT, is_meta INTEGER DEFAULT 0
|
|
270
278
|
);
|
|
271
279
|
CREATE INDEX IF NOT EXISTS events_sid ON events(sid);
|
|
272
280
|
CREATE INDEX IF NOT EXISTS events_ts ON events(ts);
|
|
273
281
|
CREATE INDEX IF NOT EXISTS events_parent ON events(parent);
|
|
274
282
|
`);
|
|
275
|
-
const insert = db.prepare('INSERT INTO events (ts, sid, parent, cwd, role, type, tool, text) VALUES (
|
|
283
|
+
const insert = db.prepare('INSERT INTO events (ts, sid, parent, cwd, role, type, tool, text, is_meta) VALUES (?,?,?,?,?,?,?,?,?)');
|
|
276
284
|
let rows = 0;
|
|
277
|
-
const tx = db.transaction(events => { for (const e of events) insert.run(e.ts, e.sid, e.parent, e.cwd, e.role, e.type, e.tool, e.text); });
|
|
285
|
+
const tx = db.transaction(events => { for (const e of events) insert.run(e.ts, e.sid, e.parent, e.cwd, e.role, e.type, e.tool, e.text, e.isMeta ? 1 : 0); });
|
|
278
286
|
let batch = [];
|
|
279
287
|
r.on('streaming_progress', ev => {
|
|
280
288
|
const b = ev.block || {};
|
|
@@ -288,6 +296,7 @@ async function rollupSqlite(r, { since, out }) {
|
|
|
288
296
|
type: b.type || null,
|
|
289
297
|
tool: b.name || null,
|
|
290
298
|
text: (text || '').slice(0, 4000),
|
|
299
|
+
isMeta: b.isMeta || false,
|
|
291
300
|
});
|
|
292
301
|
rows++;
|
|
293
302
|
if (batch.length >= 500) { tx(batch); batch = []; }
|