ccsniff 1.0.32 → 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/gui/app.js +83 -9
- package/package.json +1 -1
- package/src/cli.js +10 -58
- package/src/filters.js +89 -0
- package/src/gui-server.js +57 -8
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
|
|
|
@@ -69,7 +70,7 @@ TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
|
|
|
69
70
|
|
|
70
71
|
FILTERS (repeatable flags combine as OR within a flag, AND across flags)
|
|
71
72
|
--grep <re> text regex (case-insensitive); repeat = AND
|
|
72
|
-
--igrep <re>
|
|
73
|
+
--igrep <re> exclude if regex matches text; repeat = exclude if ANY matches
|
|
73
74
|
--invert invert the entire filter result
|
|
74
75
|
--cwd <re> working-dir regex
|
|
75
76
|
--project <name> basename(cwd) exact match; repeat = OR
|
|
@@ -114,21 +115,6 @@ EXAMPLES
|
|
|
114
115
|
`);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function parseTime(s) {
|
|
118
|
-
if (!s) return 0;
|
|
119
|
-
if (/^\d{10,}$/.test(s)) return parseInt(s, 10);
|
|
120
|
-
const m = /^(\d+)([smhdw])$/.exec(s);
|
|
121
|
-
if (m) {
|
|
122
|
-
const n = parseInt(m[1], 10);
|
|
123
|
-
const mult = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }[m[2]];
|
|
124
|
-
return Date.now() - n * mult;
|
|
125
|
-
}
|
|
126
|
-
const t = Date.parse(s);
|
|
127
|
-
return Number.isFinite(t) ? t : 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function compileRegexes(arr) { return arr.map(s => new RegExp(s, 'i')); }
|
|
131
|
-
|
|
132
118
|
function blockText(b) {
|
|
133
119
|
if (!b) return '';
|
|
134
120
|
if (typeof b.text === 'string') return b.text;
|
|
@@ -138,46 +124,6 @@ function blockText(b) {
|
|
|
138
124
|
return '';
|
|
139
125
|
}
|
|
140
126
|
|
|
141
|
-
function buildFilter(opts) {
|
|
142
|
-
const since = parseTime(opts.since || opts.after);
|
|
143
|
-
const until = parseTime(opts.until || opts.before);
|
|
144
|
-
const greps = compileRegexes(opts._multi.grep);
|
|
145
|
-
const igreps = compileRegexes(opts._multi.igrep);
|
|
146
|
-
const cwdRes = compileRegexes(opts._multi.cwd);
|
|
147
|
-
const projects = new Set(opts._multi.project);
|
|
148
|
-
const roles = new Set(opts._multi.role);
|
|
149
|
-
const types = new Set(opts._multi.type);
|
|
150
|
-
const tools = new Set(opts._multi.tool);
|
|
151
|
-
const sids = opts._multi.session.concat(opts._multi.sid || []);
|
|
152
|
-
const parent = opts.parent || null;
|
|
153
|
-
|
|
154
|
-
return ev => {
|
|
155
|
-
const conv = ev.conversation || {};
|
|
156
|
-
const block = ev.block || {};
|
|
157
|
-
const ts = ev.timestamp || 0;
|
|
158
|
-
let pass = true;
|
|
159
|
-
if (since && ts < since) pass = false;
|
|
160
|
-
else if (until && ts > until) pass = false;
|
|
161
|
-
else if (cwdRes.length && !cwdRes.every(r => r.test(conv.cwd || ''))) pass = false;
|
|
162
|
-
else if (projects.size && !projects.has(path.basename(conv.cwd || ''))) pass = false;
|
|
163
|
-
else if (roles.size && !roles.has(ev.role)) pass = false;
|
|
164
|
-
else if (types.size && !types.has(block.type)) pass = false;
|
|
165
|
-
else if (tools.size && !tools.has(block.name)) pass = false;
|
|
166
|
-
else if (sids.length && !sids.some(s => conv.id?.startsWith(s))) pass = false;
|
|
167
|
-
else if (parent && conv.parentSid !== parent) pass = false;
|
|
168
|
-
else if (opts['no-subagents'] && conv.isSubagent) pass = false;
|
|
169
|
-
else if (opts['only-subagents'] && !conv.isSubagent) pass = false;
|
|
170
|
-
else if (opts['no-meta'] && block.isMeta) pass = false;
|
|
171
|
-
else if (opts['only-meta'] && !block.isMeta) pass = false;
|
|
172
|
-
else {
|
|
173
|
-
const text = blockText(block);
|
|
174
|
-
if (greps.length && !greps.every(r => r.test(text))) pass = false;
|
|
175
|
-
else if (igreps.length && igreps.some(r => r.test(text))) pass = false;
|
|
176
|
-
}
|
|
177
|
-
return opts.invert ? !pass : pass;
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
127
|
function formatRow(ev, opts) {
|
|
182
128
|
const conv = ev.conversation || {};
|
|
183
129
|
const block = ev.block || {};
|
|
@@ -242,8 +188,14 @@ if (opts.help || process.argv.length <= 2) { printHelp(); process.exit(0); }
|
|
|
242
188
|
|
|
243
189
|
{ const r = vault(); if (r.copied > 0) process.stderr.write(`# vault: ${r.copied} copied → ~/.claude/history-backup\n`); }
|
|
244
190
|
|
|
245
|
-
|
|
246
|
-
|
|
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') {
|