ccsniff 1.1.4 → 1.1.6
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 +49 -3
- package/src/filters.js +6 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -26,10 +26,10 @@ if (process.argv[2] === 'gui') {
|
|
|
26
26
|
} else {
|
|
27
27
|
|
|
28
28
|
const FLAGS = {
|
|
29
|
-
string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format'],
|
|
30
|
-
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
|
|
29
|
+
string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
|
|
30
|
+
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
|
|
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', 'include-subagents', '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', 'git-discipline', 'include-subagents', 'stats', 'count', 'help', 'h'],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
function parseArgs(argv) {
|
|
@@ -63,6 +63,7 @@ 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
|
+
ccsniff --git-discipline [--stats] git push from a dirty/unwitnessed tree
|
|
66
67
|
(excludes subagents by default — --include-subagents to opt in;
|
|
67
68
|
excludes 'echo > .gm/exec-spool/in/...' as canonical spool-write)
|
|
68
69
|
ccsniff --stats [filters]
|
|
@@ -81,6 +82,9 @@ FILTERS (repeatable flags combine as OR within a flag, AND across flags)
|
|
|
81
82
|
--type <t> text|tool_use|tool_result|thinking|system|result; repeat = OR
|
|
82
83
|
--tool <name> tool name (Read, Bash, ...); repeat = OR
|
|
83
84
|
--session <sid> session id prefix; repeat = OR (alias: --sid)
|
|
85
|
+
--exclude-sess <sid> exclude session id prefix; repeat = exclude any (alias: --exclude-sid)
|
|
86
|
+
--exclude-cwd <re> exclude working-dir regex; repeat = exclude any
|
|
87
|
+
--exclude-project <n> exclude basename(cwd) exact match; repeat = exclude any
|
|
84
88
|
--parent <sid> subagent parent session id
|
|
85
89
|
--no-subagents exclude subagent sessions
|
|
86
90
|
--only-subagents only subagent sessions
|
|
@@ -306,6 +310,48 @@ if (opts['bash-discipline']) {
|
|
|
306
310
|
process.exit(0);
|
|
307
311
|
}
|
|
308
312
|
|
|
313
|
+
if (opts['git-discipline']) {
|
|
314
|
+
const includeSubagents = opts['include-subagents'];
|
|
315
|
+
const PUSH = /\bgit\s+push\b/;
|
|
316
|
+
const PORCELAIN_CLEAN = /\bgit\s+status\s+(--porcelain|-s)\b/;
|
|
317
|
+
const bySid = new Map();
|
|
318
|
+
for (const ev of all) {
|
|
319
|
+
if (!filter(ev)) continue;
|
|
320
|
+
if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
|
|
321
|
+
if (!includeSubagents && ev.conversation?.isSubagent) continue;
|
|
322
|
+
const sid = ev.conversation.id;
|
|
323
|
+
if (!bySid.has(sid)) bySid.set(sid, []);
|
|
324
|
+
bySid.get(sid).push(ev);
|
|
325
|
+
}
|
|
326
|
+
const violations = [];
|
|
327
|
+
for (const [sid, evs] of bySid) {
|
|
328
|
+
evs.sort((a, b) => a.timestamp - b.timestamp);
|
|
329
|
+
for (let i = 0; i < evs.length; i++) {
|
|
330
|
+
const ev = evs[i];
|
|
331
|
+
const cmd = ev.block?.input?.command || '';
|
|
332
|
+
if (!PUSH.test(cmd)) continue;
|
|
333
|
+
const lookback = evs.slice(Math.max(0, i - 20), i);
|
|
334
|
+
const witnessed = lookback.some(e => PORCELAIN_CLEAN.test(e.block?.input?.command || ''));
|
|
335
|
+
if (witnessed) continue;
|
|
336
|
+
violations.push({ ts: ev.timestamp, sid, project: path.basename(ev.conversation.cwd || ''), kind: 'push-no-porcelain-witness', cmd: cmd.slice(0, 200) });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (opts.stats || opts.count) {
|
|
340
|
+
if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
|
|
341
|
+
process.stdout.write(`# ${violations.length} git-discipline violations\n`);
|
|
342
|
+
const byProj = new Map();
|
|
343
|
+
for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
|
|
344
|
+
process.stdout.write(`# by project\n`);
|
|
345
|
+
for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
for (const v of violations) {
|
|
349
|
+
process.stdout.write(`${new Date(v.ts).toISOString().slice(0, 19)} ${v.sid.slice(0, 8)} ${v.kind.padEnd(28)} [${v.project}] ${v.cmd}\n`);
|
|
350
|
+
}
|
|
351
|
+
process.stderr.write(`# ${violations.length} violations (push-no-porcelain-witness)\n`);
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
|
|
309
355
|
// ---------- list-tools
|
|
310
356
|
if (opts['list-tools']) {
|
|
311
357
|
const tools = new Map();
|
package/src/filters.js
CHANGED
|
@@ -46,6 +46,9 @@ export function buildFilter(opts) {
|
|
|
46
46
|
const types = new Set(m.type || []);
|
|
47
47
|
const tools = new Set(m.tool || []);
|
|
48
48
|
const sids = (m.session || []).concat(m.sid || []);
|
|
49
|
+
const excludeSids = (m['exclude-sess'] || []).concat(m['exclude-sid'] || []);
|
|
50
|
+
const excludeCwdRes = compileRegexes(m['exclude-cwd']);
|
|
51
|
+
const excludeProjects = new Set(m['exclude-project'] || []);
|
|
49
52
|
const parent = opts.parent || null;
|
|
50
53
|
|
|
51
54
|
return ev => {
|
|
@@ -61,6 +64,9 @@ export function buildFilter(opts) {
|
|
|
61
64
|
else if (types.size && !types.has(block.type)) pass = false;
|
|
62
65
|
else if (tools.size && !tools.has(block.name)) pass = false;
|
|
63
66
|
else if (sids.length && !sids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
67
|
+
else if (excludeSids.length && excludeSids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
68
|
+
else if (excludeCwdRes.length && excludeCwdRes.some(r => r.test(conv.cwd || ''))) pass = false;
|
|
69
|
+
else if (excludeProjects.size && excludeProjects.has(path.basename(conv.cwd || ''))) pass = false;
|
|
64
70
|
else if (parent && conv.parentSid !== parent) pass = false;
|
|
65
71
|
else if (opts['no-subagents'] && conv.isSubagent) pass = false;
|
|
66
72
|
else if (opts['only-subagents'] && !conv.isSubagent) pass = false;
|