ccsniff 1.1.2 → 1.1.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +39 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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
@@ -29,7 +29,7 @@ const FLAGS = {
29
29
  string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format'],
30
30
  multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
31
31
  number: ['limit', 'head', 'tail-n', 'ctx', 'truncate'],
32
- bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'bash-discipline', 'stats', 'count', 'help', 'h'],
32
+ bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'bash-discipline', 'include-subagents', 'stats', 'count', 'help', 'h'],
33
33
  };
34
34
 
35
35
  function parseArgs(argv) {
@@ -63,6 +63,8 @@ USAGE
63
63
  ccsniff --list-projects
64
64
  ccsniff --list-tools
65
65
  ccsniff --bash-discipline [--stats] Bash calls that should have used Read/Glob/Grep
66
+ (excludes subagents by default — --include-subagents to opt in;
67
+ excludes 'echo > .gm/exec-spool/in/...' as canonical spool-write)
66
68
  ccsniff --stats [filters]
67
69
 
68
70
  TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
@@ -240,13 +242,27 @@ if (opts['list-projects']) {
240
242
 
241
243
  // ---------- bash-discipline (flag Bash calls that should have been Read/Glob/Grep/dispatch)
242
244
  if (opts['bash-discipline']) {
243
- const BAD_LEADING = /^\s*(cat|head|tail|ls|grep|find|sed|awk|echo)\b/;
245
+ // discipline is about MY tool routing, not subagents — they have separate prompts/contexts.
246
+ // Default: exclude subagents. --include-subagents opts them back in.
247
+ const includeSubagents = opts['include-subagents'];
248
+ const BAD_LEADING = /^\s*(cat|head|tail|ls|grep|find|sed|awk)\b/;
244
249
  const SLEEP_POLL = /\bsleep\s+\d+\s*;.*(cat|ls|grep|find|head|tail)/;
250
+ const SPOOL_WRITE = /\.gm\/exec-spool\/in\//;
251
+ // The host harness explicitly endorses `until <check>; do sleep N; done` as
252
+ // the canonical pattern for polling external state (see Bash tool description
253
+ // and Monitor docs). Same for `while !curl ...; do sleep N; done`. These are
254
+ // NOT sleep-poll violations even though they contain `sleep N`.
255
+ const ENDORSED_POLL = /^\s*(until|while)\s+/;
245
256
  const violations = [];
246
257
  for (const ev of all) {
247
258
  if (!filter(ev)) continue;
248
259
  if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
260
+ if (!includeSubagents && ev.conversation?.isSubagent) continue;
249
261
  const cmd = ev.block?.input?.command || '';
262
+ // `echo > .gm/exec-spool/in/<verb>/N.txt` is the canonical spool-write pattern, not a deviation.
263
+ if (SPOOL_WRITE.test(cmd) && /^\s*echo\b/.test(cmd)) continue;
264
+ // `until ...; do sleep N; done` is the harness-endorsed poll pattern.
265
+ if (ENDORSED_POLL.test(cmd)) continue;
250
266
  const kind = SLEEP_POLL.test(cmd) ? 'sleep-poll' : (BAD_LEADING.test(cmd) ? 'bad-leading-cmd' : null);
251
267
  if (!kind) continue;
252
268
  violations.push({ ts: ev.timestamp, sid: ev.conversation.id, project: path.basename(ev.conversation.cwd || ''), kind, cmd: cmd.slice(0, 200) });
@@ -255,12 +271,32 @@ if (opts['bash-discipline']) {
255
271
  for (const v of violations) byKind.set(v.kind, (byKind.get(v.kind) || 0) + 1);
256
272
  if (opts.stats || opts.count) {
257
273
  if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
258
- process.stdout.write(`# ${violations.length} bash-discipline violations\n`);
274
+ const subagentNote = includeSubagents ? '' : ' (subagents excluded — pass --include-subagents to include)';
275
+ process.stdout.write(`# ${violations.length} bash-discipline violations${subagentNote}\n`);
259
276
  for (const [k, c] of [...byKind.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${k}\n`);
260
277
  const byProj = new Map();
261
278
  for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
262
279
  process.stdout.write(`# by project\n`);
263
280
  for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
281
+ const byDay = new Map();
282
+ for (const v of violations) {
283
+ const day = new Date(v.ts).toISOString().slice(0, 10);
284
+ byDay.set(day, (byDay.get(day) || 0) + 1);
285
+ }
286
+ if (byDay.size > 1) {
287
+ process.stdout.write(`# by day\n`);
288
+ for (const [d, c] of [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]))) process.stdout.write(` ${String(c).padStart(6)} ${d}\n`);
289
+ }
290
+ const byHour = new Map();
291
+ for (const v of violations) {
292
+ const hour = new Date(v.ts).toISOString().slice(0, 13);
293
+ byHour.set(hour, (byHour.get(hour) || 0) + 1);
294
+ }
295
+ if (byHour.size > 1) {
296
+ process.stdout.write(`# by hour (last 12)\n`);
297
+ const sorted = [...byHour.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-12);
298
+ for (const [h, c] of sorted) process.stdout.write(` ${String(c).padStart(6)} ${h}:00\n`);
299
+ }
264
300
  process.exit(0);
265
301
  }
266
302
  for (const v of violations) {