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 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 LiveView() {
148
- return C.Panel({ head: `live · ${state.liveLog.length} events (last 200)`, children: h('div', { class: 'live' },
149
- ...state.liveLog.slice().reverse().map((e, i) => h('div', { key: i, class: 'e' },
150
- h('span', { class: 'ts' }, ts(e.ts || Date.now()).slice(11)),
151
- h('span', { class: 'k' }, e._kind || 'event'),
152
- h('span', {}, fmtLive(e)),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
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,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> inverted text regex; repeat = AND (none must match)
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 since = parseTime(opts.since || opts.after);
246
- const filter = buildFilter(opts);
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
- arr = arr.filter(e => { const t = tokenize(e.text); return [...tokens].every(x => t.includes(x)); });
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 = parseInt(q.since, 10);
269
- if (q.until) q.until = parseInt(q.until, 10);
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
- if (typeof b.text === 'string') return b.text;
3
- if (typeof b.content === 'string') return b.content;
4
- if (Array.isArray(b.content)) return b.content.map(c => c?.text || '').join('');
5
- return '';
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) {