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 +1 -1
- package/src/cli.js +4 -50
- package/src/index.cjs +32 -1
- package/src/index.js +31 -0
package/package.json
CHANGED
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', '
|
|
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);
|