ccsniff 1.1.2 → 1.1.3

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 +32 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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,20 @@ 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\//;
245
251
  const violations = [];
246
252
  for (const ev of all) {
247
253
  if (!filter(ev)) continue;
248
254
  if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
255
+ if (!includeSubagents && ev.conversation?.isSubagent) continue;
249
256
  const cmd = ev.block?.input?.command || '';
257
+ // `echo > .gm/exec-spool/in/<verb>/N.txt` is the canonical spool-write pattern, not a deviation.
258
+ if (SPOOL_WRITE.test(cmd) && /^\s*echo\b/.test(cmd)) continue;
250
259
  const kind = SLEEP_POLL.test(cmd) ? 'sleep-poll' : (BAD_LEADING.test(cmd) ? 'bad-leading-cmd' : null);
251
260
  if (!kind) continue;
252
261
  violations.push({ ts: ev.timestamp, sid: ev.conversation.id, project: path.basename(ev.conversation.cwd || ''), kind, cmd: cmd.slice(0, 200) });
@@ -255,12 +264,32 @@ if (opts['bash-discipline']) {
255
264
  for (const v of violations) byKind.set(v.kind, (byKind.get(v.kind) || 0) + 1);
256
265
  if (opts.stats || opts.count) {
257
266
  if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
258
- process.stdout.write(`# ${violations.length} bash-discipline violations\n`);
267
+ const subagentNote = includeSubagents ? '' : ' (subagents excluded — pass --include-subagents to include)';
268
+ process.stdout.write(`# ${violations.length} bash-discipline violations${subagentNote}\n`);
259
269
  for (const [k, c] of [...byKind.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${k}\n`);
260
270
  const byProj = new Map();
261
271
  for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
262
272
  process.stdout.write(`# by project\n`);
263
273
  for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
274
+ const byDay = new Map();
275
+ for (const v of violations) {
276
+ const day = new Date(v.ts).toISOString().slice(0, 10);
277
+ byDay.set(day, (byDay.get(day) || 0) + 1);
278
+ }
279
+ if (byDay.size > 1) {
280
+ process.stdout.write(`# by day\n`);
281
+ for (const [d, c] of [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]))) process.stdout.write(` ${String(c).padStart(6)} ${d}\n`);
282
+ }
283
+ const byHour = new Map();
284
+ for (const v of violations) {
285
+ const hour = new Date(v.ts).toISOString().slice(0, 13);
286
+ byHour.set(hour, (byHour.get(hour) || 0) + 1);
287
+ }
288
+ if (byHour.size > 1) {
289
+ process.stdout.write(`# by hour (last 12)\n`);
290
+ const sorted = [...byHour.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-12);
291
+ for (const [h, c] of sorted) process.stdout.write(` ${String(c).padStart(6)} ${h}:00\n`);
292
+ }
264
293
  process.exit(0);
265
294
  }
266
295
  for (const v of violations) {