@klars/agentobs 0.1.0 → 0.1.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/README.md +211 -190
- package/dist/core/queries.js +117 -42
- package/dist/server/index.js +4 -2
- package/dist/server/public/app.css +961 -668
- package/dist/server/public/app.js +647 -502
- package/dist/server/public/index.html +212 -196
- package/dist/server/public/logo.svg +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,190 +1,211 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
AgentObs
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[](https://www.npmjs.com/package/@klars/agentobs)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
|
|
13
|
+
[Website](https://agents.klars.ai) · [npm](https://www.npmjs.com/package/@klars/agentobs) · [Contributing](CONTRIBUTING.md)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<br>
|
|
18
|
+
|
|
19
|
+
<picture>
|
|
20
|
+
<source srcset="docs/assets/dashboard-dark.png" media="(prefers-color-scheme: dark)" />
|
|
21
|
+
<img src="docs/assets/dashboard.png" alt="The AgentObs dashboard: spend for the week, tool call and error-rate tiles with trend sparklines, an activity chart, and tables of tools and sessions." />
|
|
22
|
+
</picture>
|
|
23
|
+
|
|
24
|
+
<br>
|
|
25
|
+
|
|
26
|
+
## Guardrails that actually stop things
|
|
27
|
+
|
|
28
|
+
<img src="docs/assets/demo.gif" alt="Terminal demo: agentobs policy test blocks an rm -rf command, then agentobs stats shows the cost, calls, errors and blocked totals." width="820" />
|
|
29
|
+
|
|
30
|
+
AgentObs is an observability and control layer for AI coding agents. It runs
|
|
31
|
+
entirely on your machine: a CLI, a local SQLite database, and a dashboard. No
|
|
32
|
+
account, no cloud, no telemetry.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g @klars/agentobs
|
|
38
|
+
agentobs init
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`init` prints a hook configuration block. Paste it into `~/.claude/settings.json`
|
|
42
|
+
(or a project's `.claude/settings.json`), then:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
agentobs dashboard
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run Claude Code as usual. Tool calls appear in the dashboard within seconds.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What you get
|
|
53
|
+
|
|
54
|
+
| | |
|
|
55
|
+
| ---------------------- | ------------------------------------------------------------ |
|
|
56
|
+
| **Cost tracking** | Per session, per tool, per day — or blank if the model's price is unknown. Never guessed. |
|
|
57
|
+
| **Tool-call timeline** | Every call, its duration, status, and truncated input. |
|
|
58
|
+
| **Guardrails** | Block `rm -rf`, require approval for `.env` edits, stop `curl \| sh`. |
|
|
59
|
+
| **Audit trail** | Every policy decision recorded with the rule that fired. |
|
|
60
|
+
| **Any agent** | Native Claude Code hooks; JSONL ingestion or process-wrapping for everything else. |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Privacy
|
|
65
|
+
|
|
66
|
+
This is the part that matters most, since AgentObs sits in the middle of
|
|
67
|
+
everything your agent does.
|
|
68
|
+
|
|
69
|
+
- **Nothing leaves your machine.** No network calls, no analytics, no account.
|
|
70
|
+
- **Secrets are redacted before anything is written to disk.** Tool inputs and
|
|
71
|
+
outputs pass through a redaction layer that recognises AWS keys, Anthropic /
|
|
72
|
+
OpenAI / GitHub / GitLab / Slack / Stripe / Google / npm tokens, JWTs, PEM
|
|
73
|
+
private keys, `KEY=value` assignments, `--flag secret` arguments,
|
|
74
|
+
`Authorization:` headers, and credentials embedded in URLs.
|
|
75
|
+
- **Summaries are truncated** to ~500 characters.
|
|
76
|
+
- The redaction rules are unit-tested in
|
|
77
|
+
[`src/core/redact.test.ts`](src/core/redact.test.ts) — the tests are the
|
|
78
|
+
guarantee, and they have caught real leaks during development.
|
|
79
|
+
|
|
80
|
+
Everything lives in `~/.agentobs/`. Uninstalling is `rm -rf ~/.agentobs`.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
agentobs init Set up ~/.agentobs and print the hook config
|
|
88
|
+
agentobs dashboard [--port] [--host] Serve the dashboard (default 127.0.0.1:4300)
|
|
89
|
+
agentobs stats [--today] [--since] Print totals in the terminal
|
|
90
|
+
agentobs run -- <command...> Observe any command (coarse detail)
|
|
91
|
+
agentobs watch <file.jsonl> Ingest a JSONL agent log
|
|
92
|
+
agentobs export --format csv|json Export sessions, tool calls, or decisions
|
|
93
|
+
|
|
94
|
+
agentobs policy init Write a starter policy.json
|
|
95
|
+
agentobs policy check Validate it and list active rules
|
|
96
|
+
agentobs policy test <tool> <input> Dry-run a call against the policy
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Guardrails
|
|
102
|
+
|
|
103
|
+
`agentobs policy init` writes `~/.agentobs/policy.json`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"rules": [
|
|
108
|
+
{
|
|
109
|
+
"name": "no-recursive-force-delete",
|
|
110
|
+
"match": { "tool": "Bash", "command_pattern": "*rm -rf*" },
|
|
111
|
+
"decision": "block",
|
|
112
|
+
"message": "Recursive force-delete is blocked by AgentObs policy."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "protect-env-files",
|
|
116
|
+
"match": { "tool": "*", "path_pattern": "**/.env*" },
|
|
117
|
+
"decision": "needs_approval"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"default_decision": "allow"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Rules are evaluated top to bottom; **the first match wins**, so you can put a
|
|
125
|
+
narrow `allow` above a broad `block`. Check what a rule will do *before* it
|
|
126
|
+
fires mid-task:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
$ agentobs policy test Bash "rm -rf ./build"
|
|
130
|
+
|
|
131
|
+
Tool Bash
|
|
132
|
+
Input rm -rf ./build
|
|
133
|
+
Decision BLOCK
|
|
134
|
+
Rule no-recursive-force-delete
|
|
135
|
+
|
|
136
|
+
This call would be BLOCKED before running.
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Two deliberate behaviours worth knowing:
|
|
140
|
+
|
|
141
|
+
- **`needs_approval` currently behaves as a block** with a clearer message.
|
|
142
|
+
There is no channel for a hook to prompt you interactively mid-call.
|
|
143
|
+
- **A broken policy file fails open.** Invalid JSON or a malformed rule
|
|
144
|
+
degrades to allow-everything and reports the problem, because a guardrail
|
|
145
|
+
that wedges your agent is worse than no guardrail. Run `agentobs policy check`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Agent support
|
|
150
|
+
|
|
151
|
+
| Agent | How | Detail |
|
|
152
|
+
| --------------- | -------------------------- | ------------------------------------------------- |
|
|
153
|
+
| **Claude Code** | Native hooks | **Rich** — every tool call, plus policy enforcement |
|
|
154
|
+
| Any CLI agent | `agentobs run -- <cmd>` | **Coarse** — duration and exit code only |
|
|
155
|
+
| Custom / in-house | `agentobs watch <file>` | **Rich**, if it writes JSONL |
|
|
156
|
+
|
|
157
|
+
The dashboard labels coarse sessions as `coarse` rather than implying detail it
|
|
158
|
+
does not have.
|
|
159
|
+
|
|
160
|
+
### A note on cost accuracy
|
|
161
|
+
|
|
162
|
+
Claude Code's `PostToolUse` hook payload carries **no token or cost fields**.
|
|
163
|
+
AgentObs therefore reads token usage from the session transcript at
|
|
164
|
+
`SessionEnd`, which makes **session-level cost accurate** but leaves
|
|
165
|
+
**per-tool-call cost blank** for hook-sourced data. It does not divide a total
|
|
166
|
+
across calls to manufacture a number.
|
|
167
|
+
|
|
168
|
+
Model prices live in `~/.agentobs/pricing.json` and are yours to edit. A model
|
|
169
|
+
missing from that file shows cost as `—`, never `$0.00`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Dashboard access
|
|
174
|
+
|
|
175
|
+
Binds to `127.0.0.1` with no authentication — same machine, same user, same
|
|
176
|
+
trust boundary as the database file.
|
|
177
|
+
|
|
178
|
+
Binding anywhere else **requires a token**, printed at startup and included in
|
|
179
|
+
the URL:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
agentobs dashboard --host 0.0.0.0
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Never expose the dashboard to the public internet.** It shows tool inputs and
|
|
186
|
+
file paths from your repositories.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
Node.js **≥ 22.5** — AgentObs uses the built-in `node:sqlite` module, so there
|
|
193
|
+
is no native addon to compile and no C++ toolchain to install.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npm install
|
|
201
|
+
npm run build
|
|
202
|
+
npm test
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Adding an adapter for another agent: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT © [Klars AI](https://klars.ai)
|
package/dist/core/queries.js
CHANGED
|
@@ -15,20 +15,45 @@ export function rangeStart(range) {
|
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Totals for an explicit window. Used for the previous-period comparison;
|
|
20
|
+
* `getSummary` handles the current window itself.
|
|
21
|
+
*/
|
|
22
|
+
function periodTotals(db, from, to) {
|
|
23
|
+
const calls = db
|
|
24
|
+
.prepare(`SELECT COUNT(*) AS tool_calls,
|
|
25
|
+
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
26
|
+
SUM(cost_usd) AS total_cost_usd
|
|
27
|
+
FROM tool_calls
|
|
28
|
+
WHERE started_at >= ? AND started_at < ?`)
|
|
29
|
+
.get(from, to);
|
|
30
|
+
const sessions = db
|
|
31
|
+
.prepare('SELECT COUNT(*) AS n FROM sessions WHERE started_at >= ? AND started_at < ?')
|
|
32
|
+
.get(from, to);
|
|
33
|
+
const toolCalls = Number(calls.tool_calls ?? 0);
|
|
34
|
+
const errors = Number(calls.errors ?? 0);
|
|
35
|
+
return {
|
|
36
|
+
total_cost_usd: calls.total_cost_usd === null ? null : Number(calls.total_cost_usd),
|
|
37
|
+
tool_calls: toolCalls,
|
|
38
|
+
sessions: Number(sessions.n ?? 0),
|
|
39
|
+
errors,
|
|
40
|
+
error_rate: toolCalls === 0 ? 0 : errors / toolCalls,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
18
43
|
export function getSummary(db, range) {
|
|
19
44
|
const since = rangeStart(range);
|
|
20
45
|
const where = since ? 'WHERE started_at >= ?' : '';
|
|
21
46
|
const args = since ? [since] : [];
|
|
22
47
|
const calls = db
|
|
23
|
-
.prepare(`SELECT
|
|
24
|
-
COUNT(*) AS tool_calls,
|
|
25
|
-
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
26
|
-
COALESCE(SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END), 0) AS blocked,
|
|
27
|
-
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
28
|
-
COALESCE(SUM(tokens_out), 0) AS tokens_out,
|
|
29
|
-
SUM(cost_usd) AS total_cost_usd,
|
|
30
|
-
COALESCE(SUM(CASE WHEN cost_usd IS NULL AND status <> 'pending' THEN 1 ELSE 0 END), 0) AS uncosted_calls,
|
|
31
|
-
AVG(duration_ms) AS avg_duration_ms
|
|
48
|
+
.prepare(`SELECT
|
|
49
|
+
COUNT(*) AS tool_calls,
|
|
50
|
+
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
51
|
+
COALESCE(SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END), 0) AS blocked,
|
|
52
|
+
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
53
|
+
COALESCE(SUM(tokens_out), 0) AS tokens_out,
|
|
54
|
+
SUM(cost_usd) AS total_cost_usd,
|
|
55
|
+
COALESCE(SUM(CASE WHEN cost_usd IS NULL AND status <> 'pending' THEN 1 ELSE 0 END), 0) AS uncosted_calls,
|
|
56
|
+
AVG(duration_ms) AS avg_duration_ms
|
|
32
57
|
FROM tool_calls ${where}`)
|
|
33
58
|
.get(...args);
|
|
34
59
|
const sessions = db.prepare(`SELECT COUNT(*) AS n FROM sessions ${where}`).get(...args);
|
|
@@ -47,8 +72,20 @@ export function getSummary(db, range) {
|
|
|
47
72
|
tokens_in: Number(calls.tokens_in ?? 0),
|
|
48
73
|
tokens_out: Number(calls.tokens_out ?? 0),
|
|
49
74
|
avg_duration_ms: calls.avg_duration_ms === null ? null : Number(calls.avg_duration_ms),
|
|
75
|
+
previous: previousPeriod(db, range, since),
|
|
50
76
|
};
|
|
51
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Totals for the window immediately before the current one, of equal length.
|
|
80
|
+
* Returns null for 'all', where there is no previous period to compare to.
|
|
81
|
+
*/
|
|
82
|
+
function previousPeriod(db, range, since) {
|
|
83
|
+
if (!since)
|
|
84
|
+
return null;
|
|
85
|
+
const start = Date.parse(since);
|
|
86
|
+
const spanMs = range === 'today' ? 864e5 : range === '7d' ? 7 * 864e5 : 30 * 864e5;
|
|
87
|
+
return periodTotals(db, new Date(start - spanMs).toISOString(), since);
|
|
88
|
+
}
|
|
52
89
|
/**
|
|
53
90
|
* Activity/cost over time. Buckets hourly for `today` and daily otherwise so
|
|
54
91
|
* the chart keeps a readable number of points at every range.
|
|
@@ -59,29 +96,67 @@ export function getTimeline(db, range) {
|
|
|
59
96
|
const where = since ? 'WHERE started_at >= ?' : '';
|
|
60
97
|
const args = since ? [since] : [];
|
|
61
98
|
return db
|
|
62
|
-
.prepare(`SELECT strftime(?, started_at) AS bucket,
|
|
63
|
-
COUNT(*) AS calls,
|
|
64
|
-
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
65
|
-
SUM(cost_usd) AS cost_usd,
|
|
66
|
-
COALESCE(SUM(COALESCE(tokens_in, 0) + COALESCE(tokens_out, 0)), 0) AS tokens
|
|
67
|
-
FROM tool_calls ${where}
|
|
68
|
-
GROUP BY bucket
|
|
99
|
+
.prepare(`SELECT strftime(?, started_at) AS bucket,
|
|
100
|
+
COUNT(*) AS calls,
|
|
101
|
+
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
102
|
+
SUM(cost_usd) AS cost_usd,
|
|
103
|
+
COALESCE(SUM(COALESCE(tokens_in, 0) + COALESCE(tokens_out, 0)), 0) AS tokens
|
|
104
|
+
FROM tool_calls ${where}
|
|
105
|
+
GROUP BY bucket
|
|
69
106
|
ORDER BY bucket ASC`)
|
|
70
107
|
.all(fmt, ...args);
|
|
71
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Compact per-bucket series for the stat-tile sparklines.
|
|
111
|
+
*
|
|
112
|
+
* Returns a fixed 12 buckets (the stat-tile contract's trend length),
|
|
113
|
+
* zero-filled so a quiet day renders as a gap in the line rather than
|
|
114
|
+
* silently shortening the series and misstating the shape.
|
|
115
|
+
*/
|
|
116
|
+
export function getSparklines(db, range) {
|
|
117
|
+
const POINTS = 12;
|
|
118
|
+
const spanMs = range === 'today' ? 864e5 : range === '7d' ? 7 * 864e5 : 30 * 864e5;
|
|
119
|
+
const end = Date.now();
|
|
120
|
+
const start = range === 'all' ? null : end - spanMs;
|
|
121
|
+
const bucketMs = (start ? spanMs : 30 * 864e5) / POINTS;
|
|
122
|
+
const origin = start ?? end - 30 * 864e5;
|
|
123
|
+
const rows = db
|
|
124
|
+
.prepare(`SELECT started_at, status, cost_usd, session_id
|
|
125
|
+
FROM tool_calls
|
|
126
|
+
WHERE started_at >= ?`)
|
|
127
|
+
.all(new Date(origin).toISOString());
|
|
128
|
+
const calls = new Array(POINTS).fill(0);
|
|
129
|
+
const cost = new Array(POINTS).fill(0);
|
|
130
|
+
const errors = new Array(POINTS).fill(0);
|
|
131
|
+
const blocked = new Array(POINTS).fill(0);
|
|
132
|
+
const sessionSets = Array.from({ length: POINTS }, () => new Set());
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
const i = Math.min(POINTS - 1, Math.floor((Date.parse(row.started_at) - origin) / bucketMs));
|
|
135
|
+
if (i < 0)
|
|
136
|
+
continue;
|
|
137
|
+
calls[i] += 1;
|
|
138
|
+
cost[i] += row.cost_usd ?? 0;
|
|
139
|
+
if (row.status === 'error')
|
|
140
|
+
errors[i] += 1;
|
|
141
|
+
if (row.status === 'blocked')
|
|
142
|
+
blocked[i] += 1;
|
|
143
|
+
sessionSets[i].add(row.session_id);
|
|
144
|
+
}
|
|
145
|
+
return { calls, cost, errors, blocked, sessions: sessionSets.map((s) => s.size) };
|
|
146
|
+
}
|
|
72
147
|
export function getToolsBreakdown(db, range) {
|
|
73
148
|
const since = rangeStart(range);
|
|
74
149
|
const where = since ? 'WHERE started_at >= ?' : '';
|
|
75
150
|
const args = since ? [since] : [];
|
|
76
151
|
return db
|
|
77
|
-
.prepare(`SELECT tool_name,
|
|
78
|
-
COUNT(*) AS calls,
|
|
79
|
-
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
80
|
-
COALESCE(SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END), 0) AS blocked,
|
|
81
|
-
SUM(cost_usd) AS cost_usd,
|
|
82
|
-
AVG(duration_ms) AS avg_duration_ms
|
|
83
|
-
FROM tool_calls ${where}
|
|
84
|
-
GROUP BY tool_name
|
|
152
|
+
.prepare(`SELECT tool_name,
|
|
153
|
+
COUNT(*) AS calls,
|
|
154
|
+
COALESCE(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
155
|
+
COALESCE(SUM(CASE WHEN status = 'blocked' THEN 1 ELSE 0 END), 0) AS blocked,
|
|
156
|
+
SUM(cost_usd) AS cost_usd,
|
|
157
|
+
AVG(duration_ms) AS avg_duration_ms
|
|
158
|
+
FROM tool_calls ${where}
|
|
159
|
+
GROUP BY tool_name
|
|
85
160
|
ORDER BY calls DESC`)
|
|
86
161
|
.all(...args);
|
|
87
162
|
}
|
|
@@ -104,16 +179,16 @@ export function getRecentToolCalls(db, opts = {}) {
|
|
|
104
179
|
}
|
|
105
180
|
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
106
181
|
return db
|
|
107
|
-
.prepare(`SELECT tc.id, tc.session_id, s.agent_name, tc.tool_name, tc.started_at,
|
|
108
|
-
tc.duration_ms, tc.status, tc.input_summary, tc.output_summary,
|
|
109
|
-
tc.cost_usd, tc.error_message,
|
|
110
|
-
(SELECT pd.rule_matched FROM policy_decisions pd
|
|
111
|
-
WHERE pd.tool_call_id = tc.id
|
|
112
|
-
ORDER BY pd.decided_at DESC LIMIT 1) AS rule_matched
|
|
113
|
-
FROM tool_calls tc
|
|
114
|
-
LEFT JOIN sessions s ON s.id = tc.session_id
|
|
115
|
-
${where}
|
|
116
|
-
ORDER BY tc.started_at DESC
|
|
182
|
+
.prepare(`SELECT tc.id, tc.session_id, s.agent_name, tc.tool_name, tc.started_at,
|
|
183
|
+
tc.duration_ms, tc.status, tc.input_summary, tc.output_summary,
|
|
184
|
+
tc.cost_usd, tc.error_message,
|
|
185
|
+
(SELECT pd.rule_matched FROM policy_decisions pd
|
|
186
|
+
WHERE pd.tool_call_id = tc.id
|
|
187
|
+
ORDER BY pd.decided_at DESC LIMIT 1) AS rule_matched
|
|
188
|
+
FROM tool_calls tc
|
|
189
|
+
LEFT JOIN sessions s ON s.id = tc.session_id
|
|
190
|
+
${where}
|
|
191
|
+
ORDER BY tc.started_at DESC
|
|
117
192
|
LIMIT ?`)
|
|
118
193
|
.all(...args, limit);
|
|
119
194
|
}
|
|
@@ -123,20 +198,20 @@ export function getSessions(db, opts = {}) {
|
|
|
123
198
|
const where = since ? 'WHERE started_at >= ?' : '';
|
|
124
199
|
const args = since ? [since] : [];
|
|
125
200
|
return db
|
|
126
|
-
.prepare(`SELECT id, agent_name, started_at, ended_at, cwd, fidelity, tool_call_count,
|
|
127
|
-
error_count, blocked_count, total_cost_usd, total_tokens_in,
|
|
128
|
-
total_tokens_out, exit_code
|
|
129
|
-
FROM sessions ${where}
|
|
130
|
-
ORDER BY started_at DESC
|
|
201
|
+
.prepare(`SELECT id, agent_name, started_at, ended_at, cwd, fidelity, tool_call_count,
|
|
202
|
+
error_count, blocked_count, total_cost_usd, total_tokens_in,
|
|
203
|
+
total_tokens_out, exit_code
|
|
204
|
+
FROM sessions ${where}
|
|
205
|
+
ORDER BY started_at DESC
|
|
131
206
|
LIMIT ?`)
|
|
132
207
|
.all(...args, limit);
|
|
133
208
|
}
|
|
134
209
|
export function getPolicyDecisions(db, opts = {}) {
|
|
135
210
|
const limit = Math.min(Math.max(opts.limit ?? 100, 1), 500);
|
|
136
211
|
return db
|
|
137
|
-
.prepare(`SELECT id, tool_call_id, session_id, tool_name, rule_matched, decision, reason, decided_at
|
|
138
|
-
FROM policy_decisions
|
|
139
|
-
ORDER BY decided_at DESC
|
|
212
|
+
.prepare(`SELECT id, tool_call_id, session_id, tool_name, rule_matched, decision, reason, decided_at
|
|
213
|
+
FROM policy_decisions
|
|
214
|
+
ORDER BY decided_at DESC
|
|
140
215
|
LIMIT ?`)
|
|
141
216
|
.all(limit);
|
|
142
217
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { dirname, extname, join, normalize } from 'node:path';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { timingSafeEqual } from 'node:crypto';
|
|
20
20
|
import { openDb } from '../core/db.js';
|
|
21
|
-
import { getPolicyDecisions, getRecentToolCalls, getSessions, getSummary, getTimeline, getToolsBreakdown, } from '../core/queries.js';
|
|
21
|
+
import { getPolicyDecisions, getRecentToolCalls, getSessions, getSparklines, getSummary, getTimeline, getToolsBreakdown, } from '../core/queries.js';
|
|
22
22
|
import { loadPolicy } from '../core/policy-engine.js';
|
|
23
23
|
const PUBLIC_DIR = join(dirname(fileURLToPath(import.meta.url)), 'public');
|
|
24
24
|
const MIME = {
|
|
@@ -86,7 +86,9 @@ export function createDashboardServer(opts) {
|
|
|
86
86
|
const range = parseRange(url.searchParams.get('range'));
|
|
87
87
|
switch (url.pathname) {
|
|
88
88
|
case '/api/summary':
|
|
89
|
-
|
|
89
|
+
// Sparklines ride along with the summary: the tiles need both, and
|
|
90
|
+
// one request keeps the 5s poll to a single round trip.
|
|
91
|
+
json(res, { ...getSummary(db, range), sparklines: getSparklines(db, range) });
|
|
90
92
|
return;
|
|
91
93
|
case '/api/timeline':
|
|
92
94
|
json(res, getTimeline(db, range));
|