@os-eco/overstory-cli 0.6.1
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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed event store for agent activity observability.
|
|
3
|
+
*
|
|
4
|
+
* Tracks tool invocations, session lifecycle, mail events, and errors.
|
|
5
|
+
* Uses bun:sqlite for zero-dependency, synchronous database access.
|
|
6
|
+
* WAL mode enables concurrent reads from multiple agent processes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import type {
|
|
11
|
+
EventLevel,
|
|
12
|
+
EventQueryOptions,
|
|
13
|
+
EventStore,
|
|
14
|
+
InsertEvent,
|
|
15
|
+
StoredEvent,
|
|
16
|
+
ToolStats,
|
|
17
|
+
} from "../types.ts";
|
|
18
|
+
|
|
19
|
+
/** Row shape as stored in SQLite (snake_case columns). */
|
|
20
|
+
interface EventRow {
|
|
21
|
+
id: number;
|
|
22
|
+
run_id: string | null;
|
|
23
|
+
agent_name: string;
|
|
24
|
+
session_id: string | null;
|
|
25
|
+
event_type: string;
|
|
26
|
+
tool_name: string | null;
|
|
27
|
+
tool_args: string | null;
|
|
28
|
+
tool_duration_ms: number | null;
|
|
29
|
+
level: string;
|
|
30
|
+
data: string | null;
|
|
31
|
+
created_at: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CREATE_TABLE = `
|
|
35
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
run_id TEXT,
|
|
38
|
+
agent_name TEXT NOT NULL,
|
|
39
|
+
session_id TEXT,
|
|
40
|
+
event_type TEXT NOT NULL,
|
|
41
|
+
tool_name TEXT,
|
|
42
|
+
tool_args TEXT,
|
|
43
|
+
tool_duration_ms INTEGER,
|
|
44
|
+
level TEXT NOT NULL DEFAULT 'info' CHECK(level IN ('debug','info','warn','error')),
|
|
45
|
+
data TEXT,
|
|
46
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
|
|
47
|
+
)`;
|
|
48
|
+
|
|
49
|
+
const CREATE_INDEXES = `
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent_time ON events(agent_name, created_at);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_events_run_time ON events(run_id, created_at);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(event_type, created_at);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_events_tool_agent ON events(tool_name, agent_name);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_events_level_error ON events(level) WHERE level = 'error'`;
|
|
55
|
+
|
|
56
|
+
/** Convert a database row (snake_case) to a StoredEvent object (camelCase). */
|
|
57
|
+
function rowToEvent(row: EventRow): StoredEvent {
|
|
58
|
+
return {
|
|
59
|
+
id: row.id,
|
|
60
|
+
runId: row.run_id,
|
|
61
|
+
agentName: row.agent_name,
|
|
62
|
+
sessionId: row.session_id,
|
|
63
|
+
eventType: row.event_type as StoredEvent["eventType"],
|
|
64
|
+
toolName: row.tool_name,
|
|
65
|
+
toolArgs: row.tool_args,
|
|
66
|
+
toolDurationMs: row.tool_duration_ms,
|
|
67
|
+
level: row.level as EventLevel,
|
|
68
|
+
data: row.data,
|
|
69
|
+
createdAt: row.created_at,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build WHERE clause fragments and params from EventQueryOptions. */
|
|
74
|
+
function buildFilterClauses(
|
|
75
|
+
opts: EventQueryOptions | undefined,
|
|
76
|
+
existingConditions: string[] = [],
|
|
77
|
+
existingParams: Record<string, string | number> = {},
|
|
78
|
+
): { whereClause: string; params: Record<string, string | number>; limitClause: string } {
|
|
79
|
+
const conditions = [...existingConditions];
|
|
80
|
+
const params = { ...existingParams };
|
|
81
|
+
|
|
82
|
+
if (opts?.since !== undefined) {
|
|
83
|
+
conditions.push("created_at >= $since");
|
|
84
|
+
params.$since = opts.since;
|
|
85
|
+
}
|
|
86
|
+
if (opts?.until !== undefined) {
|
|
87
|
+
conditions.push("created_at <= $until");
|
|
88
|
+
params.$until = opts.until;
|
|
89
|
+
}
|
|
90
|
+
if (opts?.level !== undefined) {
|
|
91
|
+
conditions.push("level = $level");
|
|
92
|
+
params.$level = opts.level;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
96
|
+
const limitClause = opts?.limit !== undefined ? `LIMIT ${opts.limit}` : "";
|
|
97
|
+
|
|
98
|
+
return { whereClause, params, limitClause };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a new EventStore backed by a SQLite database at the given path.
|
|
103
|
+
*
|
|
104
|
+
* Initializes the database with WAL mode and a 5-second busy timeout.
|
|
105
|
+
* Creates the events table and indexes if they do not already exist.
|
|
106
|
+
*/
|
|
107
|
+
export function createEventStore(dbPath: string): EventStore {
|
|
108
|
+
const db = new Database(dbPath);
|
|
109
|
+
|
|
110
|
+
// Configure for concurrent access from multiple agent processes.
|
|
111
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
112
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
113
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
114
|
+
|
|
115
|
+
// Create schema
|
|
116
|
+
db.exec(CREATE_TABLE);
|
|
117
|
+
db.exec(CREATE_INDEXES);
|
|
118
|
+
|
|
119
|
+
// Prepare the insert statement
|
|
120
|
+
const insertStmt = db.prepare<
|
|
121
|
+
{ id: number },
|
|
122
|
+
{
|
|
123
|
+
$run_id: string | null;
|
|
124
|
+
$agent_name: string;
|
|
125
|
+
$session_id: string | null;
|
|
126
|
+
$event_type: string;
|
|
127
|
+
$tool_name: string | null;
|
|
128
|
+
$tool_args: string | null;
|
|
129
|
+
$tool_duration_ms: number | null;
|
|
130
|
+
$level: string;
|
|
131
|
+
$data: string | null;
|
|
132
|
+
}
|
|
133
|
+
>(`
|
|
134
|
+
INSERT INTO events
|
|
135
|
+
(run_id, agent_name, session_id, event_type, tool_name, tool_args, tool_duration_ms, level, data)
|
|
136
|
+
VALUES
|
|
137
|
+
($run_id, $agent_name, $session_id, $event_type, $tool_name, $tool_args, $tool_duration_ms, $level, $data)
|
|
138
|
+
RETURNING id
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
// Prepare correlateToolEnd: find the most recent tool_start for this agent+tool
|
|
142
|
+
// that has no corresponding tool_end yet (no tool_duration_ms set).
|
|
143
|
+
const correlateStmt = db.prepare<
|
|
144
|
+
{ id: number; created_at: string },
|
|
145
|
+
{ $agent_name: string; $tool_name: string }
|
|
146
|
+
>(`
|
|
147
|
+
SELECT id, created_at FROM events
|
|
148
|
+
WHERE agent_name = $agent_name
|
|
149
|
+
AND tool_name = $tool_name
|
|
150
|
+
AND event_type = 'tool_start'
|
|
151
|
+
AND tool_duration_ms IS NULL
|
|
152
|
+
ORDER BY created_at DESC
|
|
153
|
+
LIMIT 1
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
const updateDurationStmt = db.prepare<void, { $id: number; $duration_ms: number }>(`
|
|
157
|
+
UPDATE events SET tool_duration_ms = $duration_ms WHERE id = $id
|
|
158
|
+
`);
|
|
159
|
+
|
|
160
|
+
// Prepare getByAgent
|
|
161
|
+
const byAgentStmt = db.prepare<EventRow, { $agent_name: string }>(`
|
|
162
|
+
SELECT * FROM events WHERE agent_name = $agent_name ORDER BY created_at ASC
|
|
163
|
+
`);
|
|
164
|
+
|
|
165
|
+
// Prepare getByRun
|
|
166
|
+
const byRunStmt = db.prepare<EventRow, { $run_id: string }>(`
|
|
167
|
+
SELECT * FROM events WHERE run_id = $run_id ORDER BY created_at ASC
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
insert(event: InsertEvent): number {
|
|
172
|
+
const row = insertStmt.get({
|
|
173
|
+
$run_id: event.runId,
|
|
174
|
+
$agent_name: event.agentName,
|
|
175
|
+
$session_id: event.sessionId,
|
|
176
|
+
$event_type: event.eventType,
|
|
177
|
+
$tool_name: event.toolName,
|
|
178
|
+
$tool_args: event.toolArgs,
|
|
179
|
+
$tool_duration_ms: event.toolDurationMs,
|
|
180
|
+
$level: event.level,
|
|
181
|
+
$data: event.data,
|
|
182
|
+
});
|
|
183
|
+
// RETURNING id always returns a row for INSERT; if somehow null, fallback to 0
|
|
184
|
+
if (!row) {
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
return row.id;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
correlateToolEnd(
|
|
191
|
+
agentName: string,
|
|
192
|
+
toolName: string,
|
|
193
|
+
): { startId: number; durationMs: number } | null {
|
|
194
|
+
const startRow = correlateStmt.get({
|
|
195
|
+
$agent_name: agentName,
|
|
196
|
+
$tool_name: toolName,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!startRow) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const startTime = new Date(startRow.created_at).getTime();
|
|
204
|
+
const durationMs = Date.now() - startTime;
|
|
205
|
+
|
|
206
|
+
// Mark the start event with the computed duration
|
|
207
|
+
updateDurationStmt.run({
|
|
208
|
+
$id: startRow.id,
|
|
209
|
+
$duration_ms: durationMs,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return { startId: startRow.id, durationMs };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
getByAgent(agentName: string, opts?: EventQueryOptions): StoredEvent[] {
|
|
216
|
+
if (
|
|
217
|
+
opts !== undefined &&
|
|
218
|
+
(opts.since !== undefined ||
|
|
219
|
+
opts.until !== undefined ||
|
|
220
|
+
opts.level !== undefined ||
|
|
221
|
+
opts.limit !== undefined)
|
|
222
|
+
) {
|
|
223
|
+
// Use dynamic query with filters
|
|
224
|
+
const { whereClause, params, limitClause } = buildFilterClauses(
|
|
225
|
+
opts,
|
|
226
|
+
["agent_name = $agent_name"],
|
|
227
|
+
{ $agent_name: agentName },
|
|
228
|
+
);
|
|
229
|
+
const query = `SELECT * FROM events ${whereClause} ORDER BY created_at ASC ${limitClause}`;
|
|
230
|
+
const rows = db.prepare<EventRow, Record<string, string | number>>(query).all(params);
|
|
231
|
+
return rows.map(rowToEvent);
|
|
232
|
+
}
|
|
233
|
+
const rows = byAgentStmt.all({ $agent_name: agentName });
|
|
234
|
+
return rows.map(rowToEvent);
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
getByRun(runId: string, opts?: EventQueryOptions): StoredEvent[] {
|
|
238
|
+
if (
|
|
239
|
+
opts !== undefined &&
|
|
240
|
+
(opts.since !== undefined ||
|
|
241
|
+
opts.until !== undefined ||
|
|
242
|
+
opts.level !== undefined ||
|
|
243
|
+
opts.limit !== undefined)
|
|
244
|
+
) {
|
|
245
|
+
const { whereClause, params, limitClause } = buildFilterClauses(
|
|
246
|
+
opts,
|
|
247
|
+
["run_id = $run_id"],
|
|
248
|
+
{ $run_id: runId },
|
|
249
|
+
);
|
|
250
|
+
const query = `SELECT * FROM events ${whereClause} ORDER BY created_at ASC ${limitClause}`;
|
|
251
|
+
const rows = db.prepare<EventRow, Record<string, string | number>>(query).all(params);
|
|
252
|
+
return rows.map(rowToEvent);
|
|
253
|
+
}
|
|
254
|
+
const rows = byRunStmt.all({ $run_id: runId });
|
|
255
|
+
return rows.map(rowToEvent);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
getErrors(opts?: EventQueryOptions): StoredEvent[] {
|
|
259
|
+
const { whereClause, params, limitClause } = buildFilterClauses(opts, ["level = 'error'"]);
|
|
260
|
+
const query = `SELECT * FROM events ${whereClause} ORDER BY created_at DESC ${limitClause}`;
|
|
261
|
+
const rows = db.prepare<EventRow, Record<string, string | number>>(query).all(params);
|
|
262
|
+
return rows.map(rowToEvent);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
getTimeline(opts: EventQueryOptions & { since: string }): StoredEvent[] {
|
|
266
|
+
const { whereClause, params, limitClause } = buildFilterClauses(opts);
|
|
267
|
+
const query = `SELECT * FROM events ${whereClause} ORDER BY created_at ASC ${limitClause}`;
|
|
268
|
+
const rows = db.prepare<EventRow, Record<string, string | number>>(query).all(params);
|
|
269
|
+
return rows.map(rowToEvent);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
getToolStats(opts?: { agentName?: string; since?: string }): ToolStats[] {
|
|
273
|
+
const conditions: string[] = ["tool_name IS NOT NULL", "event_type = 'tool_start'"];
|
|
274
|
+
const params: Record<string, string> = {};
|
|
275
|
+
|
|
276
|
+
if (opts?.agentName !== undefined) {
|
|
277
|
+
conditions.push("agent_name = $agent_name");
|
|
278
|
+
params.$agent_name = opts.agentName;
|
|
279
|
+
}
|
|
280
|
+
if (opts?.since !== undefined) {
|
|
281
|
+
conditions.push("created_at >= $since");
|
|
282
|
+
params.$since = opts.since;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
286
|
+
const query = `
|
|
287
|
+
SELECT
|
|
288
|
+
tool_name,
|
|
289
|
+
COUNT(*) AS count,
|
|
290
|
+
COALESCE(AVG(tool_duration_ms), 0) AS avg_duration_ms,
|
|
291
|
+
COALESCE(MAX(tool_duration_ms), 0) AS max_duration_ms
|
|
292
|
+
FROM events
|
|
293
|
+
${whereClause}
|
|
294
|
+
GROUP BY tool_name
|
|
295
|
+
ORDER BY count DESC
|
|
296
|
+
`;
|
|
297
|
+
const rows = db
|
|
298
|
+
.prepare<
|
|
299
|
+
{
|
|
300
|
+
tool_name: string;
|
|
301
|
+
count: number;
|
|
302
|
+
avg_duration_ms: number;
|
|
303
|
+
max_duration_ms: number;
|
|
304
|
+
},
|
|
305
|
+
Record<string, string>
|
|
306
|
+
>(query)
|
|
307
|
+
.all(params);
|
|
308
|
+
|
|
309
|
+
return rows.map((row) => ({
|
|
310
|
+
toolName: row.tool_name,
|
|
311
|
+
count: row.count,
|
|
312
|
+
avgDurationMs: row.avg_duration_ms,
|
|
313
|
+
maxDurationMs: row.max_duration_ms,
|
|
314
|
+
}));
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
purge(opts: { all?: boolean; olderThanMs?: number; agentName?: string }): number {
|
|
318
|
+
if (opts.all) {
|
|
319
|
+
const countRow = db
|
|
320
|
+
.prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM events")
|
|
321
|
+
.get();
|
|
322
|
+
const count = countRow?.cnt ?? 0;
|
|
323
|
+
db.prepare("DELETE FROM events").run();
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const conditions: string[] = [];
|
|
328
|
+
const params: Record<string, string> = {};
|
|
329
|
+
|
|
330
|
+
if (opts.olderThanMs !== undefined) {
|
|
331
|
+
const cutoff = new Date(Date.now() - opts.olderThanMs).toISOString();
|
|
332
|
+
conditions.push("created_at < $cutoff");
|
|
333
|
+
params.$cutoff = cutoff;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (opts.agentName !== undefined) {
|
|
337
|
+
conditions.push("agent_name = $agent_name");
|
|
338
|
+
params.$agent_name = opts.agentName;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (conditions.length === 0) {
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const whereClause = conditions.join(" AND ");
|
|
346
|
+
const countRow = db
|
|
347
|
+
.prepare<{ cnt: number }, Record<string, string>>(
|
|
348
|
+
`SELECT COUNT(*) as cnt FROM events WHERE ${whereClause}`,
|
|
349
|
+
)
|
|
350
|
+
.get(params);
|
|
351
|
+
const count = countRow?.cnt ?? 0;
|
|
352
|
+
|
|
353
|
+
db.prepare<void, Record<string, string>>(`DELETE FROM events WHERE ${whereClause}`).run(
|
|
354
|
+
params,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return count;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
close(): void {
|
|
361
|
+
try {
|
|
362
|
+
db.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
363
|
+
} catch {
|
|
364
|
+
// Best effort -- checkpoint failure is non-fatal
|
|
365
|
+
}
|
|
366
|
+
db.close();
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { filterToolArgs } from "./tool-filter.ts";
|
|
3
|
+
|
|
4
|
+
describe("filterToolArgs", () => {
|
|
5
|
+
describe("Bash", () => {
|
|
6
|
+
test("keeps command and description, drops timeout/run_in_background/dangerouslyDisableSandbox", () => {
|
|
7
|
+
const result = filterToolArgs("Bash", {
|
|
8
|
+
command: "bun test",
|
|
9
|
+
description: "Run tests",
|
|
10
|
+
timeout: 60000,
|
|
11
|
+
run_in_background: true,
|
|
12
|
+
dangerouslyDisableSandbox: false,
|
|
13
|
+
});
|
|
14
|
+
expect(result.args).toEqual({
|
|
15
|
+
command: "bun test",
|
|
16
|
+
description: "Run tests",
|
|
17
|
+
});
|
|
18
|
+
expect(result.args).not.toHaveProperty("timeout");
|
|
19
|
+
expect(result.args).not.toHaveProperty("run_in_background");
|
|
20
|
+
expect(result.args).not.toHaveProperty("dangerouslyDisableSandbox");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("summary shows first 80 chars of command", () => {
|
|
24
|
+
const result = filterToolArgs("Bash", { command: "bun test" });
|
|
25
|
+
expect(result.summary).toBe("bash: bun test");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("summary truncates long commands at 80 chars", () => {
|
|
29
|
+
const longCmd = "a".repeat(120);
|
|
30
|
+
const result = filterToolArgs("Bash", { command: longCmd });
|
|
31
|
+
expect(result.summary).toBe(`bash: ${"a".repeat(80)}...`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("handles missing command gracefully", () => {
|
|
35
|
+
const result = filterToolArgs("Bash", {});
|
|
36
|
+
expect(result.args).toEqual({});
|
|
37
|
+
expect(result.summary).toBe("bash: ");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Read", () => {
|
|
42
|
+
test("keeps file_path, offset, limit", () => {
|
|
43
|
+
const result = filterToolArgs("Read", {
|
|
44
|
+
file_path: "/src/index.ts",
|
|
45
|
+
offset: 10,
|
|
46
|
+
limit: 50,
|
|
47
|
+
});
|
|
48
|
+
expect(result.args).toEqual({
|
|
49
|
+
file_path: "/src/index.ts",
|
|
50
|
+
offset: 10,
|
|
51
|
+
limit: 50,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("summary with offset and limit shows line range", () => {
|
|
56
|
+
const result = filterToolArgs("Read", {
|
|
57
|
+
file_path: "/src/index.ts",
|
|
58
|
+
offset: 10,
|
|
59
|
+
limit: 50,
|
|
60
|
+
});
|
|
61
|
+
expect(result.summary).toBe("read: /src/index.ts (lines 10-60)");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("summary with only offset shows from line", () => {
|
|
65
|
+
const result = filterToolArgs("Read", {
|
|
66
|
+
file_path: "/src/index.ts",
|
|
67
|
+
offset: 10,
|
|
68
|
+
});
|
|
69
|
+
expect(result.summary).toBe("read: /src/index.ts (from line 10)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("summary with only limit shows first N lines", () => {
|
|
73
|
+
const result = filterToolArgs("Read", {
|
|
74
|
+
file_path: "/src/index.ts",
|
|
75
|
+
limit: 50,
|
|
76
|
+
});
|
|
77
|
+
expect(result.summary).toBe("read: /src/index.ts (first 50 lines)");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("summary without offset or limit shows just path", () => {
|
|
81
|
+
const result = filterToolArgs("Read", {
|
|
82
|
+
file_path: "/src/index.ts",
|
|
83
|
+
});
|
|
84
|
+
expect(result.summary).toBe("read: /src/index.ts");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("handles missing file_path", () => {
|
|
88
|
+
const result = filterToolArgs("Read", {});
|
|
89
|
+
expect(result.summary).toBe("read: ");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("Write", () => {
|
|
94
|
+
test("keeps file_path, drops content", () => {
|
|
95
|
+
const result = filterToolArgs("Write", {
|
|
96
|
+
file_path: "/src/index.ts",
|
|
97
|
+
content: "const x = 1;\nconst y = 2;\n// lots of content...",
|
|
98
|
+
});
|
|
99
|
+
expect(result.args).toEqual({ file_path: "/src/index.ts" });
|
|
100
|
+
expect(result.args).not.toHaveProperty("content");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("summary shows file path", () => {
|
|
104
|
+
const result = filterToolArgs("Write", {
|
|
105
|
+
file_path: "/src/index.ts",
|
|
106
|
+
content: "stuff",
|
|
107
|
+
});
|
|
108
|
+
expect(result.summary).toBe("write: /src/index.ts");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("handles missing file_path", () => {
|
|
112
|
+
const result = filterToolArgs("Write", { content: "data" });
|
|
113
|
+
expect(result.args).toEqual({});
|
|
114
|
+
expect(result.summary).toBe("write: ");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("Edit", () => {
|
|
119
|
+
test("keeps file_path, drops old_string and new_string", () => {
|
|
120
|
+
const result = filterToolArgs("Edit", {
|
|
121
|
+
file_path: "/src/config.ts",
|
|
122
|
+
old_string: "const x = 1;",
|
|
123
|
+
new_string: "const x = 2;",
|
|
124
|
+
});
|
|
125
|
+
expect(result.args).toEqual({ file_path: "/src/config.ts" });
|
|
126
|
+
expect(result.args).not.toHaveProperty("old_string");
|
|
127
|
+
expect(result.args).not.toHaveProperty("new_string");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("summary shows file path", () => {
|
|
131
|
+
const result = filterToolArgs("Edit", {
|
|
132
|
+
file_path: "/src/config.ts",
|
|
133
|
+
old_string: "a",
|
|
134
|
+
new_string: "b",
|
|
135
|
+
});
|
|
136
|
+
expect(result.summary).toBe("edit: /src/config.ts");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Glob", () => {
|
|
141
|
+
test("keeps pattern and path", () => {
|
|
142
|
+
const result = filterToolArgs("Glob", {
|
|
143
|
+
pattern: "**/*.ts",
|
|
144
|
+
path: "/src",
|
|
145
|
+
});
|
|
146
|
+
expect(result.args).toEqual({ pattern: "**/*.ts", path: "/src" });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("summary with path shows pattern in path", () => {
|
|
150
|
+
const result = filterToolArgs("Glob", {
|
|
151
|
+
pattern: "**/*.ts",
|
|
152
|
+
path: "/src",
|
|
153
|
+
});
|
|
154
|
+
expect(result.summary).toBe("glob: **/*.ts in /src");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("summary without path shows only pattern", () => {
|
|
158
|
+
const result = filterToolArgs("Glob", { pattern: "**/*.ts" });
|
|
159
|
+
expect(result.summary).toBe("glob: **/*.ts");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("Grep", () => {
|
|
164
|
+
test("keeps pattern, path, glob, output_mode", () => {
|
|
165
|
+
const result = filterToolArgs("Grep", {
|
|
166
|
+
pattern: "function\\s+\\w+",
|
|
167
|
+
path: "/src",
|
|
168
|
+
glob: "*.ts",
|
|
169
|
+
output_mode: "content",
|
|
170
|
+
"-A": 3,
|
|
171
|
+
"-B": 2,
|
|
172
|
+
});
|
|
173
|
+
expect(result.args).toEqual({
|
|
174
|
+
pattern: "function\\s+\\w+",
|
|
175
|
+
path: "/src",
|
|
176
|
+
glob: "*.ts",
|
|
177
|
+
output_mode: "content",
|
|
178
|
+
});
|
|
179
|
+
expect(result.args).not.toHaveProperty("-A");
|
|
180
|
+
expect(result.args).not.toHaveProperty("-B");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("summary with path shows pattern in path", () => {
|
|
184
|
+
const result = filterToolArgs("Grep", {
|
|
185
|
+
pattern: "TODO",
|
|
186
|
+
path: "/src",
|
|
187
|
+
});
|
|
188
|
+
expect(result.summary).toBe('grep: "TODO" in /src');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("summary without path shows only pattern", () => {
|
|
192
|
+
const result = filterToolArgs("Grep", { pattern: "TODO" });
|
|
193
|
+
expect(result.summary).toBe('grep: "TODO"');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("WebFetch", () => {
|
|
198
|
+
test("keeps url, drops prompt", () => {
|
|
199
|
+
const result = filterToolArgs("WebFetch", {
|
|
200
|
+
url: "https://example.com/page",
|
|
201
|
+
prompt: "Extract the main content from this page",
|
|
202
|
+
});
|
|
203
|
+
expect(result.args).toEqual({ url: "https://example.com/page" });
|
|
204
|
+
expect(result.args).not.toHaveProperty("prompt");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("summary shows url", () => {
|
|
208
|
+
const result = filterToolArgs("WebFetch", {
|
|
209
|
+
url: "https://example.com",
|
|
210
|
+
});
|
|
211
|
+
expect(result.summary).toBe("fetch: https://example.com");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("WebSearch", () => {
|
|
216
|
+
test("keeps query, drops domain filters", () => {
|
|
217
|
+
const result = filterToolArgs("WebSearch", {
|
|
218
|
+
query: "TypeScript strict mode",
|
|
219
|
+
allowed_domains: ["developer.mozilla.org"],
|
|
220
|
+
blocked_domains: ["w3schools.com"],
|
|
221
|
+
});
|
|
222
|
+
expect(result.args).toEqual({ query: "TypeScript strict mode" });
|
|
223
|
+
expect(result.args).not.toHaveProperty("allowed_domains");
|
|
224
|
+
expect(result.args).not.toHaveProperty("blocked_domains");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("summary shows query", () => {
|
|
228
|
+
const result = filterToolArgs("WebSearch", {
|
|
229
|
+
query: "bun test runner",
|
|
230
|
+
});
|
|
231
|
+
expect(result.summary).toBe("search: bun test runner");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Task", () => {
|
|
236
|
+
test("keeps description and subagent_type", () => {
|
|
237
|
+
const result = filterToolArgs("Task", {
|
|
238
|
+
description: "Analyze the codebase structure",
|
|
239
|
+
subagent_type: "research",
|
|
240
|
+
prompt: "Look at all the files and determine...",
|
|
241
|
+
});
|
|
242
|
+
expect(result.args).toEqual({
|
|
243
|
+
description: "Analyze the codebase structure",
|
|
244
|
+
subagent_type: "research",
|
|
245
|
+
});
|
|
246
|
+
expect(result.args).not.toHaveProperty("prompt");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("summary with subagent_type shows description and type", () => {
|
|
250
|
+
const result = filterToolArgs("Task", {
|
|
251
|
+
description: "Find all config files",
|
|
252
|
+
subagent_type: "research",
|
|
253
|
+
});
|
|
254
|
+
expect(result.summary).toBe("task: Find all config files (research)");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("summary without subagent_type shows only description", () => {
|
|
258
|
+
const result = filterToolArgs("Task", {
|
|
259
|
+
description: "Find all config files",
|
|
260
|
+
});
|
|
261
|
+
expect(result.summary).toBe("task: Find all config files");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("unknown tools", () => {
|
|
266
|
+
test("returns empty args and tool name as summary", () => {
|
|
267
|
+
const result = filterToolArgs("SomeUnknownTool", {
|
|
268
|
+
foo: "bar",
|
|
269
|
+
baz: 42,
|
|
270
|
+
});
|
|
271
|
+
expect(result.args).toEqual({});
|
|
272
|
+
expect(result.summary).toBe("SomeUnknownTool");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("handles empty input for unknown tool", () => {
|
|
276
|
+
const result = filterToolArgs("Mystery", {});
|
|
277
|
+
expect(result.args).toEqual({});
|
|
278
|
+
expect(result.summary).toBe("Mystery");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("edge cases", () => {
|
|
283
|
+
test("handles empty input object for known tool", () => {
|
|
284
|
+
const result = filterToolArgs("Bash", {});
|
|
285
|
+
expect(result.args).toEqual({});
|
|
286
|
+
expect(result.summary).toBe("bash: ");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("handles null values in input", () => {
|
|
290
|
+
const result = filterToolArgs("Read", {
|
|
291
|
+
file_path: null as unknown as string,
|
|
292
|
+
offset: null as unknown as number,
|
|
293
|
+
});
|
|
294
|
+
// null is not undefined, so file_path will be picked but summary treats it as non-string
|
|
295
|
+
expect(result.args).toHaveProperty("file_path");
|
|
296
|
+
expect(result.summary).toBe("read: ");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("handles undefined values in input", () => {
|
|
300
|
+
const result = filterToolArgs("Read", {
|
|
301
|
+
file_path: undefined as unknown as string,
|
|
302
|
+
});
|
|
303
|
+
// undefined values should not appear in filtered args
|
|
304
|
+
expect(result.args).not.toHaveProperty("file_path");
|
|
305
|
+
expect(result.summary).toBe("read: ");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("handles numeric values where strings expected", () => {
|
|
309
|
+
const result = filterToolArgs("Bash", {
|
|
310
|
+
command: 42 as unknown as string,
|
|
311
|
+
});
|
|
312
|
+
// Value is kept in args as-is, but summary treats non-strings as empty
|
|
313
|
+
expect(result.args).toEqual({ command: 42 });
|
|
314
|
+
expect(result.summary).toBe("bash: ");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("preserves exact 80-char command without truncation", () => {
|
|
318
|
+
const cmd = "x".repeat(80);
|
|
319
|
+
const result = filterToolArgs("Bash", { command: cmd });
|
|
320
|
+
expect(result.summary).toBe(`bash: ${cmd}`);
|
|
321
|
+
expect(result.summary).not.toContain("...");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("truncates 81-char command", () => {
|
|
325
|
+
const cmd = "y".repeat(81);
|
|
326
|
+
const result = filterToolArgs("Bash", { command: cmd });
|
|
327
|
+
expect(result.summary).toBe(`bash: ${"y".repeat(80)}...`);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|