@agenttrace-io/cli 0.1.9
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/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1850 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1850 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentTrace CLI
|
|
4
|
+
* Command-line interface for querying traces, runs, stats and exports
|
|
5
|
+
*/
|
|
6
|
+
import { AgentTrace, TraceStorage, } from '@agenttrace-io/sdk';
|
|
7
|
+
import { startDashboard } from '@agenttrace-io/dashboard';
|
|
8
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
export const VERSION = '0.1.0';
|
|
11
|
+
/** Published npm package name. */
|
|
12
|
+
export const PACKAGE_NAME = '@agenttrace-io/cli';
|
|
13
|
+
function getDbPath() {
|
|
14
|
+
return process.env.AGENTTRACE_DB_PATH || './agenttrace.db';
|
|
15
|
+
}
|
|
16
|
+
// ANSI colors for status
|
|
17
|
+
const GREEN = '\x1b[32m';
|
|
18
|
+
const RED = '\x1b[31m';
|
|
19
|
+
const YELLOW = '\x1b[33m';
|
|
20
|
+
const RESET = '\x1b[0m';
|
|
21
|
+
const NO_COLOR = process.env.NO_COLOR === '1' ||
|
|
22
|
+
process.env.NO_COLOR === 'true' ||
|
|
23
|
+
process.argv.includes('--no-color');
|
|
24
|
+
function color(s, c) {
|
|
25
|
+
if (NO_COLOR)
|
|
26
|
+
return s;
|
|
27
|
+
return c + s + RESET;
|
|
28
|
+
}
|
|
29
|
+
function colorizeStatus(status) {
|
|
30
|
+
const s = status.toLowerCase();
|
|
31
|
+
const plain = status;
|
|
32
|
+
if (s === 'success')
|
|
33
|
+
return color(plain, GREEN);
|
|
34
|
+
if (s === 'error' || s === 'failure')
|
|
35
|
+
return color(plain, RED);
|
|
36
|
+
if (s === 'running' || s === 'timeout')
|
|
37
|
+
return color(plain, YELLOW);
|
|
38
|
+
return plain;
|
|
39
|
+
}
|
|
40
|
+
function stripAnsi(str) {
|
|
41
|
+
// eslint-disable-next-line no-control-regex
|
|
42
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
43
|
+
}
|
|
44
|
+
function visibleLength(str) {
|
|
45
|
+
return stripAnsi(str).length;
|
|
46
|
+
}
|
|
47
|
+
function pad(str, width) {
|
|
48
|
+
const vis = visibleLength(str);
|
|
49
|
+
return str + ' '.repeat(Math.max(0, width - vis));
|
|
50
|
+
}
|
|
51
|
+
function fmtNum(n) {
|
|
52
|
+
if (n == null)
|
|
53
|
+
return '0';
|
|
54
|
+
return Math.round(Number(n)).toLocaleString();
|
|
55
|
+
}
|
|
56
|
+
function fmtCost(c) {
|
|
57
|
+
if (c == null)
|
|
58
|
+
return '$0.0000';
|
|
59
|
+
return '$' + Number(c).toFixed(4);
|
|
60
|
+
}
|
|
61
|
+
function fmtLatency(ms) {
|
|
62
|
+
if (ms == null)
|
|
63
|
+
return '0ms';
|
|
64
|
+
if (ms < 1000)
|
|
65
|
+
return Math.round(ms) + 'ms';
|
|
66
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
67
|
+
}
|
|
68
|
+
function _asciiSpark(values, width = 10) {
|
|
69
|
+
if (!values || values.length === 0)
|
|
70
|
+
return '';
|
|
71
|
+
const max = Math.max(...values, 1);
|
|
72
|
+
const blocks = ' ▁▂▃▄▅▆▇█';
|
|
73
|
+
let out = '';
|
|
74
|
+
for (let i = 0; i < Math.min(width, values.length); i++) {
|
|
75
|
+
const v = values[values.length - Math.min(width, values.length) + i] || 0;
|
|
76
|
+
const idx = Math.min(blocks.length - 1, Math.max(0, Math.floor((v / max) * (blocks.length - 1))));
|
|
77
|
+
out += blocks[idx];
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function printTable(headers, rows) {
|
|
82
|
+
if (rows.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const widths = headers.map((h, i) => {
|
|
85
|
+
const rowMax = Math.max(0, ...rows.map((r) => visibleLength(r[i] ?? '')));
|
|
86
|
+
return Math.max(h.length, rowMax);
|
|
87
|
+
});
|
|
88
|
+
// header
|
|
89
|
+
console.log(headers.map((h, i) => pad(h, widths[i] ?? 0)).join(' '));
|
|
90
|
+
// separator
|
|
91
|
+
console.log(widths.map((w) => '-'.repeat(w ?? 0)).join(' '));
|
|
92
|
+
// rows
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
console.log(row.map((c, i) => pad(c, widths[i] ?? 0)).join(' '));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function printRunsTable(runs) {
|
|
98
|
+
const headers = ['ID', 'Name', 'Status', 'Traces', 'Tokens', 'Cost', '▂', 'Started'];
|
|
99
|
+
// build cost spark history per run from recent context (simple per-row mini bar)
|
|
100
|
+
const maxCost = Math.max(0.0001, ...runs.map((r) => r.totalCostUsd || 0));
|
|
101
|
+
const rows = runs.map((r) => {
|
|
102
|
+
const cost = r.totalCostUsd || 0;
|
|
103
|
+
const barLen = Math.max(1, Math.min(8, Math.round((cost / maxCost) * 8)));
|
|
104
|
+
const bar = '█'.repeat(barLen) + ' '.repeat(8 - barLen);
|
|
105
|
+
return [
|
|
106
|
+
(r.id || '').substring(0, 8),
|
|
107
|
+
r.name || '',
|
|
108
|
+
colorizeStatus(r.status || ''),
|
|
109
|
+
fmtNum(r.traceCount ?? 0),
|
|
110
|
+
fmtNum(r.totalTokens?.totalTokens ?? 0),
|
|
111
|
+
fmtCost(cost),
|
|
112
|
+
bar,
|
|
113
|
+
r.startedAt ? new Date(r.startedAt).toISOString().slice(0, 19).replace('T', ' ') : '',
|
|
114
|
+
];
|
|
115
|
+
});
|
|
116
|
+
printTable(headers, rows);
|
|
117
|
+
}
|
|
118
|
+
function printTracesTable(traces) {
|
|
119
|
+
const headers = ['ID', 'Name', 'Status', 'Latency', 'Tokens', 'Cost', 'Created'];
|
|
120
|
+
const rows = traces.map((t) => [
|
|
121
|
+
(t.id || '').substring(0, 8),
|
|
122
|
+
t.name || '',
|
|
123
|
+
colorizeStatus(t.status || ''),
|
|
124
|
+
fmtLatency(t.latencyMs ?? 0),
|
|
125
|
+
fmtNum(t.tokens?.totalTokens ?? 0),
|
|
126
|
+
fmtCost(t.costUsd ?? 0),
|
|
127
|
+
t.createdAt ? new Date(t.createdAt).toISOString().slice(0, 19).replace('T', ' ') : '',
|
|
128
|
+
]);
|
|
129
|
+
printTable(headers, rows);
|
|
130
|
+
}
|
|
131
|
+
function printStats(stats) {
|
|
132
|
+
console.log('AgentTrace Statistics');
|
|
133
|
+
console.log('=====================');
|
|
134
|
+
console.log(`Total Runs: ${fmtNum(stats.totalRuns ?? 0)}`);
|
|
135
|
+
console.log(`Total Traces: ${fmtNum(stats.totalTraces ?? 0)}`);
|
|
136
|
+
const rate = ((stats.successRate ?? 0) * 100).toFixed(1);
|
|
137
|
+
console.log(`Success Rate: ${rate}%`);
|
|
138
|
+
console.log(`Avg Latency: ${fmtLatency(stats.avgLatencyMs ?? 0)}`);
|
|
139
|
+
console.log(`Total Cost: ${fmtCost(stats.totalCostUsd ?? 0)}`);
|
|
140
|
+
console.log(`Total Tokens: ${fmtNum(stats.totalTokens ?? 0)}`);
|
|
141
|
+
console.log(`Avg Tokens: ${fmtNum(stats.avgTokensPerTrace ?? 0)}`);
|
|
142
|
+
// Simple trend hint (computed if we can infer from SDK; otherwise neutral)
|
|
143
|
+
const trendNote = stats.totalCostUsd && stats.totalCostUsd > 0 ? ' (↑ track daily with `costs --daily`)' : '';
|
|
144
|
+
console.log(`Cost trend:${trendNote}`);
|
|
145
|
+
if (stats.topTools && stats.topTools.length > 0) {
|
|
146
|
+
console.log('\nTop Tools:');
|
|
147
|
+
for (const t of stats.topTools.slice(0, 5)) {
|
|
148
|
+
console.log(` ${t.name}: ${fmtNum(t.count)} (avg ${t.avgLatencyMs}ms)`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (stats.topErrors && stats.topErrors.length > 0) {
|
|
152
|
+
console.log('\nTop Errors:');
|
|
153
|
+
for (const e of stats.topErrors.slice(0, 5)) {
|
|
154
|
+
console.log(` ${e.error}: ${fmtNum(e.count)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function printCosts(breakdown, daily) {
|
|
159
|
+
const title = daily ? 'Daily Cost Breakdown' : 'Cost Breakdown by Model';
|
|
160
|
+
console.log(title);
|
|
161
|
+
console.log('='.repeat(title.length));
|
|
162
|
+
const data = daily ? breakdown.costByDay : breakdown.costByModel;
|
|
163
|
+
const entries = Object.entries(data);
|
|
164
|
+
if (entries.length === 0) {
|
|
165
|
+
console.log('No costs recorded.');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// sort by cost desc for models, chrono for days
|
|
169
|
+
const sorted = daily
|
|
170
|
+
? entries.sort(([a], [b]) => a.localeCompare(b))
|
|
171
|
+
: entries.sort(([, c1], [, c2]) => c2 - c1);
|
|
172
|
+
for (const [key, cost] of sorted) {
|
|
173
|
+
console.log(` ${key}: $${cost.toFixed(4)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
console.log(`\nTotal: $${breakdown.totalCostUsd.toFixed(4)}`);
|
|
177
|
+
}
|
|
178
|
+
function printTraceTree(node, prefix = '', isLast = true) {
|
|
179
|
+
if (!node || !node.trace)
|
|
180
|
+
return;
|
|
181
|
+
const t = node.trace;
|
|
182
|
+
const branch = prefix + (isLast ? '└── ' : '├── ');
|
|
183
|
+
const status = colorizeStatus(t.status || '');
|
|
184
|
+
const shortId = (t.id || '').substring(0, 8);
|
|
185
|
+
console.log(`${branch}${shortId} ${t.name || ''} ${status} ${t.latencyMs ?? 0}ms $${(t.costUsd ?? 0).toFixed(4)}`);
|
|
186
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
187
|
+
const children = (node.children || []);
|
|
188
|
+
children.forEach((child, idx) => {
|
|
189
|
+
const lastChild = idx === children.length - 1;
|
|
190
|
+
printTraceTree(child, childPrefix, lastChild);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function printUsage() {
|
|
194
|
+
console.log(`Usage: agenttrace-io <command> [options] (alias: agenttrace)
|
|
195
|
+
|
|
196
|
+
Commands:
|
|
197
|
+
init Create empty agenttrace.db in current dir
|
|
198
|
+
wrap Trace any CLI command (zero-config)
|
|
199
|
+
dashboard Start the local dashboard server
|
|
200
|
+
runs List recent runs (most recent first)
|
|
201
|
+
traces List traces (most recent first)
|
|
202
|
+
stats Show summary statistics
|
|
203
|
+
costs Show cost breakdown by model (or --daily)
|
|
204
|
+
export Export traces to JSON or CSV
|
|
205
|
+
benchmark Run performance benchmark suite (prints JSON results)
|
|
206
|
+
tree Show parent/child/related trace tree (multi-agent)
|
|
207
|
+
alerts Manage alerts: list | test --name N | history
|
|
208
|
+
health Check health of gateway, dashboard, and database
|
|
209
|
+
self-stats Show self-tracked usage stats
|
|
210
|
+
budget Manage per-agent token budgets: set | list | status | check
|
|
211
|
+
who Show active agents (usage tracking)
|
|
212
|
+
cost Show agent cost breakdown (periods + by agent/model)
|
|
213
|
+
sessions List agent sessions with aggregates
|
|
214
|
+
activity Show recent agent activity timeline
|
|
215
|
+
webhook Manage webhooks: add <url> <events...> | list | remove <id> | test <id>
|
|
216
|
+
cleanup Manually run data retention cleanup (deletes expired traces, runs, usage)
|
|
217
|
+
retention Manage data retention policy: show | set <days> [--interval H]
|
|
218
|
+
version Show CLI version
|
|
219
|
+
|
|
220
|
+
Options (by command):
|
|
221
|
+
runs, traces:
|
|
222
|
+
--limit N Number of results (default: runs=20, traces=50)
|
|
223
|
+
--status FILTER Comma-separated statuses (success,error,failure,running,timeout)
|
|
224
|
+
traces, export, costs:
|
|
225
|
+
--run-id ID Filter by run ID
|
|
226
|
+
export:
|
|
227
|
+
--format json|csv Output format (default: json)
|
|
228
|
+
--output FILE Write to file instead of stdout
|
|
229
|
+
costs:
|
|
230
|
+
--daily Breakdown costs by day instead of by model
|
|
231
|
+
tree:
|
|
232
|
+
--trace-id ID Trace ID to display tree for (required)
|
|
233
|
+
alerts:
|
|
234
|
+
list List configured alerts
|
|
235
|
+
test --name NAME Test delivery for alert (forces condition + ignores cooldown)
|
|
236
|
+
history Show alert trigger history
|
|
237
|
+
who:
|
|
238
|
+
--active Only agents active in last 30min
|
|
239
|
+
--type TYPE Filter by agent type
|
|
240
|
+
--limit N Max agents to show (default 50)
|
|
241
|
+
cost:
|
|
242
|
+
--from DATE Start date (YYYY-MM-DD or ISO)
|
|
243
|
+
--to DATE End date (YYYY-MM-DD or ISO)
|
|
244
|
+
--agent NAME Filter to specific agent
|
|
245
|
+
--format json|table Output format (default: table)
|
|
246
|
+
sessions:
|
|
247
|
+
--agent NAME Filter by agent name
|
|
248
|
+
--active Only sessions with recent activity (30min)
|
|
249
|
+
--limit N Max sessions (default 20)
|
|
250
|
+
activity:
|
|
251
|
+
--agent NAME Filter by agent
|
|
252
|
+
--type ACTION Filter by action type
|
|
253
|
+
--limit N Max entries (default 30)
|
|
254
|
+
--since DURATION e.g. 1h, 30m, 2d (from now backwards)
|
|
255
|
+
webhook:
|
|
256
|
+
add --url <url> --events <e1,e2,...>
|
|
257
|
+
Register a webhook for the given event types
|
|
258
|
+
list List all configured webhooks
|
|
259
|
+
remove --id <id> Remove a webhook by ID (prefix match)
|
|
260
|
+
test --id <id> Send a test payload to a webhook by ID
|
|
261
|
+
Events: trace.complete, trace.error, run.complete, run.error, cost.threshold, agent.inactive
|
|
262
|
+
cleanup:
|
|
263
|
+
--days N Override retention days (default: use policy setting)
|
|
264
|
+
--dry-run Show what would be deleted without deleting
|
|
265
|
+
retention:
|
|
266
|
+
show Show current retention policy and storage stats
|
|
267
|
+
set <days> Set retention policy (days); optional --interval H
|
|
268
|
+
|
|
269
|
+
Global:
|
|
270
|
+
--json Emit machine-readable JSON (for runs, traces, stats, costs, export, self-stats)
|
|
271
|
+
--help Show this help
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
agenttrace-io init
|
|
275
|
+
agenttrace-io wrap claude "Write a hello world function"
|
|
276
|
+
agenttrace-io runs --limit 5 --status success,running
|
|
277
|
+
agenttrace-io traces --run-id 123e4567 --json
|
|
278
|
+
agenttrace-io export --format csv --output out.csv --run-id abc
|
|
279
|
+
agenttrace-io dashboard
|
|
280
|
+
agenttrace-io costs
|
|
281
|
+
agenttrace-io costs --daily --json
|
|
282
|
+
agenttrace-io costs --run-id abc123
|
|
283
|
+
agenttrace-io alerts list
|
|
284
|
+
agenttrace-io alerts test --name high-error-rate
|
|
285
|
+
agenttrace-io alerts history
|
|
286
|
+
agenttrace-io tree --trace-id abc123def
|
|
287
|
+
agenttrace-io self-stats
|
|
288
|
+
agenttrace-io self-stats --json
|
|
289
|
+
agenttrace-io who --active --limit 10
|
|
290
|
+
agenttrace-io cost --format table
|
|
291
|
+
agenttrace-io cost --agent researcher-1 --from 2026-01-01
|
|
292
|
+
agenttrace-io sessions --active
|
|
293
|
+
agenttrace-io activity --since 2h --limit 20
|
|
294
|
+
agenttrace-io cleanup
|
|
295
|
+
agenttrace-io cleanup --days 7 --dry-run
|
|
296
|
+
agenttrace-io retention show
|
|
297
|
+
agenttrace-io retention set 60
|
|
298
|
+
agenttrace-io retention set 90 --interval 12
|
|
299
|
+
agenttrace-io webhook add https://example.com/hook trace.complete run.complete
|
|
300
|
+
agenttrace-io webhook list
|
|
301
|
+
agenttrace-io webhook test abc12345
|
|
302
|
+
agenttrace-io webhook remove abc12345
|
|
303
|
+
npx agenttrace-io version
|
|
304
|
+
# alias also works: npx agenttrace ...
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
function isSelfTracked(t) {
|
|
308
|
+
const meta = t.metadata || t.metadata || {};
|
|
309
|
+
return meta.selfTracked === true || t.name?.startsWith?.('self:') === true;
|
|
310
|
+
}
|
|
311
|
+
function getDayStart(ts) {
|
|
312
|
+
const d = new Date(ts);
|
|
313
|
+
d.setHours(0, 0, 0, 0);
|
|
314
|
+
return d.getTime();
|
|
315
|
+
}
|
|
316
|
+
function getWeekStart(ts) {
|
|
317
|
+
const d = new Date(ts);
|
|
318
|
+
const day = d.getDay();
|
|
319
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // monday
|
|
320
|
+
d.setDate(diff);
|
|
321
|
+
d.setHours(0, 0, 0, 0);
|
|
322
|
+
return d.getTime();
|
|
323
|
+
}
|
|
324
|
+
function printSelfStats(storage, useJson) {
|
|
325
|
+
// Fetch recent data (no hard limit to include historical self usage)
|
|
326
|
+
const runs = storage.getRuns(5000);
|
|
327
|
+
const traces = storage.getTraces({ limit: 20000 });
|
|
328
|
+
const selfRuns = runs.filter((r) => isSelfTracked(r));
|
|
329
|
+
const selfTraces = traces.filter((t) => isSelfTracked(t));
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
const todayStart = getDayStart(now);
|
|
332
|
+
const weekStart = getWeekStart(now);
|
|
333
|
+
const todayTraces = selfTraces.filter((t) => (t.createdAt || 0) >= todayStart);
|
|
334
|
+
const weekTraces = selfTraces.filter((t) => (t.createdAt || 0) >= weekStart);
|
|
335
|
+
// Today's summary
|
|
336
|
+
const todayActions = todayTraces.length;
|
|
337
|
+
const todayTokens = todayTraces.reduce((s, t) => s + (t.tokens?.totalTokens || 0), 0);
|
|
338
|
+
const todayCost = todayTraces.reduce((s, t) => s + (t.costUsd || 0), 0);
|
|
339
|
+
const todaySessions = new Set(todayTraces.map((t) => t.runId)).size;
|
|
340
|
+
// Week summary
|
|
341
|
+
const weekActions = weekTraces.length;
|
|
342
|
+
const weekTokens = weekTraces.reduce((s, t) => s + (t.tokens?.totalTokens || 0), 0);
|
|
343
|
+
const weekCost = weekTraces.reduce((s, t) => s + (t.costUsd || 0), 0);
|
|
344
|
+
const weekSessions = new Set(weekTraces.map((t) => t.runId)).size;
|
|
345
|
+
// Top actions by type (use actionType from meta or derive from name)
|
|
346
|
+
const actionCounts = {};
|
|
347
|
+
for (const t of selfTraces) {
|
|
348
|
+
const meta = t.metadata || {};
|
|
349
|
+
const at = meta.actionType ||
|
|
350
|
+
(t.name || '').replace(/^self:/, '').split(':')[0] ||
|
|
351
|
+
'unknown';
|
|
352
|
+
actionCounts[at] = (actionCounts[at] || 0) + 1;
|
|
353
|
+
}
|
|
354
|
+
const topActions = Object.entries(actionCounts)
|
|
355
|
+
.sort(([, a], [, b]) => b - a)
|
|
356
|
+
.slice(0, 10)
|
|
357
|
+
.map(([type, count]) => ({ type, count }));
|
|
358
|
+
// Cost breakdown (self only) - by day for recent
|
|
359
|
+
const costByDay = {};
|
|
360
|
+
for (const t of selfTraces) {
|
|
361
|
+
if (!t.createdAt)
|
|
362
|
+
continue;
|
|
363
|
+
const day = new Date(t.createdAt).toISOString().slice(0, 10);
|
|
364
|
+
costByDay[day] = (costByDay[day] || 0) + (t.costUsd || 0);
|
|
365
|
+
}
|
|
366
|
+
const totalSelfCost = Object.values(costByDay).reduce((s, v) => s + v, 0);
|
|
367
|
+
// Active sessions
|
|
368
|
+
const activeSessions = selfRuns.filter((r) => r.status === 'running').length;
|
|
369
|
+
const activeSessionIds = selfRuns
|
|
370
|
+
.filter((r) => r.status === 'running')
|
|
371
|
+
.map((r) => (r.id || '').substring(0, 8));
|
|
372
|
+
const summary = {
|
|
373
|
+
today: {
|
|
374
|
+
actions: todayActions,
|
|
375
|
+
tokens: todayTokens,
|
|
376
|
+
costUsd: Number(todayCost.toFixed(6)),
|
|
377
|
+
sessions: todaySessions,
|
|
378
|
+
},
|
|
379
|
+
week: {
|
|
380
|
+
actions: weekActions,
|
|
381
|
+
tokens: weekTokens,
|
|
382
|
+
costUsd: Number(weekCost.toFixed(6)),
|
|
383
|
+
sessions: weekSessions,
|
|
384
|
+
},
|
|
385
|
+
topActions,
|
|
386
|
+
costBreakdown: {
|
|
387
|
+
totalCostUsd: Number(totalSelfCost.toFixed(6)),
|
|
388
|
+
costByDay,
|
|
389
|
+
},
|
|
390
|
+
activeSessions,
|
|
391
|
+
activeSessionIds,
|
|
392
|
+
totalSelfTraces: selfTraces.length,
|
|
393
|
+
totalSelfRuns: selfRuns.length,
|
|
394
|
+
};
|
|
395
|
+
if (useJson) {
|
|
396
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.log('AgentTrace Self-Tracking Stats');
|
|
400
|
+
console.log('==============================================');
|
|
401
|
+
console.log('');
|
|
402
|
+
console.log("Today's Activity:");
|
|
403
|
+
console.log(` Actions: ${summary.today.actions}`);
|
|
404
|
+
console.log(` Tokens: ${summary.today.tokens}`);
|
|
405
|
+
console.log(` Cost: $${summary.today.costUsd}`);
|
|
406
|
+
console.log(` Sessions: ${summary.today.sessions}`);
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log('This Week:');
|
|
409
|
+
console.log(` Actions: ${summary.week.actions}`);
|
|
410
|
+
console.log(` Tokens: ${summary.week.tokens}`);
|
|
411
|
+
console.log(` Cost: $${summary.week.costUsd}`);
|
|
412
|
+
console.log(` Sessions: ${summary.week.sessions}`);
|
|
413
|
+
console.log('');
|
|
414
|
+
if (topActions.length > 0) {
|
|
415
|
+
console.log('Top Actions by Type:');
|
|
416
|
+
const maxA = Math.max(...topActions.map((a) => a.count), 1);
|
|
417
|
+
for (const a of topActions.slice(0, 8)) {
|
|
418
|
+
const bar = '█'.repeat(Math.max(1, Math.round((a.count / maxA) * 12)));
|
|
419
|
+
console.log(` ${a.type.padEnd(14)} ${String(a.count).padStart(4)} ${bar}`);
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
console.log('Cost Breakdown (self-tracked):');
|
|
424
|
+
console.log(` Total: $${summary.costBreakdown.totalCostUsd}`);
|
|
425
|
+
const sortedDays = Object.keys(costByDay).sort();
|
|
426
|
+
if (sortedDays.length > 0) {
|
|
427
|
+
for (const d of sortedDays.slice(-7)) {
|
|
428
|
+
const c = costByDay[d] ?? 0;
|
|
429
|
+
console.log(` ${d}: $${c.toFixed(6)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
console.log('');
|
|
433
|
+
console.log(`Active Sessions: ${activeSessions}${activeSessionIds.length ? ' (' + activeSessionIds.join(', ') + ')' : ''}`);
|
|
434
|
+
if (summary.totalSelfTraces === 0) {
|
|
435
|
+
console.log('\n(No self-tracked data yet. Use SelfTracker in your agent to record actions.)');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// ---- Agent usage CLI helpers (who, cost, sessions, activity) ----
|
|
439
|
+
function parseSinceDuration(s) {
|
|
440
|
+
if (!s || typeof s !== 'string')
|
|
441
|
+
return undefined;
|
|
442
|
+
const m = s.trim().match(/^(\d+)([smhd])$/i);
|
|
443
|
+
if (!m)
|
|
444
|
+
return undefined;
|
|
445
|
+
const n = parseInt(m[1], 10);
|
|
446
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
447
|
+
return undefined;
|
|
448
|
+
const unit = m[2].toLowerCase();
|
|
449
|
+
const mult = unit === 's' ? 1000 : unit === 'm' ? 60000 : unit === 'h' ? 3600000 : 86400000;
|
|
450
|
+
return Date.now() - n * mult;
|
|
451
|
+
}
|
|
452
|
+
function parseDateInput(d) {
|
|
453
|
+
if (!d || typeof d !== 'string')
|
|
454
|
+
return undefined;
|
|
455
|
+
const t = Date.parse(d);
|
|
456
|
+
if (Number.isFinite(t))
|
|
457
|
+
return t;
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
function formatDuration(ms) {
|
|
461
|
+
if (ms < 1000)
|
|
462
|
+
return `${ms}ms`;
|
|
463
|
+
const totalSec = Math.floor(ms / 1000);
|
|
464
|
+
if (totalSec < 60)
|
|
465
|
+
return `${totalSec}s`;
|
|
466
|
+
const m = Math.floor(totalSec / 60);
|
|
467
|
+
const s = totalSec % 60;
|
|
468
|
+
return `${m}m${s}s`;
|
|
469
|
+
}
|
|
470
|
+
function formatDateShort(ts) {
|
|
471
|
+
if (!ts)
|
|
472
|
+
return '';
|
|
473
|
+
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ');
|
|
474
|
+
}
|
|
475
|
+
function getModelFromRec(r) {
|
|
476
|
+
const meta = r.metadata || {};
|
|
477
|
+
const m = meta.model;
|
|
478
|
+
return typeof m === 'string' ? m : 'unknown';
|
|
479
|
+
}
|
|
480
|
+
function computeAgentCostBreakdown(recs) {
|
|
481
|
+
let total = 0;
|
|
482
|
+
const byAgent = {};
|
|
483
|
+
const byModel = {};
|
|
484
|
+
for (const r of recs) {
|
|
485
|
+
const c = r.costUsd || 0;
|
|
486
|
+
total += c;
|
|
487
|
+
byAgent[r.agentName] = (byAgent[r.agentName] || 0) + c;
|
|
488
|
+
const mod = getModelFromRec(r);
|
|
489
|
+
byModel[mod] = (byModel[mod] || 0) + c;
|
|
490
|
+
}
|
|
491
|
+
return { totalCostUsd: total, costByAgent: byAgent, costByModel: byModel };
|
|
492
|
+
}
|
|
493
|
+
function getPeriodStarts() {
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
const dToday = new Date(now);
|
|
496
|
+
dToday.setHours(0, 0, 0, 0);
|
|
497
|
+
const today = dToday.getTime();
|
|
498
|
+
// week start (monday) reuse logic similar to self-stats
|
|
499
|
+
const dWeek = new Date(now);
|
|
500
|
+
const day = dWeek.getDay();
|
|
501
|
+
const diff = dWeek.getDate() - day + (day === 0 ? -6 : 1);
|
|
502
|
+
dWeek.setDate(diff);
|
|
503
|
+
dWeek.setHours(0, 0, 0, 0);
|
|
504
|
+
const week = dWeek.getTime();
|
|
505
|
+
const dMonth = new Date(now);
|
|
506
|
+
dMonth.setDate(1);
|
|
507
|
+
dMonth.setHours(0, 0, 0, 0);
|
|
508
|
+
const month = dMonth.getTime();
|
|
509
|
+
return { today, week, month, all: 0 };
|
|
510
|
+
}
|
|
511
|
+
function printAgentCostSection(title, bd) {
|
|
512
|
+
console.log(title);
|
|
513
|
+
console.log('='.repeat(title.length));
|
|
514
|
+
console.log(`Total: $${bd.totalCostUsd.toFixed(4)}`);
|
|
515
|
+
// by agent
|
|
516
|
+
const agents = Object.entries(bd.costByAgent).sort(([, a], [, b]) => b - a);
|
|
517
|
+
if (agents.length > 0) {
|
|
518
|
+
console.log('By Agent:');
|
|
519
|
+
for (const [name, c] of agents.slice(0, 10)) {
|
|
520
|
+
console.log(` ${name}: $${c.toFixed(4)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// by model
|
|
524
|
+
const models = Object.entries(bd.costByModel).sort(([, a], [, b]) => b - a);
|
|
525
|
+
if (models.length > 0) {
|
|
526
|
+
console.log('By Model:');
|
|
527
|
+
for (const [m, c] of models.slice(0, 10)) {
|
|
528
|
+
console.log(` ${m}: $${c.toFixed(4)}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
console.log('');
|
|
532
|
+
}
|
|
533
|
+
function printWhoTable(who) {
|
|
534
|
+
const headers = ['Agent', 'Type', 'Session', 'Last Action', 'Actions', 'Tokens', 'Cost'];
|
|
535
|
+
const rows = who.map((w) => [
|
|
536
|
+
w.agentName,
|
|
537
|
+
w.agentType || '',
|
|
538
|
+
w.sessionId ? w.sessionId.substring(0, 8) : '',
|
|
539
|
+
w.lastAction,
|
|
540
|
+
fmtNum(w.actions),
|
|
541
|
+
fmtNum(w.tokens),
|
|
542
|
+
fmtCost(w.costUsd || 0),
|
|
543
|
+
]);
|
|
544
|
+
printTable(headers, rows);
|
|
545
|
+
}
|
|
546
|
+
function printSessionsTable(sessions) {
|
|
547
|
+
const headers = [
|
|
548
|
+
'Session ID',
|
|
549
|
+
'Agent',
|
|
550
|
+
'Started',
|
|
551
|
+
'Duration',
|
|
552
|
+
'Actions',
|
|
553
|
+
'Tokens',
|
|
554
|
+
'Cost',
|
|
555
|
+
'Status',
|
|
556
|
+
];
|
|
557
|
+
const rows = sessions.map((s) => [
|
|
558
|
+
s.sessionId.substring(0, 12),
|
|
559
|
+
s.agentName,
|
|
560
|
+
formatDateShort(s.startedAt),
|
|
561
|
+
formatDuration(s.durationMs),
|
|
562
|
+
fmtNum(s.actions),
|
|
563
|
+
fmtNum(s.tokens),
|
|
564
|
+
fmtCost(s.costUsd || 0),
|
|
565
|
+
colorizeStatus(s.status),
|
|
566
|
+
]);
|
|
567
|
+
printTable(headers, rows);
|
|
568
|
+
}
|
|
569
|
+
function printActivityTimeline(recs) {
|
|
570
|
+
if (recs.length === 0) {
|
|
571
|
+
console.log('No activity found.');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const headers = ['Time', 'Agent', 'Action', 'Tokens', 'Cost', 'Status'];
|
|
575
|
+
const rows = recs.map((r) => [
|
|
576
|
+
formatDateShort(r.createdAt),
|
|
577
|
+
r.agentName,
|
|
578
|
+
r.action + (r.target ? `:${r.target}` : ''),
|
|
579
|
+
fmtNum(r.tokensUsed || 0),
|
|
580
|
+
fmtCost(r.costUsd || 0),
|
|
581
|
+
colorizeStatus(r.status),
|
|
582
|
+
]);
|
|
583
|
+
printTable(headers, rows);
|
|
584
|
+
}
|
|
585
|
+
function printWebhooksTable(webhooks) {
|
|
586
|
+
if (webhooks.length === 0) {
|
|
587
|
+
console.log('No webhooks configured.');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const headers = ['ID', 'URL', 'Events', 'Enabled', 'Last Triggered', 'Failures'];
|
|
591
|
+
const rows = webhooks.map((w) => [
|
|
592
|
+
w.id.substring(0, 8),
|
|
593
|
+
w.url,
|
|
594
|
+
(w.events || []).join(','),
|
|
595
|
+
w.enabled ? 'enabled' : 'disabled',
|
|
596
|
+
w.lastTriggeredAt
|
|
597
|
+
? new Date(w.lastTriggeredAt).toISOString().slice(0, 19).replace('T', ' ')
|
|
598
|
+
: 'never',
|
|
599
|
+
String(w.failureCount || 0),
|
|
600
|
+
]);
|
|
601
|
+
printTable(headers, rows);
|
|
602
|
+
}
|
|
603
|
+
function parseArgs(argv) {
|
|
604
|
+
const args = argv.slice(2);
|
|
605
|
+
// Command is the first non-flag argument
|
|
606
|
+
const command = args.find((a) => typeof a === 'string' && !a.startsWith('-')) || 'help';
|
|
607
|
+
const flags = {};
|
|
608
|
+
for (let i = 0; i < args.length; i++) {
|
|
609
|
+
const arg = args[i];
|
|
610
|
+
if (typeof arg !== 'string' || !arg.startsWith('--'))
|
|
611
|
+
continue;
|
|
612
|
+
// flag
|
|
613
|
+
const eqIdx = arg.indexOf('=');
|
|
614
|
+
if (eqIdx !== -1) {
|
|
615
|
+
const key = arg.slice(2, eqIdx);
|
|
616
|
+
const val = arg.slice(eqIdx + 1);
|
|
617
|
+
flags[key] = val;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const key = arg.slice(2);
|
|
621
|
+
const next = args[i + 1];
|
|
622
|
+
const val = i + 1 < args.length && typeof next === 'string' && !next.startsWith('-')
|
|
623
|
+
? (i++, next)
|
|
624
|
+
: true;
|
|
625
|
+
flags[key] = val;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return { command, flags };
|
|
629
|
+
}
|
|
630
|
+
function getAgentTrace(requireDb = true) {
|
|
631
|
+
const dbp = getDbPath();
|
|
632
|
+
if (requireDb && !existsSync(dbp)) {
|
|
633
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
634
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
return new AgentTrace({ dbPath: dbp, silent: true });
|
|
638
|
+
}
|
|
639
|
+
async function runMain() {
|
|
640
|
+
const { command, flags } = parseArgs(process.argv);
|
|
641
|
+
if (flags.help || command === 'help') {
|
|
642
|
+
printUsage();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const useJson = !!flags.json;
|
|
646
|
+
// detect subcommand for alerts (e.g. alerts list, alerts test)
|
|
647
|
+
const alertsSub = (() => {
|
|
648
|
+
if (command !== 'alerts')
|
|
649
|
+
return undefined;
|
|
650
|
+
const argvArgs = process.argv.slice(2);
|
|
651
|
+
const idx = argvArgs.indexOf('alerts');
|
|
652
|
+
if (idx === -1)
|
|
653
|
+
return undefined;
|
|
654
|
+
for (let k = idx + 1; k < argvArgs.length; k++) {
|
|
655
|
+
const c = argvArgs[k];
|
|
656
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
657
|
+
return c;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return undefined;
|
|
661
|
+
})();
|
|
662
|
+
// detect subcommand for webhooks (e.g. webhook add, webhook list, webhook remove, webhook test)
|
|
663
|
+
const webhookSub = (() => {
|
|
664
|
+
if (command !== 'webhook')
|
|
665
|
+
return undefined;
|
|
666
|
+
const argvArgs = process.argv.slice(2);
|
|
667
|
+
const idx = argvArgs.indexOf('webhook');
|
|
668
|
+
if (idx === -1)
|
|
669
|
+
return undefined;
|
|
670
|
+
for (let k = idx + 1; k < argvArgs.length; k++) {
|
|
671
|
+
const c = argvArgs[k];
|
|
672
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
673
|
+
return c;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
})();
|
|
678
|
+
// capture positional args for webhook commands (used in webhook subcommand handling below)
|
|
679
|
+
const _webhookPositionals = (() => {
|
|
680
|
+
if (command !== 'webhook')
|
|
681
|
+
return [];
|
|
682
|
+
const argvArgs = process.argv.slice(2);
|
|
683
|
+
const idx = argvArgs.indexOf('webhook');
|
|
684
|
+
if (idx === -1)
|
|
685
|
+
return [];
|
|
686
|
+
const result = [];
|
|
687
|
+
for (let k = idx + 1; k < argvArgs.length; k++) {
|
|
688
|
+
const c = argvArgs[k];
|
|
689
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
690
|
+
result.push(c);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// First positional is the subcommand; rest are args
|
|
694
|
+
return result.slice(1);
|
|
695
|
+
})();
|
|
696
|
+
void _webhookPositionals;
|
|
697
|
+
// detect subcommand for retention (e.g. retention show, retention set <days>)
|
|
698
|
+
const _retentionSub = (() => {
|
|
699
|
+
if (command !== 'retention')
|
|
700
|
+
return undefined;
|
|
701
|
+
const argvArgs = process.argv.slice(2);
|
|
702
|
+
const idx = argvArgs.indexOf('retention');
|
|
703
|
+
if (idx === -1)
|
|
704
|
+
return undefined;
|
|
705
|
+
for (let k = idx + 1; k < argvArgs.length; k++) {
|
|
706
|
+
const c = argvArgs[k];
|
|
707
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
708
|
+
return c;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return undefined;
|
|
712
|
+
})();
|
|
713
|
+
void _retentionSub;
|
|
714
|
+
switch (command) {
|
|
715
|
+
case 'init': {
|
|
716
|
+
const dbp = getDbPath();
|
|
717
|
+
if (existsSync(dbp)) {
|
|
718
|
+
console.log(`${dbp} already exists.`);
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
const trace = new AgentTrace({ dbPath: dbp, silent: true });
|
|
722
|
+
trace.close();
|
|
723
|
+
console.log(`Created ${dbp}`);
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
case 'wrap': {
|
|
728
|
+
const argvArgs = process.argv.slice(2);
|
|
729
|
+
const cmd = argvArgs[1];
|
|
730
|
+
if (!cmd) {
|
|
731
|
+
console.error('Usage: agenttrace-io wrap <command> [args...]');
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
const cmdArgs = argvArgs.slice(2);
|
|
735
|
+
const agenttrace = new AgentTrace({ dbPath: getDbPath(), silent: true });
|
|
736
|
+
const runId = agenttrace.startRun(`wrap:${cmd}`);
|
|
737
|
+
void runId;
|
|
738
|
+
const inputStr = `${cmd} ${cmdArgs.join(' ')}`.trim();
|
|
739
|
+
let stdout = '';
|
|
740
|
+
let stderr = '';
|
|
741
|
+
let exitCode;
|
|
742
|
+
try {
|
|
743
|
+
const { spawn } = await import('node:child_process');
|
|
744
|
+
const result = await new Promise((resolve, reject) => {
|
|
745
|
+
const child = spawn(cmd, cmdArgs, { stdio: 'pipe', shell: true });
|
|
746
|
+
child.stdout.on('data', (d) => {
|
|
747
|
+
stdout += d.toString();
|
|
748
|
+
});
|
|
749
|
+
child.stderr.on('data', (d) => {
|
|
750
|
+
stderr += d.toString();
|
|
751
|
+
});
|
|
752
|
+
child.on('close', (code) => {
|
|
753
|
+
exitCode = code ?? 0;
|
|
754
|
+
if (exitCode !== 0) {
|
|
755
|
+
const e = new Error(stderr.slice(0, 500) || `exited with code ${exitCode}`);
|
|
756
|
+
e.exitCode = exitCode;
|
|
757
|
+
reject(e);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
resolve(stdout.slice(0, 2000));
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
child.on('error', (err) => {
|
|
764
|
+
exitCode = 1;
|
|
765
|
+
reject(err);
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
await agenttrace.trace(`wrap:${cmd}`, async () => result, { input: inputStr });
|
|
769
|
+
agenttrace.completeRun('success');
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
agenttrace.completeRun('error');
|
|
773
|
+
const err = e;
|
|
774
|
+
exitCode = err?.exitCode ?? 1;
|
|
775
|
+
if (stderr)
|
|
776
|
+
process.stderr.write(stderr.slice(0, 500));
|
|
777
|
+
agenttrace.close();
|
|
778
|
+
process.exit(exitCode);
|
|
779
|
+
}
|
|
780
|
+
agenttrace.close();
|
|
781
|
+
process.exit(0);
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
case 'dashboard': {
|
|
785
|
+
const rawPort = flags.port ? parseInt(String(flags.port), 10) : NaN;
|
|
786
|
+
const port = Number.isFinite(rawPort) && rawPort > 0 ? rawPort : undefined;
|
|
787
|
+
const host = typeof flags.host === 'string' ? String(flags.host) : undefined;
|
|
788
|
+
startDashboard({ dbPath: getDbPath(), port, host });
|
|
789
|
+
// server keeps process alive
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
case 'runs': {
|
|
793
|
+
const trace = getAgentTrace();
|
|
794
|
+
const rawLimit = flags.limit ? parseInt(String(flags.limit), 10) : NaN;
|
|
795
|
+
const lim = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 20;
|
|
796
|
+
const allRuns = trace.getRuns(Math.max(1, Math.min(1000, lim)));
|
|
797
|
+
const statusRaw = flags.status ? String(flags.status) : '';
|
|
798
|
+
const runs = statusRaw
|
|
799
|
+
? (() => {
|
|
800
|
+
const allowed = statusRaw
|
|
801
|
+
.split(',')
|
|
802
|
+
.map((s) => s.trim())
|
|
803
|
+
.filter(Boolean);
|
|
804
|
+
return allowed.length ? allRuns.filter((r) => allowed.includes(r.status)) : allRuns;
|
|
805
|
+
})()
|
|
806
|
+
: allRuns;
|
|
807
|
+
trace.close();
|
|
808
|
+
if (useJson) {
|
|
809
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
810
|
+
}
|
|
811
|
+
else if (runs.length === 0) {
|
|
812
|
+
console.log('No runs found.');
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
printRunsTable(runs);
|
|
816
|
+
}
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case 'traces': {
|
|
820
|
+
const trace = getAgentTrace();
|
|
821
|
+
const rawLimit = flags.limit ? parseInt(String(flags.limit), 10) : NaN;
|
|
822
|
+
const lim = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 50;
|
|
823
|
+
const runId = (flags['run-id'] || flags.runId || flags['runId']);
|
|
824
|
+
const filter = {
|
|
825
|
+
limit: Math.max(1, Math.min(1000, lim)),
|
|
826
|
+
};
|
|
827
|
+
if (runId) {
|
|
828
|
+
filter.runId = String(runId);
|
|
829
|
+
}
|
|
830
|
+
const statusRaw = flags.status ? String(flags.status) : '';
|
|
831
|
+
if (statusRaw) {
|
|
832
|
+
const statuses = statusRaw
|
|
833
|
+
.split(',')
|
|
834
|
+
.map((s) => s.trim())
|
|
835
|
+
.filter(Boolean);
|
|
836
|
+
if (statuses.length) {
|
|
837
|
+
filter.status = statuses;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const traces = trace.getTraces(filter);
|
|
841
|
+
trace.close();
|
|
842
|
+
if (useJson) {
|
|
843
|
+
console.log(JSON.stringify(traces, null, 2));
|
|
844
|
+
}
|
|
845
|
+
else if (traces.length === 0) {
|
|
846
|
+
console.log('No traces found.');
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
printTracesTable(traces);
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case 'stats': {
|
|
854
|
+
const trace = getAgentTrace();
|
|
855
|
+
const stats = trace.getStats();
|
|
856
|
+
trace.close();
|
|
857
|
+
if (useJson) {
|
|
858
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
printStats(stats);
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case 'costs': {
|
|
866
|
+
const trace = getAgentTrace();
|
|
867
|
+
const runId = (flags['run-id'] || flags.runId || flags['runId']);
|
|
868
|
+
const daily = !!flags.daily;
|
|
869
|
+
const breakdown = trace.getCostBreakdown({ runId: runId ? String(runId) : undefined });
|
|
870
|
+
trace.close();
|
|
871
|
+
if (useJson) {
|
|
872
|
+
console.log(JSON.stringify(breakdown, null, 2));
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
printCosts(breakdown, daily);
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
case 'export': {
|
|
880
|
+
const trace = getAgentTrace();
|
|
881
|
+
const format = flags.format === 'csv' ? 'csv' : 'json';
|
|
882
|
+
const runId = (flags['run-id'] || flags.runId || flags['runId']);
|
|
883
|
+
const filter = {};
|
|
884
|
+
if (runId) {
|
|
885
|
+
filter.runId = String(runId);
|
|
886
|
+
}
|
|
887
|
+
const data = trace.export(format, filter);
|
|
888
|
+
trace.close();
|
|
889
|
+
const outFile = flags.output ? String(flags.output) : '';
|
|
890
|
+
if (outFile) {
|
|
891
|
+
writeFileSync(outFile, data, 'utf8');
|
|
892
|
+
console.log(`Exported ${format.toUpperCase()} to ${outFile}`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
console.log(data);
|
|
896
|
+
}
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
case 'tree': {
|
|
900
|
+
const traceId = (flags['trace-id'] || flags.traceId || flags['traceId']);
|
|
901
|
+
if (!traceId) {
|
|
902
|
+
console.error('Usage: agenttrace-io tree --trace-id <id>');
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
const tr = getAgentTrace();
|
|
906
|
+
const tree = (() => {
|
|
907
|
+
try {
|
|
908
|
+
return tr.getTraceTree(String(traceId));
|
|
909
|
+
}
|
|
910
|
+
catch (e) {
|
|
911
|
+
console.error('Error:', e instanceof Error ? e.message : String(e));
|
|
912
|
+
tr.close();
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
})();
|
|
916
|
+
tr.close();
|
|
917
|
+
if (useJson) {
|
|
918
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
919
|
+
}
|
|
920
|
+
else if (!tree || !tree.trace) {
|
|
921
|
+
console.log('Trace not found.');
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
console.log('Trace Tree:');
|
|
925
|
+
printTraceTree(tree);
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
case 'alerts': {
|
|
930
|
+
const sub = alertsSub || 'list';
|
|
931
|
+
const dbp = getDbPath();
|
|
932
|
+
const dbExists = existsSync(dbp);
|
|
933
|
+
if (!dbExists && (sub === 'test' || sub === 'history')) {
|
|
934
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
935
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
if (sub === 'list' || sub === '') {
|
|
939
|
+
if (!dbExists) {
|
|
940
|
+
if (useJson) {
|
|
941
|
+
console.log('[]');
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
console.log('No alerts configured.');
|
|
945
|
+
}
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
const agent = new AgentTrace({ dbPath: dbp, silent: true });
|
|
949
|
+
const alerts = agent.getAlerts();
|
|
950
|
+
agent.close();
|
|
951
|
+
if (useJson) {
|
|
952
|
+
console.log(JSON.stringify(alerts, null, 2));
|
|
953
|
+
}
|
|
954
|
+
else if (alerts.length === 0) {
|
|
955
|
+
console.log('No alerts configured.');
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
console.log('Configured alerts:');
|
|
959
|
+
for (const a of alerts) {
|
|
960
|
+
const parts = [`cooldown=${a.cooldown}s`];
|
|
961
|
+
if (a.webhook)
|
|
962
|
+
parts.push('webhook');
|
|
963
|
+
if (a.email)
|
|
964
|
+
parts.push('email');
|
|
965
|
+
const last = a.lastTriggered
|
|
966
|
+
? new Date(a.lastTriggered).toISOString().slice(0, 19)
|
|
967
|
+
: 'never';
|
|
968
|
+
console.log(` ${a.name} (${parts.join(', ')}) lastTriggered=${last}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
const agent = new AgentTrace({ dbPath: dbp, silent: true });
|
|
974
|
+
if (sub === 'history') {
|
|
975
|
+
const history = agent.getAlertHistory();
|
|
976
|
+
agent.close();
|
|
977
|
+
if (useJson) {
|
|
978
|
+
console.log(JSON.stringify(history, null, 2));
|
|
979
|
+
}
|
|
980
|
+
else if (history.length === 0) {
|
|
981
|
+
console.log('No alert history.');
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
console.log('Alert history (newest first):');
|
|
985
|
+
for (const h of history.slice(0, 100)) {
|
|
986
|
+
const t = new Date(h.triggeredAt).toISOString().slice(0, 19).replace('T', ' ');
|
|
987
|
+
const del = h.delivered ? 'delivered' : `failed${h.error ? ' (' + h.error + ')' : ''}`;
|
|
988
|
+
console.log(` ${t} ${h.alertName} ${del}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
if (sub === 'test') {
|
|
994
|
+
const name = flags.name || flags['name'] ? String(flags.name || flags['name']) : '';
|
|
995
|
+
if (!name) {
|
|
996
|
+
console.error('Usage: agenttrace-io alerts test --name <name>');
|
|
997
|
+
agent.close();
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
const alerts = agent.getAlerts();
|
|
1001
|
+
const def = alerts.find((a) => a.name === name);
|
|
1002
|
+
if (!def) {
|
|
1003
|
+
console.error(`Alert '${name}' not found. Register it via the SDK first.`);
|
|
1004
|
+
agent.close();
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
// Force a test by registering a temp always-true version (bypasses cooldown + uses stored config)
|
|
1008
|
+
const testAlert = {
|
|
1009
|
+
name: def.name,
|
|
1010
|
+
condition: () => true,
|
|
1011
|
+
webhook: def.webhook,
|
|
1012
|
+
email: def.email,
|
|
1013
|
+
cooldown: 0,
|
|
1014
|
+
lastTriggered: 0,
|
|
1015
|
+
};
|
|
1016
|
+
agent.registerAlert(testAlert);
|
|
1017
|
+
const fired = await agent.checkAlerts();
|
|
1018
|
+
agent.close();
|
|
1019
|
+
if (fired.length > 0) {
|
|
1020
|
+
const f = fired[0];
|
|
1021
|
+
const outcome = f.delivered ? 'delivered' : `failed${f.error ? ': ' + f.error : ''}`;
|
|
1022
|
+
console.log(`Test-fired alert '${name}'. ${outcome}`);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
console.log(`Alert '${name}' test did not fire.`);
|
|
1026
|
+
}
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
console.error(`Unknown alerts subcommand: ${sub}`);
|
|
1030
|
+
printUsage();
|
|
1031
|
+
agent.close();
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
case 'health': {
|
|
1036
|
+
const dbp = getDbPath();
|
|
1037
|
+
const useJsonLocal = useJson;
|
|
1038
|
+
const GREEN = '\x1b[32m';
|
|
1039
|
+
const RED = '\x1b[31m';
|
|
1040
|
+
const YELLOW = '\x1b[33m';
|
|
1041
|
+
const RESET = '\x1b[0m';
|
|
1042
|
+
const checks = {};
|
|
1043
|
+
// Database check (always; creates empty db if absent like init)
|
|
1044
|
+
let dbOk = false;
|
|
1045
|
+
let dbTraceCount = 0;
|
|
1046
|
+
let dbSize = 0;
|
|
1047
|
+
let dbIntegrity;
|
|
1048
|
+
try {
|
|
1049
|
+
const tr = new AgentTrace({ dbPath: dbp, silent: true });
|
|
1050
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1051
|
+
const h = tr.getHealth();
|
|
1052
|
+
tr.close();
|
|
1053
|
+
dbOk = h.status === 'ok';
|
|
1054
|
+
dbTraceCount = h.traceCount;
|
|
1055
|
+
dbSize = h.dbSize;
|
|
1056
|
+
dbIntegrity = h.integrity;
|
|
1057
|
+
checks.database = {
|
|
1058
|
+
ok: dbOk,
|
|
1059
|
+
dbPath: dbp,
|
|
1060
|
+
traceCount: dbTraceCount,
|
|
1061
|
+
dbSize,
|
|
1062
|
+
integrity: dbIntegrity,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
catch (e) {
|
|
1066
|
+
checks.database = { ok: false, dbPath: dbp, error: String(e) };
|
|
1067
|
+
}
|
|
1068
|
+
// Dashboard check (local default)
|
|
1069
|
+
let dashOk = false;
|
|
1070
|
+
const dashPort = 4317;
|
|
1071
|
+
const dashUrl = `http://127.0.0.1:${dashPort}/api/health`;
|
|
1072
|
+
try {
|
|
1073
|
+
const controller = new AbortController();
|
|
1074
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
1075
|
+
const resp = await fetch(dashUrl, { signal: controller.signal, method: 'GET' });
|
|
1076
|
+
clearTimeout(timeout);
|
|
1077
|
+
if (resp.ok) {
|
|
1078
|
+
const data = (await resp.json());
|
|
1079
|
+
dashOk = data && data.status === 'ok';
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch (_) {
|
|
1083
|
+
// not reachable
|
|
1084
|
+
}
|
|
1085
|
+
checks.dashboard = { ok: dashOk, url: dashUrl };
|
|
1086
|
+
// Gateway: treat as core data access (db/sdk layer)
|
|
1087
|
+
const gatewayOk = dbOk;
|
|
1088
|
+
checks.gateway = { ok: gatewayOk };
|
|
1089
|
+
const overallOk = gatewayOk && dbOk;
|
|
1090
|
+
if (useJsonLocal) {
|
|
1091
|
+
console.log(JSON.stringify({
|
|
1092
|
+
status: overallOk ? 'ok' : 'degraded',
|
|
1093
|
+
checks,
|
|
1094
|
+
}, null, 2));
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
console.log('AgentTrace Health');
|
|
1098
|
+
console.log('=================');
|
|
1099
|
+
const gStr = gatewayOk ? `${GREEN}ok${RESET}` : `${RED}fail${RESET}`;
|
|
1100
|
+
console.log(`gateway: ${gStr}`);
|
|
1101
|
+
const dStr = dashOk ? `${GREEN}ok${RESET}` : `${YELLOW}not running${RESET}`;
|
|
1102
|
+
console.log(`dashboard: ${dStr} (${dashUrl})`);
|
|
1103
|
+
const dbStr = dbOk ? `${GREEN}ok${RESET}` : `${RED}fail${RESET}`;
|
|
1104
|
+
const extra = dbOk ? ` (traces=${dbTraceCount}, size=${dbSize}B)` : '';
|
|
1105
|
+
console.log(`database: ${dbStr} (${dbp})${extra}`);
|
|
1106
|
+
if (dbOk && dbIntegrity && (!dbIntegrity.tablesExist || !dbIntegrity.noOrphans)) {
|
|
1107
|
+
console.log(` ${YELLOW}integrity: tablesExist=${dbIntegrity.tablesExist}, noOrphans=${dbIntegrity.noOrphans}${dbIntegrity.details ? ' ' + dbIntegrity.details : ''}${RESET}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (!overallOk) {
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case 'version': {
|
|
1116
|
+
console.log(`${PACKAGE_NAME} ${VERSION}`);
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
case 'self-stats': {
|
|
1120
|
+
const dbp = getDbPath();
|
|
1121
|
+
// self-stats creates db on demand (no error if missing; will just show zeros)
|
|
1122
|
+
const storage = new TraceStorage(dbp);
|
|
1123
|
+
try {
|
|
1124
|
+
printSelfStats(storage, useJson);
|
|
1125
|
+
}
|
|
1126
|
+
finally {
|
|
1127
|
+
storage.close();
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case 'who': {
|
|
1132
|
+
const dbp = getDbPath();
|
|
1133
|
+
const storage = new TraceStorage(dbp);
|
|
1134
|
+
try {
|
|
1135
|
+
const activeOnly = !!flags.active;
|
|
1136
|
+
const typeF = flags.type ? String(flags.type) : undefined;
|
|
1137
|
+
const rawLim = flags.limit ? parseInt(String(flags.limit), 10) : NaN;
|
|
1138
|
+
const lim = Number.isFinite(rawLim) && rawLim > 0 ? rawLim : 50;
|
|
1139
|
+
const who = storage.getAgentWho({ activeOnly, agentType: typeF, limit: lim });
|
|
1140
|
+
if (useJson) {
|
|
1141
|
+
console.log(JSON.stringify(who, null, 2));
|
|
1142
|
+
}
|
|
1143
|
+
else if (who.length === 0) {
|
|
1144
|
+
console.log('No agents found.');
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
printWhoTable(who);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
finally {
|
|
1151
|
+
storage.close();
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
case 'cost': {
|
|
1156
|
+
const dbp = getDbPath();
|
|
1157
|
+
const storage = new TraceStorage(dbp);
|
|
1158
|
+
try {
|
|
1159
|
+
const agentF = flags.agent ? String(flags.agent) : undefined;
|
|
1160
|
+
const from = parseDateInput(flags.from);
|
|
1161
|
+
const to = parseDateInput(flags.to);
|
|
1162
|
+
const fmt = (flags.format ? String(flags.format) : 'table').toLowerCase();
|
|
1163
|
+
const isJson = fmt === 'json' || useJson;
|
|
1164
|
+
const allRecs = storage.getAgentUsage({
|
|
1165
|
+
agentName: agentF,
|
|
1166
|
+
limit: 50000,
|
|
1167
|
+
});
|
|
1168
|
+
if (from || to) {
|
|
1169
|
+
const filtered = allRecs.filter((r) => {
|
|
1170
|
+
if (from && r.createdAt < from)
|
|
1171
|
+
return false;
|
|
1172
|
+
if (to && r.createdAt > to)
|
|
1173
|
+
return false;
|
|
1174
|
+
return true;
|
|
1175
|
+
});
|
|
1176
|
+
const bd = computeAgentCostBreakdown(filtered);
|
|
1177
|
+
if (isJson) {
|
|
1178
|
+
console.log(JSON.stringify({
|
|
1179
|
+
range: {
|
|
1180
|
+
from: from ? new Date(from).toISOString() : null,
|
|
1181
|
+
to: to ? new Date(to).toISOString() : null,
|
|
1182
|
+
},
|
|
1183
|
+
agent: agentF || null,
|
|
1184
|
+
...bd,
|
|
1185
|
+
}, null, 2));
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
const title = `Agent Cost Breakdown (custom range)${agentF ? ` for ${agentF}` : ''}`;
|
|
1189
|
+
printAgentCostSection(title, bd);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
// show 4 periods + breakdowns
|
|
1194
|
+
const periods = getPeriodStarts();
|
|
1195
|
+
const todayRecs = allRecs.filter((r) => r.createdAt >= periods.today);
|
|
1196
|
+
const weekRecs = allRecs.filter((r) => r.createdAt >= periods.week);
|
|
1197
|
+
const monthRecs = allRecs.filter((r) => r.createdAt >= periods.month);
|
|
1198
|
+
const allBd = computeAgentCostBreakdown(allRecs);
|
|
1199
|
+
const todayBd = computeAgentCostBreakdown(todayRecs);
|
|
1200
|
+
const weekBd = computeAgentCostBreakdown(weekRecs);
|
|
1201
|
+
const monthBd = computeAgentCostBreakdown(monthRecs);
|
|
1202
|
+
if (isJson) {
|
|
1203
|
+
console.log(JSON.stringify({
|
|
1204
|
+
agent: agentF || null,
|
|
1205
|
+
today: todayBd,
|
|
1206
|
+
week: weekBd,
|
|
1207
|
+
month: monthBd,
|
|
1208
|
+
allTime: allBd,
|
|
1209
|
+
}, null, 2));
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
console.log('Agent Cost Breakdown');
|
|
1213
|
+
console.log('====================');
|
|
1214
|
+
if (agentF)
|
|
1215
|
+
console.log(`Filter: agent=${agentF}`);
|
|
1216
|
+
console.log('');
|
|
1217
|
+
printAgentCostSection('Today', todayBd);
|
|
1218
|
+
printAgentCostSection('This Week', weekBd);
|
|
1219
|
+
printAgentCostSection('This Month', monthBd);
|
|
1220
|
+
printAgentCostSection('All Time', allBd);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
finally {
|
|
1225
|
+
storage.close();
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
case 'sessions': {
|
|
1230
|
+
const dbp = getDbPath();
|
|
1231
|
+
const storage = new TraceStorage(dbp);
|
|
1232
|
+
try {
|
|
1233
|
+
const agentF = flags.agent ? String(flags.agent) : undefined;
|
|
1234
|
+
const activeOnly = !!flags.active;
|
|
1235
|
+
const rawLim = flags.limit ? parseInt(String(flags.limit), 10) : NaN;
|
|
1236
|
+
const lim = Number.isFinite(rawLim) && rawLim > 0 ? rawLim : 20;
|
|
1237
|
+
const sessions = storage.getAgentSessions({ agentName: agentF, activeOnly, limit: lim });
|
|
1238
|
+
if (useJson) {
|
|
1239
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
1240
|
+
}
|
|
1241
|
+
else if (sessions.length === 0) {
|
|
1242
|
+
console.log('No sessions found.');
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
printSessionsTable(sessions);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
storage.close();
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
case 'activity': {
|
|
1254
|
+
const dbp = getDbPath();
|
|
1255
|
+
const storage = new TraceStorage(dbp);
|
|
1256
|
+
try {
|
|
1257
|
+
const agentF = flags.agent ? String(flags.agent) : undefined;
|
|
1258
|
+
const actionF = flags.type ? String(flags.type) : undefined; // --type means action
|
|
1259
|
+
const rawLim = flags.limit ? parseInt(String(flags.limit), 10) : NaN;
|
|
1260
|
+
const lim = Number.isFinite(rawLim) && rawLim > 0 ? rawLim : 30;
|
|
1261
|
+
const sinceFrom = parseSinceDuration(flags.since);
|
|
1262
|
+
const f = { limit: lim };
|
|
1263
|
+
if (agentF)
|
|
1264
|
+
f.agentName = agentF;
|
|
1265
|
+
if (actionF)
|
|
1266
|
+
f.action = actionF;
|
|
1267
|
+
if (sinceFrom)
|
|
1268
|
+
f.fromDate = sinceFrom;
|
|
1269
|
+
const recs = storage.getAgentUsage(f);
|
|
1270
|
+
if (useJson) {
|
|
1271
|
+
console.log(JSON.stringify(recs, null, 2));
|
|
1272
|
+
}
|
|
1273
|
+
else if (recs.length === 0) {
|
|
1274
|
+
console.log('No activity found.');
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
printActivityTimeline(recs);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
storage.close();
|
|
1282
|
+
}
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
case 'webhook': {
|
|
1286
|
+
const sub = webhookSub || 'list';
|
|
1287
|
+
const dbp = getDbPath();
|
|
1288
|
+
const dbExists = existsSync(dbp);
|
|
1289
|
+
if (sub === 'list' || sub === '') {
|
|
1290
|
+
if (!dbExists) {
|
|
1291
|
+
if (useJson) {
|
|
1292
|
+
console.log('[]');
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
console.log('No webhooks configured.');
|
|
1296
|
+
}
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
const agent = new AgentTrace({ dbPath: dbp, silent: true });
|
|
1300
|
+
try {
|
|
1301
|
+
const webhooks = agent.getWebhooks();
|
|
1302
|
+
if (useJson) {
|
|
1303
|
+
console.log(JSON.stringify(webhooks, null, 2));
|
|
1304
|
+
}
|
|
1305
|
+
else if (webhooks.length === 0) {
|
|
1306
|
+
console.log('No webhooks configured.');
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
printWebhooksTable(webhooks);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
finally {
|
|
1313
|
+
agent.close();
|
|
1314
|
+
}
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
// For add/remove/test, require existing db
|
|
1318
|
+
if (!dbExists) {
|
|
1319
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
1320
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
const agent = new AgentTrace({ dbPath: dbp, silent: true });
|
|
1324
|
+
try {
|
|
1325
|
+
if (sub === 'add') {
|
|
1326
|
+
const url = flags.url ? String(flags.url) : '';
|
|
1327
|
+
const eventsRaw = flags.events ? String(flags.events) : '';
|
|
1328
|
+
if (!url) {
|
|
1329
|
+
console.error('Usage: agenttrace-io webhook add --url <url> [--events <event1,event2,...>]');
|
|
1330
|
+
console.error('Events: trace.complete, trace.error, run.complete, run.error, cost.threshold, agent.inactive');
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
const defaultEvents = [
|
|
1334
|
+
'trace.complete',
|
|
1335
|
+
'trace.error',
|
|
1336
|
+
'run.complete',
|
|
1337
|
+
'run.error',
|
|
1338
|
+
'cost.threshold',
|
|
1339
|
+
'agent.inactive',
|
|
1340
|
+
];
|
|
1341
|
+
const events = eventsRaw
|
|
1342
|
+
? eventsRaw
|
|
1343
|
+
.split(',')
|
|
1344
|
+
.map((e) => e.trim())
|
|
1345
|
+
.filter(Boolean)
|
|
1346
|
+
: defaultEvents;
|
|
1347
|
+
const id = agent.addWebhook(url, events);
|
|
1348
|
+
if (useJson) {
|
|
1349
|
+
console.log(JSON.stringify({ id, url, events }, null, 2));
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
console.log(`Registered webhook: ${id.substring(0, 8)} -> ${url} (${events.join(',')})`);
|
|
1353
|
+
}
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
if (sub === 'remove') {
|
|
1357
|
+
const id = flags.id ? String(flags.id) : '';
|
|
1358
|
+
if (!id) {
|
|
1359
|
+
console.error('Usage: agenttrace-io webhook remove --id <id>');
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
const webhooks = agent.getWebhooks();
|
|
1363
|
+
const match = webhooks.find((w) => w.id.startsWith(id) || w.id === id);
|
|
1364
|
+
if (!match) {
|
|
1365
|
+
console.error(`Webhook '${id}' not found.`);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
agent.removeWebhook(match.id);
|
|
1369
|
+
if (useJson) {
|
|
1370
|
+
console.log(JSON.stringify({ id: match.id, removed: true }, null, 2));
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
console.log(`Removed webhook ${match.id.substring(0, 8)}.`);
|
|
1374
|
+
}
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
if (sub === 'test') {
|
|
1378
|
+
const id = flags.id ? String(flags.id) : '';
|
|
1379
|
+
if (!id) {
|
|
1380
|
+
console.error('Usage: agenttrace-io webhook test --id <id>');
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
const webhooks = agent.getWebhooks();
|
|
1384
|
+
const match = webhooks.find((w) => w.id.startsWith(id) || w.id === id);
|
|
1385
|
+
if (!match) {
|
|
1386
|
+
console.error(`Webhook '${id}' not found.`);
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
const result = await agent.testWebhook(match.id);
|
|
1390
|
+
if (useJson) {
|
|
1391
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1392
|
+
}
|
|
1393
|
+
else if (result.ok) {
|
|
1394
|
+
console.log(`Webhook ${match.id.substring(0, 8)} delivered (HTTP ${result.status}).`);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
console.log(`Webhook ${match.id.substring(0, 8)} failed: ${result.error || `HTTP ${result.status}`}`);
|
|
1398
|
+
}
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
console.error(`Unknown webhook subcommand: ${sub}`);
|
|
1402
|
+
printUsage();
|
|
1403
|
+
process.exit(1);
|
|
1404
|
+
}
|
|
1405
|
+
finally {
|
|
1406
|
+
agent.close();
|
|
1407
|
+
}
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
case 'cleanup': {
|
|
1411
|
+
const dbp = getDbPath();
|
|
1412
|
+
if (!existsSync(dbp)) {
|
|
1413
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
1414
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1417
|
+
const storage = new TraceStorage(dbp);
|
|
1418
|
+
const rawDays = flags.days ? parseInt(String(flags.days), 10) : NaN;
|
|
1419
|
+
const days = Number.isFinite(rawDays) && rawDays > 0 ? rawDays : 30;
|
|
1420
|
+
const cutoff = Date.now() - days * 86400000;
|
|
1421
|
+
const tracesDeleted = storage.cleanupOldTraces(cutoff);
|
|
1422
|
+
const runsDeleted = storage.cleanupOldRuns(cutoff);
|
|
1423
|
+
const usageDeleted = storage.cleanupOldAgentUsage(cutoff);
|
|
1424
|
+
storage.close();
|
|
1425
|
+
if (useJson) {
|
|
1426
|
+
console.log(JSON.stringify({ tracesDeleted, runsDeleted, usageDeleted, days }, null, 2));
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
console.log(`Cleanup complete (older than ${days} days):`);
|
|
1430
|
+
console.log(` Traces deleted: ${tracesDeleted}`);
|
|
1431
|
+
console.log(` Runs deleted: ${runsDeleted}`);
|
|
1432
|
+
console.log(` Usage deleted: ${usageDeleted}`);
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
case 'retention': {
|
|
1437
|
+
const dbp = getDbPath();
|
|
1438
|
+
const dbExists = existsSync(dbp);
|
|
1439
|
+
const argvArgs2 = process.argv.slice(2);
|
|
1440
|
+
const idx2 = argvArgs2.indexOf('retention');
|
|
1441
|
+
let sub = 'show';
|
|
1442
|
+
if (idx2 !== -1) {
|
|
1443
|
+
for (let k = idx2 + 1; k < argvArgs2.length; k++) {
|
|
1444
|
+
const c = argvArgs2[k];
|
|
1445
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
1446
|
+
sub = c;
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (sub === 'show' || sub === 'stats') {
|
|
1452
|
+
if (!dbExists) {
|
|
1453
|
+
if (useJson) {
|
|
1454
|
+
console.log(JSON.stringify({ error: 'no database' }, null, 2));
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
console.log('No database found. Run "agenttrace-io init" first.');
|
|
1458
|
+
}
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
const storage = new TraceStorage(dbp);
|
|
1462
|
+
const policy = storage.getRetentionPolicy();
|
|
1463
|
+
const stats = storage.getStorageStats();
|
|
1464
|
+
storage.close();
|
|
1465
|
+
if (useJson) {
|
|
1466
|
+
console.log(JSON.stringify({ policy, stats }, null, 2));
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
console.log('Retention Policy:');
|
|
1470
|
+
console.log(` Retention days: ${policy.retentionDays}`);
|
|
1471
|
+
console.log(` Cleanup interval (hrs): ${policy.cleanupIntervalHours}`);
|
|
1472
|
+
console.log('');
|
|
1473
|
+
console.log('Storage Stats:');
|
|
1474
|
+
console.log(` DB size: ${stats.totalSizeBytes} bytes`);
|
|
1475
|
+
console.log(` Trace count: ${stats.traceCount}`);
|
|
1476
|
+
console.log(` Run count: ${stats.runCount}`);
|
|
1477
|
+
if (stats.oldestTrace) {
|
|
1478
|
+
console.log(` Oldest trace: ${new Date(stats.oldestTrace).toISOString().slice(0, 19)}`);
|
|
1479
|
+
}
|
|
1480
|
+
if (stats.newestTrace) {
|
|
1481
|
+
console.log(` Newest trace: ${new Date(stats.newestTrace).toISOString().slice(0, 19)}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
if (!dbExists) {
|
|
1487
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
1488
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
const storage = new TraceStorage(dbp);
|
|
1492
|
+
if (sub === 'set') {
|
|
1493
|
+
const rawDays = flags.days ? parseInt(String(flags.days), 10) : NaN;
|
|
1494
|
+
if (!Number.isFinite(rawDays) || rawDays < 0) {
|
|
1495
|
+
console.error('Usage: agenttrace-io retention set --days <N> [--interval <H>]');
|
|
1496
|
+
storage.close();
|
|
1497
|
+
process.exit(1);
|
|
1498
|
+
}
|
|
1499
|
+
const interval = flags.interval ? parseInt(String(flags.interval), 10) : undefined;
|
|
1500
|
+
storage.setRetentionPolicy(rawDays, Number.isFinite(interval) && interval > 0 ? interval : undefined);
|
|
1501
|
+
storage.close();
|
|
1502
|
+
if (useJson) {
|
|
1503
|
+
console.log(JSON.stringify({ retentionDays: rawDays, cleanupIntervalHours: interval || 24 }, null, 2));
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
console.log(`Retention policy set: ${rawDays} days, cleanup every ${interval || 24}h`);
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
console.error(`Unknown retention subcommand: ${sub}`);
|
|
1511
|
+
printUsage();
|
|
1512
|
+
storage.close();
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
case 'key': {
|
|
1517
|
+
const dbp = getDbPath();
|
|
1518
|
+
const dbExists = existsSync(dbp);
|
|
1519
|
+
const argvArgs3 = process.argv.slice(2);
|
|
1520
|
+
const idx3 = argvArgs3.indexOf('key');
|
|
1521
|
+
let sub = 'list';
|
|
1522
|
+
if (idx3 !== -1) {
|
|
1523
|
+
for (let k = idx3 + 1; k < argvArgs3.length; k++) {
|
|
1524
|
+
const c = argvArgs3[k];
|
|
1525
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
1526
|
+
sub = c;
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (sub === 'list') {
|
|
1532
|
+
if (!dbExists) {
|
|
1533
|
+
if (useJson) {
|
|
1534
|
+
console.log('[]');
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
console.log('No API keys found.');
|
|
1538
|
+
}
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
const storage = new TraceStorage(dbp);
|
|
1542
|
+
const keys = storage.getApiKeys();
|
|
1543
|
+
storage.close();
|
|
1544
|
+
if (useJson) {
|
|
1545
|
+
console.log(JSON.stringify(keys, null, 2));
|
|
1546
|
+
}
|
|
1547
|
+
else if (keys.length === 0) {
|
|
1548
|
+
console.log('No API keys found.');
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
console.log('API Keys:');
|
|
1552
|
+
for (const k of keys) {
|
|
1553
|
+
const status = k.enabled ? 'enabled' : 'disabled';
|
|
1554
|
+
const last = k.lastUsedAt ? new Date(k.lastUsedAt).toISOString().slice(0, 19) : 'never';
|
|
1555
|
+
console.log(` ${k.id.substring(0, 8)} ${k.name} [${status}] last_used=${last}`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
if (!dbExists) {
|
|
1561
|
+
console.error(`No ${dbp} found in current directory.`);
|
|
1562
|
+
console.error('Run "agenttrace-io init" to create one.');
|
|
1563
|
+
process.exit(1);
|
|
1564
|
+
}
|
|
1565
|
+
const storage = new TraceStorage(dbp);
|
|
1566
|
+
if (sub === 'create') {
|
|
1567
|
+
const name = flags.name ? String(flags.name) : '';
|
|
1568
|
+
if (!name) {
|
|
1569
|
+
console.error('Usage: agenttrace-io key create --name <name>');
|
|
1570
|
+
storage.close();
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
const created = storage.createApiKey(name);
|
|
1574
|
+
storage.close();
|
|
1575
|
+
if (useJson) {
|
|
1576
|
+
console.log(JSON.stringify({
|
|
1577
|
+
id: created.id,
|
|
1578
|
+
name: created.name,
|
|
1579
|
+
key: created.key,
|
|
1580
|
+
preview: created.preview,
|
|
1581
|
+
createdAt: created.createdAt,
|
|
1582
|
+
}, null, 2));
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
console.log(`Created API key: ${created.name}`);
|
|
1586
|
+
console.log(` ID: ${created.id.substring(0, 8)}`);
|
|
1587
|
+
console.log(` Key: ${created.key}`);
|
|
1588
|
+
console.log(` Preview: ${created.preview}`);
|
|
1589
|
+
console.log(' (Store this key — it will not be shown again)');
|
|
1590
|
+
}
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
if (sub === 'revoke' || sub === 'delete' || sub === 'remove') {
|
|
1594
|
+
const id = flags.id ? String(flags.id) : '';
|
|
1595
|
+
if (!id) {
|
|
1596
|
+
console.error('Usage: agenttrace-io key revoke --id <id>');
|
|
1597
|
+
storage.close();
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
storage.revokeApiKey(id);
|
|
1601
|
+
storage.close();
|
|
1602
|
+
if (useJson) {
|
|
1603
|
+
console.log(JSON.stringify({ revoked: true, id }, null, 2));
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
console.log(`Revoked API key ${id.substring(0, 8)}`);
|
|
1607
|
+
}
|
|
1608
|
+
break;
|
|
1609
|
+
}
|
|
1610
|
+
console.error(`Unknown key subcommand: ${sub}`);
|
|
1611
|
+
printUsage();
|
|
1612
|
+
storage.close();
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
case 'benchmark': {
|
|
1617
|
+
const trace = getAgentTrace();
|
|
1618
|
+
const results = [];
|
|
1619
|
+
// Write benchmark
|
|
1620
|
+
{
|
|
1621
|
+
const start = Date.now();
|
|
1622
|
+
const ops = 100;
|
|
1623
|
+
for (let i = 0; i < ops; i++) {
|
|
1624
|
+
trace.startRun(`bench-${i}`);
|
|
1625
|
+
trace.completeRun();
|
|
1626
|
+
}
|
|
1627
|
+
const durationMs = Date.now() - start;
|
|
1628
|
+
results.push({
|
|
1629
|
+
name: 'write',
|
|
1630
|
+
ops,
|
|
1631
|
+
durationMs,
|
|
1632
|
+
opsPerSec: Math.round((ops / durationMs) * 1000),
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
// Read benchmark
|
|
1636
|
+
{
|
|
1637
|
+
const start = Date.now();
|
|
1638
|
+
const runs = trace.getRuns(1000);
|
|
1639
|
+
const durationMs = Date.now() - start;
|
|
1640
|
+
results.push({
|
|
1641
|
+
name: 'read',
|
|
1642
|
+
ops: runs.length,
|
|
1643
|
+
durationMs,
|
|
1644
|
+
opsPerSec: Math.round((runs.length / Math.max(1, durationMs)) * 1000),
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
// Stats benchmark
|
|
1648
|
+
{
|
|
1649
|
+
const start = Date.now();
|
|
1650
|
+
const iterations = 10;
|
|
1651
|
+
for (let i = 0; i < iterations; i++) {
|
|
1652
|
+
trace.getStats();
|
|
1653
|
+
}
|
|
1654
|
+
const durationMs = Date.now() - start;
|
|
1655
|
+
results.push({
|
|
1656
|
+
name: 'stats',
|
|
1657
|
+
ops: iterations,
|
|
1658
|
+
durationMs,
|
|
1659
|
+
opsPerSec: Math.round((iterations / durationMs) * 1000),
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
trace.close();
|
|
1663
|
+
if (useJson) {
|
|
1664
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
console.log('Benchmark Results:');
|
|
1668
|
+
for (const r of results) {
|
|
1669
|
+
console.log(` ${r.name}: ${r.ops} ops in ${r.durationMs}ms (${r.opsPerSec} ops/sec)`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
break;
|
|
1673
|
+
}
|
|
1674
|
+
case 'budget':
|
|
1675
|
+
case 'budget-check': {
|
|
1676
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1677
|
+
const budgetArgv = process.argv.slice(2);
|
|
1678
|
+
const budgetIdx = budgetArgv.indexOf(command);
|
|
1679
|
+
let budgetSub = command === 'budget-check' ? 'check' : 'list';
|
|
1680
|
+
let budgetAgent;
|
|
1681
|
+
if (budgetIdx !== -1) {
|
|
1682
|
+
for (let k = budgetIdx + 1; k < budgetArgv.length; k++) {
|
|
1683
|
+
const c = budgetArgv[k];
|
|
1684
|
+
if (typeof c === 'string' && !c.startsWith('-')) {
|
|
1685
|
+
if (budgetSub === 'list' || budgetSub === 'check') {
|
|
1686
|
+
budgetSub = c;
|
|
1687
|
+
}
|
|
1688
|
+
else if (!budgetAgent) {
|
|
1689
|
+
budgetAgent = c;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const sub = flags.sub || budgetSub;
|
|
1695
|
+
const agent = (flags.agent ? String(flags.agent) : undefined) || budgetAgent || undefined;
|
|
1696
|
+
const dbp = getDbPath();
|
|
1697
|
+
const storage = new TraceStorage(dbp);
|
|
1698
|
+
try {
|
|
1699
|
+
const _db = storage.db;
|
|
1700
|
+
if (sub === 'list' || sub === '') {
|
|
1701
|
+
const rows = storage.db.prepare('SELECT * FROM budgets ORDER BY agent_name').all() || [];
|
|
1702
|
+
if (useJson) {
|
|
1703
|
+
console.log(JSON.stringify(rows.map((r) => ({
|
|
1704
|
+
agent: r.agent_name,
|
|
1705
|
+
maxTokensPerDay: r.max_tokens_per_day,
|
|
1706
|
+
maxCostPerDay: r.max_cost_per_day,
|
|
1707
|
+
})), null, 2));
|
|
1708
|
+
}
|
|
1709
|
+
else if (!rows || rows.length === 0) {
|
|
1710
|
+
console.log('No budgets configured.');
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
console.log('Budgets:');
|
|
1714
|
+
for (const r of rows) {
|
|
1715
|
+
console.log(` ${r.agent_name}: tokens=${r.max_tokens_per_day || 0}/day cost=$${(r.max_cost_per_day || 0).toFixed(2)}/day`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
if (sub === 'set') {
|
|
1721
|
+
const name = agent || '';
|
|
1722
|
+
if (!name) {
|
|
1723
|
+
console.error('Usage: agenttrace-io budget set <agent-name> --tokens <N> --cost <M>');
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
const maxTokens = flags.tokens ? parseInt(String(flags.tokens), 10) || 0 : 0;
|
|
1727
|
+
const maxCost = flags.cost ? parseFloat(String(flags.cost)) || 0 : 0;
|
|
1728
|
+
const now = Date.now();
|
|
1729
|
+
storage.db
|
|
1730
|
+
.prepare(`
|
|
1731
|
+
INSERT OR REPLACE INTO budgets (agent_name, max_tokens_per_day, max_cost_per_day, created_at)
|
|
1732
|
+
VALUES (?, ?, ?, ?)
|
|
1733
|
+
`)
|
|
1734
|
+
.run(name, maxTokens, maxCost, now);
|
|
1735
|
+
if (useJson) {
|
|
1736
|
+
console.log(JSON.stringify({ agent: name, maxTokensPerDay: maxTokens, maxCostPerDay: maxCost }, null, 2));
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
console.log(`Budget set for ${name}: ${maxTokens} tokens/day, $${maxCost.toFixed(2)} cost/day`);
|
|
1740
|
+
}
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
// status or check
|
|
1744
|
+
const name = agent || '';
|
|
1745
|
+
if (!name) {
|
|
1746
|
+
console.error('Usage: agenttrace-io budget status <agent-name> or budget check <agent-name>');
|
|
1747
|
+
process.exit(1);
|
|
1748
|
+
}
|
|
1749
|
+
const brow = storage.db
|
|
1750
|
+
.prepare('SELECT * FROM budgets WHERE agent_name = ?')
|
|
1751
|
+
.get(name);
|
|
1752
|
+
const maxT = brow ? Number(brow.max_tokens_per_day || 0) : 0;
|
|
1753
|
+
const maxC = brow ? Number(brow.max_cost_per_day || 0) : 0;
|
|
1754
|
+
const dayStart = getDayStart(Date.now());
|
|
1755
|
+
const urow = storage.db
|
|
1756
|
+
.prepare('SELECT COALESCE(SUM(tokens_used),0) as t, COALESCE(SUM(cost_usd),0) as c FROM agent_usage WHERE agent_name = ? AND created_at >= ?')
|
|
1757
|
+
.get(name, dayStart);
|
|
1758
|
+
const usedT = urow ? Number(urow.t || 0) : 0;
|
|
1759
|
+
const usedC = urow ? Number(urow.c || 0) : 0;
|
|
1760
|
+
// projected daily
|
|
1761
|
+
const elapsed = Date.now() - dayStart;
|
|
1762
|
+
const dayMs = 86400000;
|
|
1763
|
+
const frac = elapsed > 0 ? Math.min(1, elapsed / dayMs) : 1;
|
|
1764
|
+
const projT = frac > 0 ? Math.round(usedT / frac) : usedT;
|
|
1765
|
+
const projC = frac > 0 ? usedC / frac : usedC;
|
|
1766
|
+
const overT = maxT > 0 && usedT > maxT;
|
|
1767
|
+
const overC = maxC > 0 && usedC > maxC;
|
|
1768
|
+
const isOver = overT || overC;
|
|
1769
|
+
if (sub === 'check' || command === 'budget-check') {
|
|
1770
|
+
storage.close();
|
|
1771
|
+
if (isOver) {
|
|
1772
|
+
if (!useJson)
|
|
1773
|
+
console.error(`Over budget for ${name}`);
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
process.exit(0);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (useJson) {
|
|
1781
|
+
console.log(JSON.stringify({
|
|
1782
|
+
agent: name,
|
|
1783
|
+
maxTokensPerDay: maxT,
|
|
1784
|
+
maxCostPerDay: maxC,
|
|
1785
|
+
usedTokens: usedT,
|
|
1786
|
+
usedCostUsd: usedC,
|
|
1787
|
+
projectedTokens: projT,
|
|
1788
|
+
projectedCostUsd: Number(projC.toFixed(4)),
|
|
1789
|
+
overBudget: isOver,
|
|
1790
|
+
overTokens: overT,
|
|
1791
|
+
overCost: overC,
|
|
1792
|
+
}, null, 2));
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
console.log(`Budget status for ${name}:`);
|
|
1796
|
+
console.log(` Tokens today: ${usedT} / ${maxT || '∞'} (projected ~${projT})`);
|
|
1797
|
+
console.log(` Cost today: $${usedC.toFixed(4)} / $${maxC.toFixed(2) || '∞'} (projected ~$${projC.toFixed(4)})`);
|
|
1798
|
+
if (isOver) {
|
|
1799
|
+
console.log(' *** OVER BUDGET ***');
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
finally {
|
|
1804
|
+
storage.close();
|
|
1805
|
+
}
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
default: {
|
|
1809
|
+
console.error(`Unknown command: ${command}`);
|
|
1810
|
+
printUsage();
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function main() {
|
|
1816
|
+
try {
|
|
1817
|
+
const result = runMain();
|
|
1818
|
+
if (result && typeof result.then === 'function') {
|
|
1819
|
+
return result.catch((err) => {
|
|
1820
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
catch (err) {
|
|
1826
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
1827
|
+
process.exit(1);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
// Run CLI main() only when this file is executed directly (bin or `node dist/index.js`).
|
|
1831
|
+
// Prevents side effects (e.g. printing help) when the module is imported (tests, `require`/`import`).
|
|
1832
|
+
const isMain = (() => {
|
|
1833
|
+
try {
|
|
1834
|
+
const invoked = process.argv[1];
|
|
1835
|
+
if (!invoked)
|
|
1836
|
+
return false;
|
|
1837
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1838
|
+
// Normalize for cross-platform (esp. Windows backslashes)
|
|
1839
|
+
return invoked === thisFile || invoked.replace(/\\/g, '/') === thisFile.replace(/\\/g, '/');
|
|
1840
|
+
}
|
|
1841
|
+
catch (_) {
|
|
1842
|
+
/* ignore */
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
})();
|
|
1846
|
+
if (isMain) {
|
|
1847
|
+
main();
|
|
1848
|
+
}
|
|
1849
|
+
export { main };
|
|
1850
|
+
//# sourceMappingURL=index.js.map
|