ccsniff 1.0.27 → 1.0.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { JsonlReplayer, rollup } from './index.js';
2
+ import { JsonlReplayer, rollup, vault } from './index.js';
3
3
  import path from 'path';
4
4
 
5
5
  if (process.argv[2] === 'gui') {
@@ -22,11 +22,13 @@ if (process.argv[2] === 'gui') {
22
22
  process.stdin.resume();
23
23
  } else {
24
24
 
25
+ { const r = vault(); if (r.copied > 0) process.stderr.write(`# vault: ${r.copied} copied → ~/.claude/history-backup\n`); }
26
+
25
27
  const FLAGS = {
26
28
  string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort'],
27
29
  multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
28
30
  number: ['limit', 'head', 'tail-n', 'ctx', 'truncate'],
29
- bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'stats', 'count', 'gm-audit', 'help', 'h'],
31
+ bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'stats', 'count', 'help', 'h'],
30
32
  };
31
33
 
32
34
  function parseArgs(argv) {
@@ -60,7 +62,6 @@ USAGE
60
62
  ccsniff --list-projects
61
63
  ccsniff --list-tools
62
64
  ccsniff --stats [filters]
63
- ccsniff --gm-audit [filters]
64
65
 
65
66
  TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
66
67
  --since <t> include events at/after t (alias: --after)
@@ -240,53 +241,6 @@ if (opts.help || process.argv.length <= 2) { printHelp(); process.exit(0); }
240
241
  const since = parseTime(opts.since || opts.after);
241
242
  const filter = buildFilter(opts);
242
243
 
243
- // ---------- gm-audit (existing, untouched logic, now respects new filters)
244
- if (opts['gm-audit']) {
245
- const sessions = new Map();
246
- const r2 = new JsonlReplayer();
247
- r2.on('streaming_progress', ev => {
248
- if (!filter(ev)) return;
249
- const conv = ev.conversation;
250
- if (conv.isSubagent) return;
251
- if (ev.role !== 'user' && ev.role !== 'assistant') return;
252
- const sid = conv.id;
253
- if (!sessions.has(sid)) sessions.set(sid, { cwd: conv.cwd, turns: [] });
254
- const s = sessions.get(sid);
255
- if (ev.role === 'user' && ev.block?.type === 'text') {
256
- const t = ev.block.text || '';
257
- const isContinuation = /^This session is being continued from a previous conversation/.test(t.trimStart());
258
- const isSystem = ev.block.isMeta || isContinuation || /^<(task-notification|command-name|local-command|system-reminder)\b/.test(t.trimStart()) || t === '[Request interrupted by user]' || t === '[Request interrupted by user for tool use]';
259
- s.turns.push({ isMeta: isSystem, firstTool: null, text: t.slice(0, 80) });
260
- } else if (ev.role === 'assistant' && ev.block?.type === 'tool_use' && s.turns.length) {
261
- const last = s.turns[s.turns.length - 1];
262
- if (last.firstTool === null) last.firstTool = ev.block.name || '';
263
- }
264
- });
265
- r2.replay({ since });
266
- const isTerse = t => t.text.trim().length <= 5 || /^\/\w/.test(t.text.trim());
267
- let totalReal = 0, totalCompliant = 0, totalTerse = 0;
268
- for (const [sid, s] of sessions) {
269
- const real = s.turns.filter(t => !t.isMeta);
270
- const compliant = real.filter(t => t.firstTool === 'Skill' || t.firstTool === 'mcp__gm__Skill');
271
- const terse = real.filter(t => isTerse(t) && t.firstTool !== 'Skill' && t.firstTool !== 'mcp__gm__Skill');
272
- totalReal += real.length;
273
- totalCompliant += compliant.length;
274
- totalTerse += terse.length;
275
- const pct = real.length ? Math.round(100 * compliant.length / real.length) : 0;
276
- const violations = real.filter(t => t.firstTool !== 'Skill' && t.firstTool !== 'mcp__gm__Skill' && !isTerse(t));
277
- if (real.length === 0) continue;
278
- process.stdout.write(`[${pct}%] ${path.basename(s.cwd || sid)} (${compliant.length}/${real.length}, terse-skip:${terse.length}) sid=${sid.slice(0, 8)}\n`);
279
- for (const v of violations.slice(0, 3)) {
280
- process.stdout.write(` MISS first=${v.firstTool || 'none'} msg="${v.text.replace(/\s+/g, ' ')}"\n`);
281
- }
282
- }
283
- const total = totalReal ? Math.round(100 * totalCompliant / totalReal) : 0;
284
- const adjCompliant = totalCompliant + totalTerse;
285
- const adjTotal = totalReal ? Math.round(100 * adjCompliant / totalReal) : 0;
286
- process.stderr.write(`# gm-audit: ${totalCompliant}/${totalReal} compliant (${total}%) | adj (terse-skip): ${adjCompliant}/${totalReal} (${adjTotal}%) | ${sessions.size} sessions\n`);
287
- process.exit(0);
288
- }
289
-
290
244
  // ---------- rollup (filtered)
291
245
  if (opts.rollup) {
292
246
  const stats = await rollup({ since, out: opts.rollup, format: opts.format || 'ndjson' });
package/src/index.cjs CHANGED
@@ -228,6 +228,37 @@ function replay(projectsDir, opts) {
228
228
  return new JsonlReplayer(projectsDir);
229
229
  }
230
230
 
231
+ function vault({ projectsDir = DEFAULT_DIR, destDir = path.join(os.homedir(), '.claude', 'history-backup') } = {}) {
232
+ if (!fs.existsSync(projectsDir)) return { copied: 0, skipped: 0 };
233
+ let copied = 0, skipped = 0;
234
+ const walk = (src, depth) => {
235
+ if (depth > 5) return;
236
+ let entries;
237
+ try { entries = fs.readdirSync(src, { withFileTypes: true }); } catch { return; }
238
+ for (const d of entries) {
239
+ const srcPath = path.join(src, d.name);
240
+ const rel = path.relative(projectsDir, srcPath);
241
+ const dstPath = path.join(destDir, rel);
242
+ if (d.isDirectory()) { walk(srcPath, depth + 1); continue; }
243
+ if (!d.name.endsWith('.jsonl')) continue;
244
+ let srcStat;
245
+ try { srcStat = fs.statSync(srcPath); } catch { continue; }
246
+ try {
247
+ const dstStat = fs.statSync(dstPath);
248
+ if (dstStat.size === srcStat.size && dstStat.mtimeMs >= srcStat.mtimeMs) { skipped++; continue; }
249
+ } catch {}
250
+ try {
251
+ fs.mkdirSync(path.dirname(dstPath), { recursive: true });
252
+ fs.copyFileSync(srcPath, dstPath);
253
+ fs.utimesSync(dstPath, srcStat.atime, srcStat.mtime);
254
+ copied++;
255
+ } catch {}
256
+ }
257
+ };
258
+ walk(projectsDir, 0);
259
+ return { copied, skipped };
260
+ }
261
+
231
262
  async function rollup({ projectsDir, since = 0, out, format = 'ndjson' } = {}) {
232
263
  if (!out) throw new Error('rollup: out path required');
233
264
  const r = new JsonlReplayer(projectsDir);
@@ -299,4 +330,4 @@ async function rollupSqlite(r, { since, out }) {
299
330
  return { ...stats, rows, format: 'sqlite', out };
300
331
  }
301
332
 
302
- module.exports = { JsonlWatcher, JsonlReplayer, watch, replay, rollup };
333
+ module.exports = { JsonlWatcher, JsonlReplayer, watch, replay, rollup, vault };
package/src/index.js CHANGED
@@ -234,6 +234,37 @@ export function replay(projectsDir, opts) {
234
234
  return new JsonlReplayer(projectsDir);
235
235
  }
236
236
 
237
+ export function vault({ projectsDir = DEFAULT_DIR, destDir = path.join(os.homedir(), '.claude', 'history-backup') } = {}) {
238
+ if (!fs.existsSync(projectsDir)) return { copied: 0, skipped: 0 };
239
+ let copied = 0, skipped = 0;
240
+ const walk = (src, depth) => {
241
+ if (depth > 5) return;
242
+ let entries;
243
+ try { entries = fs.readdirSync(src, { withFileTypes: true }); } catch { return; }
244
+ for (const d of entries) {
245
+ const srcPath = path.join(src, d.name);
246
+ const rel = path.relative(projectsDir, srcPath);
247
+ const dstPath = path.join(destDir, rel);
248
+ if (d.isDirectory()) { walk(srcPath, depth + 1); continue; }
249
+ if (!d.name.endsWith('.jsonl')) continue;
250
+ let srcStat;
251
+ try { srcStat = fs.statSync(srcPath); } catch { continue; }
252
+ try {
253
+ const dstStat = fs.statSync(dstPath);
254
+ if (dstStat.size === srcStat.size && dstStat.mtimeMs >= srcStat.mtimeMs) { skipped++; continue; }
255
+ } catch {}
256
+ try {
257
+ fs.mkdirSync(path.dirname(dstPath), { recursive: true });
258
+ fs.copyFileSync(srcPath, dstPath);
259
+ fs.utimesSync(dstPath, srcStat.atime, srcStat.mtime);
260
+ copied++;
261
+ } catch {}
262
+ }
263
+ };
264
+ walk(projectsDir, 0);
265
+ return { copied, skipped };
266
+ }
267
+
237
268
  export async function rollup({ projectsDir, since = 0, out, format = 'ndjson' } = {}) {
238
269
  if (!out) throw new Error('rollup: out path required');
239
270
  const r = new JsonlReplayer(projectsDir);