ccsniff 1.0.31 → 1.0.33
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 +15 -0
- package/gui/app.js +83 -9
- package/package.json +1 -1
- package/src/cli.js +12 -60
- package/src/filters.js +89 -0
- package/src/gui-server.js +57 -8
- package/src/unsloth.js +13 -4
package/README.md
CHANGED
|
@@ -67,6 +67,21 @@ Two formats are supported via `--unsloth-format`:
|
|
|
67
67
|
|
|
68
68
|
Sessions with no user/assistant turn pair are skipped (no training value).
|
|
69
69
|
|
|
70
|
+
## GUI (Observatory)
|
|
71
|
+
|
|
72
|
+
A turnkey, zero-dep browser observatory ships with the package. It indexes
|
|
73
|
+
every session in `~/.claude/projects` and exposes sessions, projects, tools,
|
|
74
|
+
timeline, errors, subagent tree, live stream, and BM25 full-text search.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx ccsniff gui --open # auto-open in default browser
|
|
78
|
+
npx ccsniff gui --port 4791 # custom port (default 4791)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Endpoints under `/api/*`: `snapshot`, `sessions`, `projects`, `tools`,
|
|
82
|
+
`timeline`, `stats`, `errors`, `subagents`, `events`, `search`, `stream`
|
|
83
|
+
(SSE).
|
|
84
|
+
|
|
70
85
|
## API
|
|
71
86
|
|
|
72
87
|
### `watch(projectsDir?)` → `JsonlWatcher`
|
package/gui/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mount, components as C, h } from 'anentrypoint-design';
|
|
2
2
|
|
|
3
|
-
const TABS = ['overview', 'sessions', 'projects', 'tools', 'timeline', 'errors', 'subagents', 'live', 'search'];
|
|
3
|
+
const TABS = ['overview', 'sessions', 'projects', 'tools', 'timeline', 'errors', 'subagents', 'events', 'live', 'search'];
|
|
4
4
|
const state = {
|
|
5
5
|
tab: 'overview',
|
|
6
6
|
data: { snapshot: null, sessions: [], projects: [], tools: [], timeline: [], stats: null, errors: [], subagents: [] },
|
|
@@ -8,6 +8,9 @@ const state = {
|
|
|
8
8
|
searchResults: [],
|
|
9
9
|
searching: false,
|
|
10
10
|
liveLog: [],
|
|
11
|
+
defaults: { active: 'recent', presets: [] },
|
|
12
|
+
activePreset: 'recent',
|
|
13
|
+
events: { total: 0, rows: [], loading: false },
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const api = (p) => fetch(p).then(r => r.json());
|
|
@@ -144,16 +147,81 @@ function SubagentsView() {
|
|
|
144
147
|
) });
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
function PresetChips() {
|
|
151
|
+
const presets = state.defaults.presets || [];
|
|
152
|
+
return h('div', { style: 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 12px' },
|
|
153
|
+
...presets.map(p => h('span', {
|
|
154
|
+
class: 'pill' + (p.id === state.activePreset ? ' accent' : ''),
|
|
155
|
+
style: 'cursor:pointer;user-select:none;' + (p.id === state.activePreset ? 'outline:1px solid currentColor' : ''),
|
|
156
|
+
onclick: () => { state.activePreset = p.id; loadEvents(); },
|
|
157
|
+
}, p.label)),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function currentPresetQuery() {
|
|
162
|
+
const p = (state.defaults.presets || []).find(x => x.id === state.activePreset);
|
|
163
|
+
return p?.query || {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function loadEvents() {
|
|
167
|
+
state.events.loading = true; render();
|
|
168
|
+
const q = currentPresetQuery();
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
for (const [k, v] of Object.entries(q)) if (v !== undefined && v !== null && v !== '') params.set(k, String(v));
|
|
171
|
+
params.set('limit', '200');
|
|
172
|
+
const r = await api('/api/events?' + params.toString());
|
|
173
|
+
state.events = { total: r.total || 0, rows: r.rows || [], loading: false, error: r.error || null };
|
|
174
|
+
render();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function EventsView() {
|
|
178
|
+
const rows = state.events.rows;
|
|
179
|
+
return C.Panel({ head: `events · ${state.events.total} match · showing ${rows.length}`, children: h('div', {},
|
|
180
|
+
PresetChips(),
|
|
181
|
+
state.events.error ? h('div', { class: 'err', style: 'padding:8px 12px' }, state.events.error) : null,
|
|
182
|
+
h('div', { class: 'row-grid', style: 'grid-template-columns:140px 80px 110px 100px 1fr;opacity:.55' },
|
|
183
|
+
h('span', {}, 'when'), h('span', {}, 'role'), h('span', {}, 'type'), h('span', {}, 'tool'), h('span', {}, 'text')),
|
|
184
|
+
...rows.map(e => h('div', { class: 'row-grid', style: 'grid-template-columns:140px 80px 110px 100px 1fr' },
|
|
185
|
+
h('span', {}, ts(e.ts).slice(5)),
|
|
186
|
+
h('span', {}, e.role || '—'),
|
|
187
|
+
h('span', {}, e.type || '—'),
|
|
188
|
+
h('span', { class: 'accent' }, e.tool || '—'),
|
|
189
|
+
h('span', { class: 'truncate' + (e.isError ? ' err' : '') },
|
|
190
|
+
h('span', { class: 'pill' }, e.project || '—'),
|
|
191
|
+
' ',
|
|
192
|
+
(e.text || '').slice(0, 400)),
|
|
153
193
|
)),
|
|
154
194
|
) });
|
|
155
195
|
}
|
|
156
196
|
|
|
197
|
+
function liveMatches(e) {
|
|
198
|
+
const q = currentPresetQuery();
|
|
199
|
+
if (q.role && e.role !== q.role) return false;
|
|
200
|
+
if (q.type && e.type !== q.type) return false;
|
|
201
|
+
if (q.tool && e.tool !== q.tool) return false;
|
|
202
|
+
if (q.project && e.project !== q.project) return false;
|
|
203
|
+
if (q.isMeta === true && !e.isMeta) return false;
|
|
204
|
+
if (q.isMeta === false && e.isMeta) return false;
|
|
205
|
+
if (q.isSubagent === true && !e.isSubagent) return false;
|
|
206
|
+
if (q.isSubagent === false && e.isSubagent) return false;
|
|
207
|
+
if (q.isError === true && !e.isError) return false;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function LiveView() {
|
|
212
|
+
const filtered = state.liveLog.filter(e => e._kind !== 'event' || liveMatches(e));
|
|
213
|
+
return C.Panel({ head: `live · ${filtered.length}/${state.liveLog.length} events (preset: ${state.activePreset})`, children: h('div', {},
|
|
214
|
+
PresetChips(),
|
|
215
|
+
h('div', { class: 'live' },
|
|
216
|
+
...filtered.slice().reverse().map((e, i) => h('div', { key: i, class: 'e' },
|
|
217
|
+
h('span', { class: 'ts' }, ts(e.ts || Date.now()).slice(11)),
|
|
218
|
+
h('span', { class: 'k' }, e._kind || 'event'),
|
|
219
|
+
h('span', {}, fmtLive(e)),
|
|
220
|
+
)),
|
|
221
|
+
),
|
|
222
|
+
) });
|
|
223
|
+
}
|
|
224
|
+
|
|
157
225
|
function fmtLive(e) {
|
|
158
226
|
if (e._kind === 'conversation') return 'new ' + (e.conv?.title || e.conv?.id?.slice(0, 8));
|
|
159
227
|
if (e._kind === 'start' || e._kind === 'complete') return (e.sid || '').slice(0, 8);
|
|
@@ -192,7 +260,7 @@ async function doSearch() {
|
|
|
192
260
|
state.searching = false; render();
|
|
193
261
|
}
|
|
194
262
|
|
|
195
|
-
const VIEWS = { overview: Overview, sessions: SessionsView, projects: ProjectsView, tools: ToolsView, timeline: TimelineView, errors: ErrorsView, subagents: SubagentsView, live: LiveView, search: SearchView };
|
|
263
|
+
const VIEWS = { overview: Overview, sessions: SessionsView, projects: ProjectsView, tools: ToolsView, timeline: TimelineView, errors: ErrorsView, subagents: SubagentsView, events: EventsView, live: LiveView, search: SearchView };
|
|
196
264
|
|
|
197
265
|
function App() {
|
|
198
266
|
const s = state.data.snapshot || {};
|
|
@@ -201,7 +269,7 @@ function App() {
|
|
|
201
269
|
brand: '247420', leaf: 'ccsniff',
|
|
202
270
|
items: TABS.map(t => [t, '#/' + t]),
|
|
203
271
|
active: state.tab,
|
|
204
|
-
onNav: (t) => { state.tab = t; location.hash = '#/' + t; render(); },
|
|
272
|
+
onNav: (t) => { state.tab = t; location.hash = '#/' + t; render(); if (t === 'events') loadEvents(); },
|
|
205
273
|
}),
|
|
206
274
|
main: h('div', { style: 'padding:16px;display:flex;flex-direction:column;gap:16px' },
|
|
207
275
|
C.Crumb({ trail: ['247420', 'ccsniff'], leaf: state.tab, right: h('span', { class: 'pill' }, n(s.events) + ' events') }),
|
|
@@ -224,7 +292,13 @@ render();
|
|
|
224
292
|
})();
|
|
225
293
|
|
|
226
294
|
(async function init() {
|
|
295
|
+
try {
|
|
296
|
+
const d = await api('/api/defaults');
|
|
297
|
+
state.defaults = d || state.defaults;
|
|
298
|
+
state.activePreset = d?.active || state.activePreset;
|
|
299
|
+
} catch {}
|
|
227
300
|
await loadAll();
|
|
301
|
+
if (state.tab === 'events') loadEvents();
|
|
228
302
|
setInterval(loadAll, 15_000);
|
|
229
303
|
const sse = new EventSource('/api/stream');
|
|
230
304
|
const push = (kind, data) => {
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { JsonlReplayer, rollup, vault } from './index.js';
|
|
3
3
|
import { toUnslothMessages, toShareGPT } from './unsloth.js';
|
|
4
|
+
import { parseTime, compileRegexes, buildFilter } from './filters.js';
|
|
4
5
|
import fs from 'fs';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
|
|
@@ -24,8 +25,6 @@ if (process.argv[2] === 'gui') {
|
|
|
24
25
|
process.stdin.resume();
|
|
25
26
|
} else {
|
|
26
27
|
|
|
27
|
-
{ const r = vault(); if (r.copied > 0) process.stderr.write(`# vault: ${r.copied} copied → ~/.claude/history-backup\n`); }
|
|
28
|
-
|
|
29
28
|
const FLAGS = {
|
|
30
29
|
string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format'],
|
|
31
30
|
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
|
|
@@ -71,7 +70,7 @@ TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
|
|
|
71
70
|
|
|
72
71
|
FILTERS (repeatable flags combine as OR within a flag, AND across flags)
|
|
73
72
|
--grep <re> text regex (case-insensitive); repeat = AND
|
|
74
|
-
--igrep <re>
|
|
73
|
+
--igrep <re> exclude if regex matches text; repeat = exclude if ANY matches
|
|
75
74
|
--invert invert the entire filter result
|
|
76
75
|
--cwd <re> working-dir regex
|
|
77
76
|
--project <name> basename(cwd) exact match; repeat = OR
|
|
@@ -116,21 +115,6 @@ EXAMPLES
|
|
|
116
115
|
`);
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
function parseTime(s) {
|
|
120
|
-
if (!s) return 0;
|
|
121
|
-
if (/^\d{10,}$/.test(s)) return parseInt(s, 10);
|
|
122
|
-
const m = /^(\d+)([smhdw])$/.exec(s);
|
|
123
|
-
if (m) {
|
|
124
|
-
const n = parseInt(m[1], 10);
|
|
125
|
-
const mult = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }[m[2]];
|
|
126
|
-
return Date.now() - n * mult;
|
|
127
|
-
}
|
|
128
|
-
const t = Date.parse(s);
|
|
129
|
-
return Number.isFinite(t) ? t : 0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function compileRegexes(arr) { return arr.map(s => new RegExp(s, 'i')); }
|
|
133
|
-
|
|
134
118
|
function blockText(b) {
|
|
135
119
|
if (!b) return '';
|
|
136
120
|
if (typeof b.text === 'string') return b.text;
|
|
@@ -140,46 +124,6 @@ function blockText(b) {
|
|
|
140
124
|
return '';
|
|
141
125
|
}
|
|
142
126
|
|
|
143
|
-
function buildFilter(opts) {
|
|
144
|
-
const since = parseTime(opts.since || opts.after);
|
|
145
|
-
const until = parseTime(opts.until || opts.before);
|
|
146
|
-
const greps = compileRegexes(opts._multi.grep);
|
|
147
|
-
const igreps = compileRegexes(opts._multi.igrep);
|
|
148
|
-
const cwdRes = compileRegexes(opts._multi.cwd);
|
|
149
|
-
const projects = new Set(opts._multi.project);
|
|
150
|
-
const roles = new Set(opts._multi.role);
|
|
151
|
-
const types = new Set(opts._multi.type);
|
|
152
|
-
const tools = new Set(opts._multi.tool);
|
|
153
|
-
const sids = opts._multi.session.concat(opts._multi.sid || []);
|
|
154
|
-
const parent = opts.parent || null;
|
|
155
|
-
|
|
156
|
-
return ev => {
|
|
157
|
-
const conv = ev.conversation || {};
|
|
158
|
-
const block = ev.block || {};
|
|
159
|
-
const ts = ev.timestamp || 0;
|
|
160
|
-
let pass = true;
|
|
161
|
-
if (since && ts < since) pass = false;
|
|
162
|
-
else if (until && ts > until) pass = false;
|
|
163
|
-
else if (cwdRes.length && !cwdRes.every(r => r.test(conv.cwd || ''))) pass = false;
|
|
164
|
-
else if (projects.size && !projects.has(path.basename(conv.cwd || ''))) pass = false;
|
|
165
|
-
else if (roles.size && !roles.has(ev.role)) pass = false;
|
|
166
|
-
else if (types.size && !types.has(block.type)) pass = false;
|
|
167
|
-
else if (tools.size && !tools.has(block.name)) pass = false;
|
|
168
|
-
else if (sids.length && !sids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
169
|
-
else if (parent && conv.parentSid !== parent) pass = false;
|
|
170
|
-
else if (opts['no-subagents'] && conv.isSubagent) pass = false;
|
|
171
|
-
else if (opts['only-subagents'] && !conv.isSubagent) pass = false;
|
|
172
|
-
else if (opts['no-meta'] && block.isMeta) pass = false;
|
|
173
|
-
else if (opts['only-meta'] && !block.isMeta) pass = false;
|
|
174
|
-
else {
|
|
175
|
-
const text = blockText(block);
|
|
176
|
-
if (greps.length && !greps.every(r => r.test(text))) pass = false;
|
|
177
|
-
else if (igreps.length && igreps.some(r => r.test(text))) pass = false;
|
|
178
|
-
}
|
|
179
|
-
return opts.invert ? !pass : pass;
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
127
|
function formatRow(ev, opts) {
|
|
184
128
|
const conv = ev.conversation || {};
|
|
185
129
|
const block = ev.block || {};
|
|
@@ -242,8 +186,16 @@ function sortRows(rows, key, reverse) {
|
|
|
242
186
|
const { opts } = parseArgs(process.argv.slice(2));
|
|
243
187
|
if (opts.help || process.argv.length <= 2) { printHelp(); process.exit(0); }
|
|
244
188
|
|
|
245
|
-
const
|
|
246
|
-
|
|
189
|
+
{ const r = vault(); if (r.copied > 0) process.stderr.write(`# vault: ${r.copied} copied → ~/.claude/history-backup\n`); }
|
|
190
|
+
|
|
191
|
+
let since, filter;
|
|
192
|
+
try {
|
|
193
|
+
since = parseTime(opts.since || opts.after);
|
|
194
|
+
filter = buildFilter(opts);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
process.stderr.write(`ccsniff: ${e.message}\n`);
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
247
199
|
|
|
248
200
|
// ---------- rollup (filtered)
|
|
249
201
|
if (opts.rollup) {
|
package/src/filters.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Filter primitives shared by cli.js, gui-server.js, and tests.
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function parseTime(s) {
|
|
5
|
+
if (s === undefined || s === null || s === '') return 0;
|
|
6
|
+
if (typeof s === 'number') return Number.isFinite(s) ? s : 0;
|
|
7
|
+
s = String(s).trim();
|
|
8
|
+
if (!s) return 0;
|
|
9
|
+
if (/^\d{10,}$/.test(s)) return parseInt(s, 10);
|
|
10
|
+
const m = /^(\d+)\s*([smhdw])$/i.exec(s);
|
|
11
|
+
if (m) {
|
|
12
|
+
const n = parseInt(m[1], 10);
|
|
13
|
+
const mult = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }[m[2].toLowerCase()];
|
|
14
|
+
return Date.now() - n * mult;
|
|
15
|
+
}
|
|
16
|
+
const t = Date.parse(s);
|
|
17
|
+
if (Number.isFinite(t)) return t;
|
|
18
|
+
throw new Error(`invalid time: ${s} (use ISO date, epoch ms, or Ns/Nm/Nh/Nd/Nw)`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compileRegexes(arr) {
|
|
22
|
+
return (arr || []).map(s => {
|
|
23
|
+
try { return new RegExp(s, 'i'); }
|
|
24
|
+
catch (e) { throw new Error(`invalid regex /${s}/: ${e.message}`); }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function blockText(b) {
|
|
29
|
+
if (!b) return '';
|
|
30
|
+
if (typeof b.text === 'string') return b.text;
|
|
31
|
+
if (typeof b.content === 'string') return b.content;
|
|
32
|
+
if (Array.isArray(b.content)) return b.content.map(c => c?.text || '').join('');
|
|
33
|
+
if (b.input) { try { return JSON.stringify(b.input); } catch { return ''; } }
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildFilter(opts) {
|
|
38
|
+
const m = opts._multi || {};
|
|
39
|
+
const since = parseTime(opts.since || opts.after);
|
|
40
|
+
const until = parseTime(opts.until || opts.before);
|
|
41
|
+
const greps = compileRegexes(m.grep);
|
|
42
|
+
const igreps = compileRegexes(m.igrep);
|
|
43
|
+
const cwdRes = compileRegexes(m.cwd);
|
|
44
|
+
const projects = new Set(m.project || []);
|
|
45
|
+
const roles = new Set(m.role || []);
|
|
46
|
+
const types = new Set(m.type || []);
|
|
47
|
+
const tools = new Set(m.tool || []);
|
|
48
|
+
const sids = (m.session || []).concat(m.sid || []);
|
|
49
|
+
const parent = opts.parent || null;
|
|
50
|
+
|
|
51
|
+
return ev => {
|
|
52
|
+
const conv = ev.conversation || {};
|
|
53
|
+
const block = ev.block || {};
|
|
54
|
+
const ts = ev.timestamp || 0;
|
|
55
|
+
let pass = true;
|
|
56
|
+
if (since && ts < since) pass = false;
|
|
57
|
+
else if (until && ts > until) pass = false;
|
|
58
|
+
else if (cwdRes.length && !cwdRes.every(r => r.test(conv.cwd || ''))) pass = false;
|
|
59
|
+
else if (projects.size && !projects.has(path.basename(conv.cwd || ''))) pass = false;
|
|
60
|
+
else if (roles.size && !roles.has(ev.role)) pass = false;
|
|
61
|
+
else if (types.size && !types.has(block.type)) pass = false;
|
|
62
|
+
else if (tools.size && !tools.has(block.name)) pass = false;
|
|
63
|
+
else if (sids.length && !sids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
64
|
+
else if (parent && conv.parentSid !== parent) pass = false;
|
|
65
|
+
else if (opts['no-subagents'] && conv.isSubagent) pass = false;
|
|
66
|
+
else if (opts['only-subagents'] && !conv.isSubagent) pass = false;
|
|
67
|
+
else if (opts['no-meta'] && block.isMeta) pass = false;
|
|
68
|
+
else if (opts['only-meta'] && !block.isMeta) pass = false;
|
|
69
|
+
else {
|
|
70
|
+
const text = blockText(block);
|
|
71
|
+
if (greps.length && !greps.every(r => r.test(text))) pass = false;
|
|
72
|
+
else if (igreps.length && igreps.some(r => r.test(text))) pass = false;
|
|
73
|
+
}
|
|
74
|
+
return opts.invert ? !pass : pass;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_PRESETS = [
|
|
79
|
+
{ id: 'recent', label: 'Last 24h', query: { since: '24h', isMeta: false } },
|
|
80
|
+
{ id: 'week', label: 'Last 7d', query: { since: '7d', isMeta: false } },
|
|
81
|
+
{ id: 'errors', label: 'Errors', query: { since: '7d', isError: true } },
|
|
82
|
+
{ id: 'tools', label: 'Tool calls', query: { since: '24h', type: 'tool_use' } },
|
|
83
|
+
{ id: 'user-turns', label: 'User turns', query: { since: '7d', role: 'user', type: 'text', isMeta: false } },
|
|
84
|
+
{ id: 'assistant', label: 'Assistant text', query: { since: '24h', role: 'assistant', type: 'text' } },
|
|
85
|
+
{ id: 'subagents', label: 'Subagents', query: { since: '7d', isSubagent: true } },
|
|
86
|
+
{ id: 'all', label: 'All time', query: {} },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_ACTIVE = 'recent';
|
package/src/gui-server.js
CHANGED
|
@@ -4,9 +4,12 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { JsonlReplayer, JsonlWatcher } from './index.js';
|
|
6
6
|
import { buildIndex, search, snippet, tokenize } from './bm25.js';
|
|
7
|
+
import { DEFAULT_PRESETS, DEFAULT_ACTIVE } from './filters.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const GUI_DIR = path.join(__dirname, '..', 'gui');
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_FILTERS = { active: DEFAULT_ACTIVE, presets: DEFAULT_PRESETS };
|
|
10
13
|
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml' };
|
|
11
14
|
|
|
12
15
|
function blockText(b) {
|
|
@@ -34,6 +37,7 @@ function flattenEvent(ev, idx) {
|
|
|
34
37
|
tool: b.name || null,
|
|
35
38
|
text: blockText(b),
|
|
36
39
|
isError: !!b.is_error || ev.role === 'streaming_error',
|
|
40
|
+
isMeta: !!b.isMeta,
|
|
37
41
|
cost: b.total_cost_usd || null,
|
|
38
42
|
duration: b.duration_ms || null,
|
|
39
43
|
subtype: b.subtype || null,
|
|
@@ -223,20 +227,42 @@ class Store {
|
|
|
223
227
|
return out;
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
events_filtered({ role, type, project, sid, tool, since, until, limit = 200, offset = 0, q } = {}) {
|
|
230
|
+
events_filtered({ role, type, project, sid, tool, since, until, limit = 200, offset = 0, q, grep, igrep, isMeta, isSubagent, isError, parent } = {}) {
|
|
227
231
|
let arr = this.events;
|
|
232
|
+
let greRe = null, igreRe = null;
|
|
233
|
+
try {
|
|
234
|
+
if (grep) greRe = new RegExp(grep, 'i');
|
|
235
|
+
if (igrep) igreRe = new RegExp(igrep, 'i');
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return { total: 0, rows: [], error: `invalid regex: ${e.message}` };
|
|
238
|
+
}
|
|
228
239
|
if (q) {
|
|
229
|
-
const tokens = new Set(tokenize(q));
|
|
230
|
-
|
|
240
|
+
const tokens = [...new Set(tokenize(q))];
|
|
241
|
+
if (tokens.length) {
|
|
242
|
+
arr = arr.filter(e => { const t = tokenize(e.text); return tokens.every(x => t.includes(x)); });
|
|
243
|
+
} else {
|
|
244
|
+
// q tokenized to nothing (too short / stopwords) → substring fallback
|
|
245
|
+
const needle = String(q).toLowerCase();
|
|
246
|
+
arr = arr.filter(e => (e.text || '').toLowerCase().includes(needle));
|
|
247
|
+
}
|
|
231
248
|
}
|
|
232
249
|
arr = arr.filter(e => {
|
|
233
250
|
if (role && e.role !== role) return false;
|
|
234
251
|
if (type && e.type !== type) return false;
|
|
235
252
|
if (project && e.project !== project) return false;
|
|
236
253
|
if (sid && !e.sid.startsWith(sid)) return false;
|
|
254
|
+
if (parent && e.parent !== parent) return false;
|
|
237
255
|
if (tool && e.tool !== tool) return false;
|
|
238
256
|
if (since && e.ts < since) return false;
|
|
239
257
|
if (until && e.ts > until) return false;
|
|
258
|
+
if (isMeta === true && !e.isMeta) return false;
|
|
259
|
+
if (isMeta === false && e.isMeta) return false;
|
|
260
|
+
if (isSubagent === true && !e.isSubagent) return false;
|
|
261
|
+
if (isSubagent === false && e.isSubagent) return false;
|
|
262
|
+
if (isError === true && !e.isError) return false;
|
|
263
|
+
if (isError === false && e.isError) return false;
|
|
264
|
+
if (greRe && !greRe.test(e.text || '')) return false;
|
|
265
|
+
if (igreRe && igreRe.test(e.text || '')) return false;
|
|
240
266
|
return true;
|
|
241
267
|
});
|
|
242
268
|
return { total: arr.length, rows: arr.slice(offset, offset + limit) };
|
|
@@ -260,14 +286,36 @@ function serveStatic(req, res) {
|
|
|
260
286
|
});
|
|
261
287
|
}
|
|
262
288
|
|
|
289
|
+
function parseRelTime(s) {
|
|
290
|
+
if (s === null || s === undefined || s === '') return 0;
|
|
291
|
+
const str = String(s).trim();
|
|
292
|
+
if (/^\d{10,}$/.test(str)) return parseInt(str, 10);
|
|
293
|
+
const m = /^(\d+)\s*([smhdw])$/i.exec(str);
|
|
294
|
+
if (m) {
|
|
295
|
+
const n = parseInt(m[1], 10);
|
|
296
|
+
const mult = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }[m[2].toLowerCase()];
|
|
297
|
+
return Date.now() - n * mult;
|
|
298
|
+
}
|
|
299
|
+
const t = Date.parse(str);
|
|
300
|
+
return Number.isFinite(t) ? t : 0;
|
|
301
|
+
}
|
|
302
|
+
function parseBool(v) {
|
|
303
|
+
if (v === undefined) return undefined;
|
|
304
|
+
if (v === 'true' || v === '1') return true;
|
|
305
|
+
if (v === 'false' || v === '0') return false;
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
263
308
|
function parseQuery(u) {
|
|
264
309
|
const q = {};
|
|
265
310
|
for (const [k, v] of u.searchParams) q[k] = v;
|
|
266
|
-
if (q.limit) q.limit = parseInt(q.limit, 10);
|
|
267
|
-
if (q.offset) q.offset = parseInt(q.offset, 10);
|
|
268
|
-
if (q.since) q.since =
|
|
269
|
-
if (q.until) q.until =
|
|
270
|
-
if (q.bucket) q.bucket = parseInt(q.bucket, 10);
|
|
311
|
+
if (q.limit !== undefined) q.limit = parseInt(q.limit, 10) || 200;
|
|
312
|
+
if (q.offset !== undefined) q.offset = parseInt(q.offset, 10) || 0;
|
|
313
|
+
if (q.since !== undefined) q.since = parseRelTime(q.since);
|
|
314
|
+
if (q.until !== undefined) q.until = parseRelTime(q.until);
|
|
315
|
+
if (q.bucket !== undefined) q.bucket = parseInt(q.bucket, 10) || 0;
|
|
316
|
+
for (const k of ['isMeta', 'isSubagent', 'isError']) {
|
|
317
|
+
if (q[k] !== undefined) q[k] = parseBool(q[k]);
|
|
318
|
+
}
|
|
271
319
|
return q;
|
|
272
320
|
}
|
|
273
321
|
|
|
@@ -291,6 +339,7 @@ export function createServer({ projectsDir, port = 0, host = '127.0.0.1' } = {})
|
|
|
291
339
|
if (path === '/api/errors') return send(res, 200, store.errorsList());
|
|
292
340
|
if (path === '/api/subagents') return send(res, 200, store.subagents());
|
|
293
341
|
if (path === '/api/events') return send(res, 200, store.events_filtered(q));
|
|
342
|
+
if (path === '/api/defaults') return send(res, 200, DEFAULT_FILTERS);
|
|
294
343
|
if (path === '/api/search') return send(res, 200, { query: q.q || '', results: q.q ? store.search(q.q, q) : [] });
|
|
295
344
|
if (path === '/api/reindex') { store.rebuildIndex(); return send(res, 200, { ok: true, at: store.lastBuilt }); }
|
|
296
345
|
if (path === '/api/stream') {
|
package/src/unsloth.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
const SYS_REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
|
|
2
|
+
const CC_ENVELOPE = /<\/?(command-name|command-message|command-args|local-command-stdout|local-command-stderr|user-prompt-submit-hook|stdin)[^>]*>/gi;
|
|
3
|
+
|
|
4
|
+
function sanitize(s) {
|
|
5
|
+
if (typeof s !== 'string' || !s) return s || '';
|
|
6
|
+
return s.replace(SYS_REMINDER, '').replace(CC_ENVELOPE, '').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
function textOf(b) {
|
|
2
|
-
|
|
3
|
-
if (typeof b.
|
|
4
|
-
if (
|
|
5
|
-
|
|
10
|
+
let raw = '';
|
|
11
|
+
if (typeof b.text === 'string') raw = b.text;
|
|
12
|
+
else if (typeof b.content === 'string') raw = b.content;
|
|
13
|
+
else if (Array.isArray(b.content)) raw = b.content.map(c => c?.text || '').join('');
|
|
14
|
+
return sanitize(raw);
|
|
6
15
|
}
|
|
7
16
|
|
|
8
17
|
function groupBySession(events) {
|