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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +41 -1
  3. package/src/index.js +13 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Watch Claude Code JSONL output files and emit structured events as a Node.js EventEmitter",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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' || (e.type === 'user' && e.isMeta)) return;
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 = []; }