ccsniff 1.0.33 → 1.1.2
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/README.md +14 -0
- package/package.json +9 -1
- package/src/cli.js +34 -1
- package/src/index.js +5 -1
- package/src/router.js +86 -0
- package/src/store.js +294 -0
package/README.md
CHANGED
|
@@ -33,6 +33,20 @@ CommonJS:
|
|
|
33
33
|
const { watch, JsonlWatcher } = require('ccsniff');
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Mountable Express router (`/v1/history/*`)
|
|
37
|
+
|
|
38
|
+
ccsniff ships a mountable router that hosts all the read-only history endpoints used by AgentGUI's live client (`/v1/history/snapshot`, `/sessions`, `/sessions/:sid/events`, `/search`, `/stream` SSE, and `POST /reindex`). It reads `~/.claude/projects` by default; override with the `CLAUDE_PROJECTS_DIR` env var. `express` is an optional peer dependency — install it in the host app.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import express from 'express';
|
|
42
|
+
import { createHistoryRouter } from 'ccsniff';
|
|
43
|
+
|
|
44
|
+
const app = express();
|
|
45
|
+
app.use(await createHistoryRouter({ projectsDir: process.env.CLAUDE_PROJECTS_DIR }));
|
|
46
|
+
app.listen(3000);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
36
50
|
## CLI
|
|
37
51
|
|
|
38
52
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccsniff",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -39,5 +39,13 @@
|
|
|
39
39
|
"homepage": "https://anentrypoint.github.io/ccsniff",
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"express": "^4 || ^5"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"express": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
}
|
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', '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', 'stats', 'count', 'help', 'h'],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
function parseArgs(argv) {
|
|
@@ -62,6 +62,7 @@ USAGE
|
|
|
62
62
|
ccsniff --list-sessions [filters]
|
|
63
63
|
ccsniff --list-projects
|
|
64
64
|
ccsniff --list-tools
|
|
65
|
+
ccsniff --bash-discipline [--stats] Bash calls that should have used Read/Glob/Grep
|
|
65
66
|
ccsniff --stats [filters]
|
|
66
67
|
|
|
67
68
|
TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
|
|
@@ -237,6 +238,38 @@ if (opts['list-projects']) {
|
|
|
237
238
|
process.exit(0);
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// ---------- bash-discipline (flag Bash calls that should have been Read/Glob/Grep/dispatch)
|
|
242
|
+
if (opts['bash-discipline']) {
|
|
243
|
+
const BAD_LEADING = /^\s*(cat|head|tail|ls|grep|find|sed|awk|echo)\b/;
|
|
244
|
+
const SLEEP_POLL = /\bsleep\s+\d+\s*;.*(cat|ls|grep|find|head|tail)/;
|
|
245
|
+
const violations = [];
|
|
246
|
+
for (const ev of all) {
|
|
247
|
+
if (!filter(ev)) continue;
|
|
248
|
+
if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
|
|
249
|
+
const cmd = ev.block?.input?.command || '';
|
|
250
|
+
const kind = SLEEP_POLL.test(cmd) ? 'sleep-poll' : (BAD_LEADING.test(cmd) ? 'bad-leading-cmd' : null);
|
|
251
|
+
if (!kind) continue;
|
|
252
|
+
violations.push({ ts: ev.timestamp, sid: ev.conversation.id, project: path.basename(ev.conversation.cwd || ''), kind, cmd: cmd.slice(0, 200) });
|
|
253
|
+
}
|
|
254
|
+
const byKind = new Map();
|
|
255
|
+
for (const v of violations) byKind.set(v.kind, (byKind.get(v.kind) || 0) + 1);
|
|
256
|
+
if (opts.stats || opts.count) {
|
|
257
|
+
if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
|
|
258
|
+
process.stdout.write(`# ${violations.length} bash-discipline violations\n`);
|
|
259
|
+
for (const [k, c] of [...byKind.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${k}\n`);
|
|
260
|
+
const byProj = new Map();
|
|
261
|
+
for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
|
|
262
|
+
process.stdout.write(`# by project\n`);
|
|
263
|
+
for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
for (const v of violations) {
|
|
267
|
+
process.stdout.write(`${new Date(v.ts).toISOString().slice(0, 19)} ${v.sid.slice(0, 8)} ${v.kind.padEnd(15)} [${v.project}] ${v.cmd}\n`);
|
|
268
|
+
}
|
|
269
|
+
process.stderr.write(`# ${violations.length} violations (${[...byKind.entries()].map(([k, c]) => `${k}:${c}`).join(' ')})\n`);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
240
273
|
// ---------- list-tools
|
|
241
274
|
if (opts['list-tools']) {
|
|
242
275
|
const tools = new Map();
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
|
|
6
|
-
const DEFAULT_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
6
|
+
const DEFAULT_DIR = process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects');
|
|
7
|
+
export { DEFAULT_DIR };
|
|
7
8
|
const DEBOUNCE_MS = 16;
|
|
8
9
|
|
|
9
10
|
export class JsonlWatcher extends EventEmitter {
|
|
@@ -265,6 +266,9 @@ export function vault({ projectsDir = DEFAULT_DIR, destDir = path.join(os.homedi
|
|
|
265
266
|
return { copied, skipped };
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
export { Store, getStore, flattenEvent, blockText, DEFAULT_PROJECTS_DIR } from './store.js';
|
|
270
|
+
export { createHistoryRouter } from './router.js';
|
|
271
|
+
|
|
268
272
|
export async function rollup({ projectsDir, since = 0, out, format = 'ndjson' } = {}) {
|
|
269
273
|
if (!out) throw new Error('rollup: out path required');
|
|
270
274
|
const r = new JsonlReplayer(projectsDir);
|
package/src/router.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Mountable Express router exposing /v1/history/* — mount at app.use('/', createHistoryRouter()).
|
|
2
|
+
// Express is a peer dependency. Caller must `npm i express`.
|
|
3
|
+
import { Store, getStore, DEFAULT_PROJECTS_DIR } from './store.js';
|
|
4
|
+
|
|
5
|
+
function loadExpress() {
|
|
6
|
+
// Lazy import so ccsniff doesn't hard-require express for its other exports.
|
|
7
|
+
// Caller (e.g. agentgui) already has express in its deps.
|
|
8
|
+
// eslint-disable-next-line no-undef
|
|
9
|
+
return import('express').then(m => m.default || m);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function createHistoryRouter({ projectsDir, store: providedStore } = {}) {
|
|
13
|
+
const express = await loadExpress();
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
const store = providedStore || getStore(projectsDir || DEFAULT_PROJECTS_DIR);
|
|
16
|
+
|
|
17
|
+
const corsHeaders = (res) => {
|
|
18
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
19
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
router.use((req, res, next) => { corsHeaders(res); next(); });
|
|
23
|
+
|
|
24
|
+
router.get('/v1/history/snapshot', (req, res) => {
|
|
25
|
+
try { res.json(store.snapshot()); }
|
|
26
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
router.get('/v1/history/sessions', (req, res) => {
|
|
30
|
+
try { res.json({ sessions: store.sessions() }); }
|
|
31
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
router.get('/v1/history/sessions/:sid/events', (req, res) => {
|
|
35
|
+
try { res.json({ sid: req.params.sid, events: store.sessionEvents(req.params.sid) }); }
|
|
36
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.get('/v1/history/search', (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const q = req.query.q || '';
|
|
42
|
+
const limit = parseInt(req.query.limit, 10) || 50;
|
|
43
|
+
const opts = { limit };
|
|
44
|
+
for (const k of ['role', 'type', 'project', 'sid']) {
|
|
45
|
+
if (req.query[k]) opts[k] = req.query[k];
|
|
46
|
+
}
|
|
47
|
+
const results = q ? store.search(q, opts) : [];
|
|
48
|
+
const hits = results.map(r => ({ sid: r.sid, snippet: r.snippet, score: r.score, ts: r.ts, role: r.role, type: r.type, project: r.project, text: r.text }));
|
|
49
|
+
res.json({ query: q, hits, results });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
res.status(500).json({ error: { message: e.message } });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
router.post('/v1/history/reindex', (req, res) => {
|
|
56
|
+
try { store.rebuildIndex(); res.json({ ok: true, at: store.lastBuilt }); }
|
|
57
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.get('/v1/history/stream', (req, res) => {
|
|
61
|
+
res.writeHead(200, {
|
|
62
|
+
'Content-Type': 'text/event-stream',
|
|
63
|
+
'Cache-Control': 'no-cache',
|
|
64
|
+
'Connection': 'keep-alive',
|
|
65
|
+
'Access-Control-Allow-Origin': '*',
|
|
66
|
+
});
|
|
67
|
+
res.write('event: hello\ndata: {}\n\n');
|
|
68
|
+
store.sseClients.add(res);
|
|
69
|
+
req.on('close', () => store.sseClients.delete(res));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Compatibility GET reindex (some clients may call GET).
|
|
73
|
+
router.get('/v1/history/reindex', (req, res) => {
|
|
74
|
+
try { store.rebuildIndex(); res.json({ ok: true, at: store.lastBuilt }); }
|
|
75
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
router.get('/v1/history', (req, res) => {
|
|
79
|
+
try { res.json(store.snapshot()); }
|
|
80
|
+
catch (e) { res.status(500).json({ error: { message: e.message } }); }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return router;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { Store, getStore, DEFAULT_PROJECTS_DIR };
|
package/src/store.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Shared store for both legacy /api/* (gui-server.js) and new /v1/history/* (router.js).
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { JsonlReplayer, JsonlWatcher } from './index.js';
|
|
5
|
+
import { buildIndex, search, snippet, tokenize } from './bm25.js';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_PROJECTS_DIR =
|
|
8
|
+
process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects');
|
|
9
|
+
|
|
10
|
+
export function blockText(b) {
|
|
11
|
+
if (!b) return '';
|
|
12
|
+
if (typeof b.text === 'string') return b.text;
|
|
13
|
+
if (typeof b.content === 'string') return b.content;
|
|
14
|
+
if (Array.isArray(b.content)) return b.content.map(c => c?.text || '').join('');
|
|
15
|
+
if (b.input) { try { return JSON.stringify(b.input); } catch { return ''; } }
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function flattenEvent(ev, idx) {
|
|
20
|
+
const c = ev.conversation || {};
|
|
21
|
+
const b = ev.block || {};
|
|
22
|
+
return {
|
|
23
|
+
i: idx,
|
|
24
|
+
ts: ev.timestamp || 0,
|
|
25
|
+
sid: c.id || '',
|
|
26
|
+
parent: c.parentSid || null,
|
|
27
|
+
cwd: c.cwd || '',
|
|
28
|
+
project: path.basename(c.cwd || ''),
|
|
29
|
+
isSubagent: !!c.isSubagent,
|
|
30
|
+
role: ev.role,
|
|
31
|
+
type: b.type || null,
|
|
32
|
+
tool: b.name || null,
|
|
33
|
+
text: blockText(b),
|
|
34
|
+
isError: !!b.is_error || ev.role === 'streaming_error',
|
|
35
|
+
isMeta: !!b.isMeta,
|
|
36
|
+
cost: b.total_cost_usd || null,
|
|
37
|
+
duration: b.duration_ms || null,
|
|
38
|
+
subtype: b.subtype || null,
|
|
39
|
+
model: b.model || null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class Store {
|
|
44
|
+
constructor(projectsDir) {
|
|
45
|
+
this.projectsDir = projectsDir || DEFAULT_PROJECTS_DIR;
|
|
46
|
+
this.events = [];
|
|
47
|
+
this.errors = [];
|
|
48
|
+
this.fileBytes = 0;
|
|
49
|
+
this.fileCount = 0;
|
|
50
|
+
this.index = null;
|
|
51
|
+
this.lastBuilt = 0;
|
|
52
|
+
this.watcher = null;
|
|
53
|
+
this.sseClients = new Set();
|
|
54
|
+
this.convs = new Map();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadOnce() {
|
|
58
|
+
const r = new JsonlReplayer(this.projectsDir);
|
|
59
|
+
let i = 0;
|
|
60
|
+
r.on('conversation_created', ev => this.convs.set(ev.conversation.id, ev.conversation));
|
|
61
|
+
r.on('streaming_progress', ev => { this.events.push(flattenEvent(ev, i++)); });
|
|
62
|
+
r.on('streaming_error', ev => { this.errors.push({ ts: ev.timestamp, sid: ev.conversationId, error: ev.error, recoverable: ev.recoverable }); });
|
|
63
|
+
const stats = r.replay({});
|
|
64
|
+
this.fileCount = stats.files;
|
|
65
|
+
this.rebuildIndex();
|
|
66
|
+
return stats;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rebuildIndex() {
|
|
70
|
+
this.index = buildIndex(this.events, e => e.text);
|
|
71
|
+
this.lastBuilt = Date.now();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
startLive() {
|
|
75
|
+
if (this.watcher) return;
|
|
76
|
+
this.watcher = new JsonlWatcher(this.projectsDir);
|
|
77
|
+
this.watcher.on('conversation_created', ev => {
|
|
78
|
+
this.convs.set(ev.conversation.id, ev.conversation);
|
|
79
|
+
this.broadcast('conversation', { conv: ev.conversation, ts: ev.timestamp });
|
|
80
|
+
});
|
|
81
|
+
this.watcher.on('streaming_progress', ev => {
|
|
82
|
+
const fl = flattenEvent(ev, this.events.length);
|
|
83
|
+
this.events.push(fl);
|
|
84
|
+
this.broadcast('event', { sid: fl.sid, payload: fl });
|
|
85
|
+
});
|
|
86
|
+
this.watcher.on('streaming_error', ev => {
|
|
87
|
+
const e = { ts: ev.timestamp, sid: ev.conversationId, error: ev.error, recoverable: ev.recoverable };
|
|
88
|
+
this.errors.push(e);
|
|
89
|
+
this.broadcast('error', e);
|
|
90
|
+
});
|
|
91
|
+
this.watcher.on('streaming_start', ev => this.broadcast('start', { sid: ev.conversationId, ts: ev.timestamp }));
|
|
92
|
+
this.watcher.on('streaming_complete', ev => this.broadcast('complete', { sid: ev.conversationId, ts: ev.timestamp }));
|
|
93
|
+
this.watcher.start();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
stop() {
|
|
97
|
+
if (this.watcher) this.watcher.stop();
|
|
98
|
+
for (const r of this.sseClients) try { r.end(); } catch {}
|
|
99
|
+
this.sseClients.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
broadcast(kind, data) {
|
|
103
|
+
const payload = `event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
104
|
+
for (const res of this.sseClients) { try { res.write(payload); } catch {} }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
snapshot() {
|
|
108
|
+
const sids = new Set(), projects = new Set(), tools = new Map();
|
|
109
|
+
let earliest = Infinity, latest = 0, bytes = 0;
|
|
110
|
+
for (const e of this.events) {
|
|
111
|
+
sids.add(e.sid); if (e.project) projects.add(e.project);
|
|
112
|
+
if (e.tool) tools.set(e.tool, (tools.get(e.tool) || 0) + 1);
|
|
113
|
+
if (e.ts < earliest) earliest = e.ts;
|
|
114
|
+
if (e.ts > latest) latest = e.ts;
|
|
115
|
+
bytes += (e.text || '').length;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
events: this.events.length,
|
|
119
|
+
sessions: sids.size,
|
|
120
|
+
projects: projects.size,
|
|
121
|
+
tools: tools.size,
|
|
122
|
+
errors: this.errors.length,
|
|
123
|
+
files: this.fileCount,
|
|
124
|
+
bytes,
|
|
125
|
+
earliest: earliest === Infinity ? 0 : earliest,
|
|
126
|
+
latest,
|
|
127
|
+
dateRange: { earliest: earliest === Infinity ? 0 : earliest, latest },
|
|
128
|
+
indexedAt: this.lastBuilt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sessions() {
|
|
133
|
+
const map = new Map();
|
|
134
|
+
for (const e of this.events) {
|
|
135
|
+
let s = map.get(e.sid);
|
|
136
|
+
if (!s) {
|
|
137
|
+
const conv = this.convs.get(e.sid) || {};
|
|
138
|
+
s = { sid: e.sid, title: conv.title || '', project: e.project, cwd: e.cwd, parent: e.parent, isSubagent: e.isSubagent, first: e.ts, last: e.ts, events: 0, tools: 0, userTurns: 0, cost: 0, errors: 0 };
|
|
139
|
+
map.set(e.sid, s);
|
|
140
|
+
}
|
|
141
|
+
s.events++;
|
|
142
|
+
if (e.ts < s.first) s.first = e.ts;
|
|
143
|
+
if (e.ts > s.last) s.last = e.ts;
|
|
144
|
+
if (e.type === 'tool_use') s.tools++;
|
|
145
|
+
if (e.role === 'user' && e.type === 'text') s.userTurns++;
|
|
146
|
+
if (e.cost) s.cost += e.cost;
|
|
147
|
+
if (e.isError) s.errors++;
|
|
148
|
+
}
|
|
149
|
+
return [...map.values()].sort((a, b) => b.last - a.last);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
sessionEvents(sid) {
|
|
153
|
+
return this.events.filter(e => e.sid === sid);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
projects() {
|
|
157
|
+
const map = new Map();
|
|
158
|
+
for (const e of this.events) {
|
|
159
|
+
if (!e.project) continue;
|
|
160
|
+
let p = map.get(e.project);
|
|
161
|
+
if (!p) { p = { project: e.project, sessions: new Set(), events: 0, tools: 0, last: 0, errors: 0, cost: 0 }; map.set(e.project, p); }
|
|
162
|
+
p.events++;
|
|
163
|
+
p.sessions.add(e.sid);
|
|
164
|
+
if (e.type === 'tool_use') p.tools++;
|
|
165
|
+
if (e.ts > p.last) p.last = e.ts;
|
|
166
|
+
if (e.cost) p.cost += e.cost;
|
|
167
|
+
if (e.isError) p.errors++;
|
|
168
|
+
}
|
|
169
|
+
return [...map.values()].map(p => ({ ...p, sessions: p.sessions.size })).sort((a, b) => b.last - a.last);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
tools() {
|
|
173
|
+
const map = new Map();
|
|
174
|
+
for (const e of this.events) {
|
|
175
|
+
if (!e.tool) continue;
|
|
176
|
+
let t = map.get(e.tool);
|
|
177
|
+
if (!t) { t = { tool: e.tool, count: 0, sessions: new Set(), errors: 0, last: 0 }; map.set(e.tool, t); }
|
|
178
|
+
t.count++;
|
|
179
|
+
t.sessions.add(e.sid);
|
|
180
|
+
if (e.isError) t.errors++;
|
|
181
|
+
if (e.ts > t.last) t.last = e.ts;
|
|
182
|
+
}
|
|
183
|
+
return [...map.values()].map(t => ({ ...t, sessions: t.sessions.size })).sort((a, b) => b.count - a.count);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
timeline(bucketMs = 3600_000) {
|
|
187
|
+
const buckets = new Map();
|
|
188
|
+
for (const e of this.events) {
|
|
189
|
+
const k = Math.floor(e.ts / bucketMs) * bucketMs;
|
|
190
|
+
let b = buckets.get(k);
|
|
191
|
+
if (!b) { b = { t: k, events: 0, tools: 0, errors: 0, sessions: new Set() }; buckets.set(k, b); }
|
|
192
|
+
b.events++;
|
|
193
|
+
if (e.type === 'tool_use') b.tools++;
|
|
194
|
+
if (e.isError) b.errors++;
|
|
195
|
+
b.sessions.add(e.sid);
|
|
196
|
+
}
|
|
197
|
+
return [...buckets.values()].map(b => ({ ...b, sessions: b.sessions.size })).sort((a, b) => a.t - b.t);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
stats() {
|
|
201
|
+
const role = {}, type = {}, model = {};
|
|
202
|
+
let cost = 0, dur = 0, results = 0;
|
|
203
|
+
for (const e of this.events) {
|
|
204
|
+
role[e.role || '?'] = (role[e.role || '?'] || 0) + 1;
|
|
205
|
+
type[e.type || '?'] = (type[e.type || '?'] || 0) + 1;
|
|
206
|
+
if (e.model) model[e.model] = (model[e.model] || 0) + 1;
|
|
207
|
+
if (e.cost) { cost += e.cost; results++; }
|
|
208
|
+
if (e.duration) dur += e.duration;
|
|
209
|
+
}
|
|
210
|
+
return { role, type, model, totalCostUsd: cost, totalDurationMs: dur, results };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
errorsList() { return this.errors.slice(-200).reverse(); }
|
|
214
|
+
|
|
215
|
+
subagents() {
|
|
216
|
+
const tree = new Map();
|
|
217
|
+
for (const e of this.events) {
|
|
218
|
+
if (!e.isSubagent) continue;
|
|
219
|
+
const parent = e.parent || 'orphan';
|
|
220
|
+
let p = tree.get(parent);
|
|
221
|
+
if (!p) { p = { parent, children: new Map() }; tree.set(parent, p); }
|
|
222
|
+
let c = p.children.get(e.sid);
|
|
223
|
+
if (!c) { c = { sid: e.sid, project: e.project, events: 0, last: 0 }; p.children.set(e.sid, c); }
|
|
224
|
+
c.events++;
|
|
225
|
+
if (e.ts > c.last) c.last = e.ts;
|
|
226
|
+
}
|
|
227
|
+
return [...tree.values()].map(p => ({ parent: p.parent, children: [...p.children.values()] }));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
search(q, { limit = 50, role, type, project, sid } = {}) {
|
|
231
|
+
if (!this.index) this.rebuildIndex();
|
|
232
|
+
const hits = search(this.index, q, { limit: limit * 4 });
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const h of hits) {
|
|
235
|
+
const e = this.events[h.i];
|
|
236
|
+
if (role && e.role !== role) continue;
|
|
237
|
+
if (type && e.type !== type) continue;
|
|
238
|
+
if (project && e.project !== project) continue;
|
|
239
|
+
if (sid && !e.sid.startsWith(sid)) continue;
|
|
240
|
+
out.push({ ...e, score: h.score, terms: h.terms, snippet: snippet(e.text, h.terms) });
|
|
241
|
+
if (out.length >= limit) break;
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
events_filtered({ role, type, project, sid, tool, since, until, limit = 200, offset = 0, q, grep, igrep, isMeta, isSubagent, isError, parent } = {}) {
|
|
247
|
+
let arr = this.events;
|
|
248
|
+
let greRe = null, igreRe = null;
|
|
249
|
+
try {
|
|
250
|
+
if (grep) greRe = new RegExp(grep, 'i');
|
|
251
|
+
if (igrep) igreRe = new RegExp(igrep, 'i');
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return { total: 0, rows: [], error: `invalid regex: ${e.message}` };
|
|
254
|
+
}
|
|
255
|
+
if (q) {
|
|
256
|
+
const tokens = [...new Set(tokenize(q))];
|
|
257
|
+
if (tokens.length) {
|
|
258
|
+
arr = arr.filter(e => { const t = tokenize(e.text); return tokens.every(x => t.includes(x)); });
|
|
259
|
+
} else {
|
|
260
|
+
const needle = String(q).toLowerCase();
|
|
261
|
+
arr = arr.filter(e => (e.text || '').toLowerCase().includes(needle));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
arr = arr.filter(e => {
|
|
265
|
+
if (role && e.role !== role) return false;
|
|
266
|
+
if (type && e.type !== type) return false;
|
|
267
|
+
if (project && e.project !== project) return false;
|
|
268
|
+
if (sid && !e.sid.startsWith(sid)) return false;
|
|
269
|
+
if (parent && e.parent !== parent) return false;
|
|
270
|
+
if (tool && e.tool !== tool) return false;
|
|
271
|
+
if (since && e.ts < since) return false;
|
|
272
|
+
if (until && e.ts > until) return false;
|
|
273
|
+
if (isMeta === true && !e.isMeta) return false;
|
|
274
|
+
if (isMeta === false && e.isMeta) return false;
|
|
275
|
+
if (isSubagent === true && !e.isSubagent) return false;
|
|
276
|
+
if (isSubagent === false && e.isSubagent) return false;
|
|
277
|
+
if (isError === true && !e.isError) return false;
|
|
278
|
+
if (isError === false && e.isError) return false;
|
|
279
|
+
if (greRe && !greRe.test(e.text || '')) return false;
|
|
280
|
+
if (igreRe && igreRe.test(e.text || '')) return false;
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
return { total: arr.length, rows: arr.slice(offset, offset + limit) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let _shared = null;
|
|
288
|
+
export function getStore(projectsDir) {
|
|
289
|
+
if (_shared) return _shared;
|
|
290
|
+
_shared = new Store(projectsDir);
|
|
291
|
+
_shared.loadOnce();
|
|
292
|
+
_shared.startLive();
|
|
293
|
+
return _shared;
|
|
294
|
+
}
|