@agentgazer/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +87 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +819 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +136 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/assets/index-3NEZyhLk.js +135 -0
- package/dist/dashboard/assets/index-7vaLtwK6.js +135 -0
- package/dist/dashboard/assets/index-B7YewC-Z.css +1 -0
- package/dist/dashboard/assets/index-BE2JQXkd.js +135 -0
- package/dist/dashboard/assets/index-BMIiAulZ.js +127 -0
- package/dist/dashboard/assets/index-BRyPy_KL.css +1 -0
- package/dist/dashboard/assets/index-BYHG3kAu.css +1 -0
- package/dist/dashboard/assets/index-BYpKdiBX.js +135 -0
- package/dist/dashboard/assets/index-BZBLlvtd.css +1 -0
- package/dist/dashboard/assets/index-Bl576aEh.css +1 -0
- package/dist/dashboard/assets/index-Bt6Ph9iA.css +1 -0
- package/dist/dashboard/assets/index-Bu9Qf8LK.js +135 -0
- package/dist/dashboard/assets/index-Bv9VkjUK.css +1 -0
- package/dist/dashboard/assets/index-C5DQefrc.js +135 -0
- package/dist/dashboard/assets/index-C5kNlo-q.js +135 -0
- package/dist/dashboard/assets/index-CS7Cz-tc.css +1 -0
- package/dist/dashboard/assets/index-Cobhg8CQ.js +127 -0
- package/dist/dashboard/assets/index-DJHDvbfs.js +127 -0
- package/dist/dashboard/assets/index-Dit9uSMr.css +1 -0
- package/dist/dashboard/assets/index-DqswNtc2.css +1 -0
- package/dist/dashboard/assets/index-DuCLiTI-.js +127 -0
- package/dist/dashboard/assets/index-Dzlrp-MJ.js +135 -0
- package/dist/dashboard/assets/index-PGK7aC5L.js +135 -0
- package/dist/dashboard/assets/index-VzlmtD1J.js +135 -0
- package/dist/dashboard/assets/index-XbTAQwJ9.js +135 -0
- package/dist/dashboard/assets/index-rQZ6KUmI.js +60 -0
- package/dist/dashboard/assets/index-reNMLKj9.js +127 -0
- package/dist/dashboard/dist/assets/index-3NEZyhLk.js +135 -0
- package/dist/dashboard/dist/assets/index-BYHG3kAu.css +1 -0
- package/dist/dashboard/dist/favicon.svg +16 -0
- package/dist/dashboard/dist/index.html +14 -0
- package/dist/dashboard/favicon.svg +16 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/secret-store.d.ts +42 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/secret-store.js +414 -0
- package/dist/secret-store.js.map +1 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const readline = __importStar(require("node:readline"));
|
|
40
|
+
const config_js_1 = require("./config.js");
|
|
41
|
+
const secret_store_js_1 = require("./secret-store.js");
|
|
42
|
+
const server_1 = require("@agentgazer/server");
|
|
43
|
+
const proxy_1 = require("@agentgazer/proxy");
|
|
44
|
+
const shared_1 = require("@agentgazer/shared");
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Arg parsing
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function parseFlags(argv) {
|
|
49
|
+
const flags = {};
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg.startsWith("--")) {
|
|
53
|
+
const key = arg.slice(2);
|
|
54
|
+
const next = argv[i + 1];
|
|
55
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
56
|
+
flags[key] = next;
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
flags[key] = "";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return flags;
|
|
65
|
+
}
|
|
66
|
+
function parsePositional(argv) {
|
|
67
|
+
const positional = [];
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const arg = argv[i];
|
|
70
|
+
if (arg.startsWith("--")) {
|
|
71
|
+
const next = argv[i + 1];
|
|
72
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
73
|
+
i++; // skip flag value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
positional.push(arg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return positional;
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Help
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function printUsage() {
|
|
86
|
+
console.log(`
|
|
87
|
+
AgentGazer — AI Agent Observability
|
|
88
|
+
|
|
89
|
+
Usage: agentgazer <command> [options]
|
|
90
|
+
|
|
91
|
+
Commands:
|
|
92
|
+
onboard First-time setup — generate token and configure providers
|
|
93
|
+
start Start the server, proxy, and dashboard
|
|
94
|
+
status Show current configuration
|
|
95
|
+
reset-token Generate a new auth token
|
|
96
|
+
providers list List configured providers
|
|
97
|
+
providers set-key Interactive provider key setup
|
|
98
|
+
providers set <name> <key> Set provider key (non-interactive)
|
|
99
|
+
providers remove <name> Remove a provider
|
|
100
|
+
version Show version
|
|
101
|
+
doctor Check system health
|
|
102
|
+
agents List registered agents
|
|
103
|
+
stats [agentId] Show agent statistics (auto-selects if only one agent)
|
|
104
|
+
uninstall Remove AgentGazer (curl-installed only)
|
|
105
|
+
help Show this help message
|
|
106
|
+
|
|
107
|
+
Options (for start):
|
|
108
|
+
--port <number> Server/dashboard port (default: 8080)
|
|
109
|
+
--proxy-port <number> LLM proxy port (default: 4000)
|
|
110
|
+
--retention-days <number> Data retention period in days (default: 30)
|
|
111
|
+
--no-open Don't auto-open browser
|
|
112
|
+
|
|
113
|
+
Options (for doctor, stats, agents):
|
|
114
|
+
--port <number> Server port to check (default: 8080)
|
|
115
|
+
--proxy-port <number> Proxy port to check (default: 4000)
|
|
116
|
+
|
|
117
|
+
Options (for stats):
|
|
118
|
+
--range <period> Time range (default: 24h)
|
|
119
|
+
|
|
120
|
+
Options (for uninstall):
|
|
121
|
+
--yes Skip confirmation prompts
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
agentgazer onboard First-time setup
|
|
125
|
+
agentgazer start Start with defaults
|
|
126
|
+
agentgazer start --port 9090 Use custom server port
|
|
127
|
+
agentgazer providers set-key Interactive provider setup
|
|
128
|
+
agentgazer providers list List configured providers
|
|
129
|
+
agentgazer stats Show stats (auto-selects agent)
|
|
130
|
+
agentgazer stats my-agent --range 7d Show stats for specific agent
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Subcommands
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
const KNOWN_PROVIDERS = shared_1.KNOWN_PROVIDER_NAMES;
|
|
137
|
+
function ask(rl, question) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function cmdOnboard() {
|
|
143
|
+
const saved = (0, config_js_1.ensureConfig)();
|
|
144
|
+
console.log(`
|
|
145
|
+
AgentGazer — Setup
|
|
146
|
+
───────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
Token: ${saved.token}
|
|
149
|
+
Config: ${(0, config_js_1.getConfigDir)()}/config.json
|
|
150
|
+
Database: ${(0, config_js_1.getDbPath)()}
|
|
151
|
+
Server: http://localhost:8080
|
|
152
|
+
Proxy: http://localhost:4000
|
|
153
|
+
|
|
154
|
+
───────────────────────────────────────
|
|
155
|
+
`);
|
|
156
|
+
// Initialize secret store
|
|
157
|
+
const { store, backendName } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
|
|
158
|
+
console.log(` Secret backend: ${backendName}\n`);
|
|
159
|
+
const rl = readline.createInterface({
|
|
160
|
+
input: process.stdin,
|
|
161
|
+
output: process.stdout,
|
|
162
|
+
});
|
|
163
|
+
let providerCount = 0;
|
|
164
|
+
try {
|
|
165
|
+
console.log(" Configure provider API keys (the proxy will inject these for you).");
|
|
166
|
+
console.log(` Available providers: ${KNOWN_PROVIDERS.join(", ")}\n`);
|
|
167
|
+
for (const provider of KNOWN_PROVIDERS) {
|
|
168
|
+
const key = await ask(rl, ` API key for ${provider} (press Enter to skip): `);
|
|
169
|
+
if (!key)
|
|
170
|
+
continue;
|
|
171
|
+
// Store API key in secret store
|
|
172
|
+
await store.set(secret_store_js_1.PROVIDER_SERVICE, provider, key);
|
|
173
|
+
// Store provider entry in config.json (apiKey is empty — actual key is in secret store)
|
|
174
|
+
const providerConfig = { apiKey: "" };
|
|
175
|
+
(0, config_js_1.setProvider)(provider, providerConfig);
|
|
176
|
+
providerCount++;
|
|
177
|
+
console.log(` ✓ ${provider} configured.\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
rl.close();
|
|
182
|
+
}
|
|
183
|
+
console.log(`
|
|
184
|
+
───────────────────────────────────────
|
|
185
|
+
Setup complete. ${providerCount} provider(s) configured.
|
|
186
|
+
───────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
Add this to your project to start tracking:
|
|
189
|
+
|
|
190
|
+
┌──────────────────────────────────────────────────────────┐
|
|
191
|
+
│ │
|
|
192
|
+
│ import { AgentGazer } from "@agentgazer/sdk"; │
|
|
193
|
+
│ │
|
|
194
|
+
│ const at = AgentGazer.init({ │
|
|
195
|
+
│ apiKey: "${saved.token.slice(0, 20)}...",│
|
|
196
|
+
│ agentId: "my-agent", │
|
|
197
|
+
│ }); │
|
|
198
|
+
│ │
|
|
199
|
+
│ at.track({ │
|
|
200
|
+
│ provider: "openai", │
|
|
201
|
+
│ model: "gpt-4o", │
|
|
202
|
+
│ tokens: { input: 150, output: 50 }, │
|
|
203
|
+
│ latency_ms: 1200, │
|
|
204
|
+
│ status: 200, │
|
|
205
|
+
│ }); │
|
|
206
|
+
│ │
|
|
207
|
+
└──────────────────────────────────────────────────────────┘
|
|
208
|
+
|
|
209
|
+
Or point your LLM client at the proxy (with auto API key injection):
|
|
210
|
+
|
|
211
|
+
export OPENAI_BASE_URL=http://localhost:4000/openai/v1
|
|
212
|
+
|
|
213
|
+
Next: run "agentgazer start" to launch.
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
async function cmdProviders(args) {
|
|
217
|
+
const action = args[0];
|
|
218
|
+
switch (action) {
|
|
219
|
+
case "list": {
|
|
220
|
+
// List reads from config.json only — no secret store access needed.
|
|
221
|
+
const providers = (0, config_js_1.listProviders)();
|
|
222
|
+
const names = Object.keys(providers);
|
|
223
|
+
if (names.length === 0) {
|
|
224
|
+
console.log("No providers configured. Use \"agentgazer providers set-key\" to add one.");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log("\n Configured providers:");
|
|
228
|
+
console.log(" ───────────────────────────────────────");
|
|
229
|
+
for (const name of names) {
|
|
230
|
+
const p = providers[name];
|
|
231
|
+
const keyStatus = p.apiKey ? "(plaintext — run \"agentgazer start\" to migrate)" : "(secured)";
|
|
232
|
+
const rateInfo = p.rateLimit
|
|
233
|
+
? ` (rate limit: ${p.rateLimit.maxRequests} req / ${p.rateLimit.windowSeconds}s)`
|
|
234
|
+
: "";
|
|
235
|
+
console.log(` ${name}: ${keyStatus}${rateInfo}`);
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "set": {
|
|
241
|
+
const name = args[1];
|
|
242
|
+
const key = args[2];
|
|
243
|
+
if (!name || !key) {
|
|
244
|
+
console.error("Usage: agentgazer providers set <provider-name> <api-key>");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (!KNOWN_PROVIDERS.includes(name)) {
|
|
248
|
+
console.warn(`Warning: "${name}" is not a known provider (${KNOWN_PROVIDERS.join(", ")}). Proceeding anyway.`);
|
|
249
|
+
}
|
|
250
|
+
const { store, backendName } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
|
|
251
|
+
// Store API key in secret store
|
|
252
|
+
await store.set(secret_store_js_1.PROVIDER_SERVICE, name, key);
|
|
253
|
+
// Ensure provider entry exists in config.json (for rate limits etc.)
|
|
254
|
+
const config = (0, config_js_1.ensureConfig)();
|
|
255
|
+
if (!config.providers)
|
|
256
|
+
config.providers = {};
|
|
257
|
+
if (!config.providers[name]) {
|
|
258
|
+
config.providers[name] = { apiKey: "" };
|
|
259
|
+
}
|
|
260
|
+
// Remove any plaintext apiKey that might be in config
|
|
261
|
+
config.providers[name].apiKey = "";
|
|
262
|
+
(0, config_js_1.saveConfig)(config);
|
|
263
|
+
console.log(`Provider "${name}" configured (secret stored in ${backendName}).`);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "set-key": {
|
|
267
|
+
// Interactive provider key setup
|
|
268
|
+
const rl = readline.createInterface({
|
|
269
|
+
input: process.stdin,
|
|
270
|
+
output: process.stdout,
|
|
271
|
+
});
|
|
272
|
+
console.log("\n Available providers:");
|
|
273
|
+
console.log(" ───────────────────────────────────────");
|
|
274
|
+
KNOWN_PROVIDERS.forEach((p, i) => {
|
|
275
|
+
console.log(` ${i + 1}. ${p}`);
|
|
276
|
+
});
|
|
277
|
+
console.log();
|
|
278
|
+
try {
|
|
279
|
+
const choice = await ask(rl, " Select provider (number): ");
|
|
280
|
+
const idx = parseInt(choice, 10) - 1;
|
|
281
|
+
if (isNaN(idx) || idx < 0 || idx >= KNOWN_PROVIDERS.length) {
|
|
282
|
+
console.error(" Invalid selection.");
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const provider = KNOWN_PROVIDERS[idx];
|
|
286
|
+
const apiKey = await ask(rl, ` API key for ${provider}: `);
|
|
287
|
+
if (!apiKey) {
|
|
288
|
+
console.error(" API key is required.");
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
// Store API key in secret store
|
|
292
|
+
const { store, backendName: backend } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
|
|
293
|
+
await store.set(secret_store_js_1.PROVIDER_SERVICE, provider, apiKey);
|
|
294
|
+
// Store provider entry in config.json (apiKey is empty — actual key is in secret store)
|
|
295
|
+
const providerConfig = { apiKey: "" };
|
|
296
|
+
(0, config_js_1.setProvider)(provider, providerConfig);
|
|
297
|
+
console.log(`\n ✓ ${provider} configured (secret stored in ${backend}).`);
|
|
298
|
+
console.log(` Rate limits can be configured in the Dashboard.`);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
rl.close();
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "remove": {
|
|
306
|
+
const name = args[1];
|
|
307
|
+
if (!name) {
|
|
308
|
+
console.error("Usage: agentgazer providers remove <provider-name>");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const { store } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
|
|
312
|
+
// Delete from secret store
|
|
313
|
+
await store.delete(secret_store_js_1.PROVIDER_SERVICE, name);
|
|
314
|
+
// Remove from config.json
|
|
315
|
+
(0, config_js_1.removeProvider)(name);
|
|
316
|
+
console.log(`Provider "${name}" removed.`);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
default:
|
|
320
|
+
console.error("Usage: agentgazer providers <list|set-key|set|remove>");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function cmdStatus() {
|
|
325
|
+
const config = (0, config_js_1.readConfig)();
|
|
326
|
+
if (!config) {
|
|
327
|
+
console.log("No configuration found. Run \"agentgazer onboard\" first.");
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
console.log(`
|
|
331
|
+
AgentGazer — Status
|
|
332
|
+
───────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
Token: ${config.token}
|
|
335
|
+
Config: ${(0, config_js_1.getConfigDir)()}/config.json
|
|
336
|
+
Database: ${(0, config_js_1.getDbPath)()}
|
|
337
|
+
Server: http://localhost:8080 (default)
|
|
338
|
+
Proxy: http://localhost:4000 (default)
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
function cmdResetToken() {
|
|
342
|
+
const config = (0, config_js_1.resetToken)();
|
|
343
|
+
console.log(`Token reset. New token: ${config.token}`);
|
|
344
|
+
}
|
|
345
|
+
async function cmdStart(flags) {
|
|
346
|
+
const config = (0, config_js_1.ensureConfig)();
|
|
347
|
+
const serverPort = flags["port"] ? parseInt(flags["port"], 10) : 8080;
|
|
348
|
+
const proxyPort = flags["proxy-port"]
|
|
349
|
+
? parseInt(flags["proxy-port"], 10)
|
|
350
|
+
: 4000;
|
|
351
|
+
if (isNaN(serverPort) || serverPort < 1 || serverPort > 65535) {
|
|
352
|
+
console.error("Error: --port must be a valid port number (1-65535)");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
356
|
+
console.error("Error: --proxy-port must be a valid port number (1-65535)");
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
if (serverPort < 1024) {
|
|
360
|
+
console.warn("Warning: port %d may require elevated privileges on Unix systems.", serverPort);
|
|
361
|
+
}
|
|
362
|
+
if (proxyPort < 1024) {
|
|
363
|
+
console.warn("Warning: proxy port %d may require elevated privileges on Unix systems.", proxyPort);
|
|
364
|
+
}
|
|
365
|
+
const retentionDays = flags["retention-days"]
|
|
366
|
+
? parseInt(flags["retention-days"], 10)
|
|
367
|
+
: 30;
|
|
368
|
+
if (isNaN(retentionDays) || retentionDays < 1) {
|
|
369
|
+
console.error("Error: --retention-days must be a positive integer");
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
// Resolve dashboard directory
|
|
373
|
+
let dashboardDir;
|
|
374
|
+
const possibleDashboardPaths = [
|
|
375
|
+
path.resolve(__dirname, "dashboard"),
|
|
376
|
+
path.resolve(__dirname, "../../dashboard-local/dist"),
|
|
377
|
+
path.resolve(__dirname, "../../../apps/dashboard-local/dist"),
|
|
378
|
+
];
|
|
379
|
+
for (const p of possibleDashboardPaths) {
|
|
380
|
+
try {
|
|
381
|
+
const fs = await import("node:fs");
|
|
382
|
+
if (fs.existsSync(path.join(p, "index.html"))) {
|
|
383
|
+
dashboardDir = p;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// ignore
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Start the local server
|
|
392
|
+
const { db, shutdown: shutdownServer } = await (0, server_1.startServer)({
|
|
393
|
+
port: serverPort,
|
|
394
|
+
token: config.token,
|
|
395
|
+
dbPath: (0, config_js_1.getDbPath)(),
|
|
396
|
+
dashboardDir,
|
|
397
|
+
retentionDays,
|
|
398
|
+
});
|
|
399
|
+
// Initialize secret store
|
|
400
|
+
const configDir = (0, config_js_1.getConfigDir)();
|
|
401
|
+
const configPath = path.join(configDir, "config.json");
|
|
402
|
+
const { store, backendName } = await (0, secret_store_js_1.detectSecretStore)(configDir);
|
|
403
|
+
console.log(` Secret backend: ${backendName}`);
|
|
404
|
+
// Auto-migrate plaintext keys from config.json to secret store
|
|
405
|
+
const migratedCount = await (0, secret_store_js_1.migrateFromPlaintextConfig)(configPath, store);
|
|
406
|
+
if (migratedCount > 0) {
|
|
407
|
+
console.log(` Migrated ${migratedCount} provider key(s) from config.json to secret store.`);
|
|
408
|
+
}
|
|
409
|
+
// Load provider keys from secret store
|
|
410
|
+
const providerKeys = await (0, secret_store_js_1.loadProviderKeys)(store);
|
|
411
|
+
// Start the LLM proxy (with db for policy enforcement and rate limits)
|
|
412
|
+
const { shutdown: shutdownProxy } = (0, proxy_1.startProxy)({
|
|
413
|
+
apiKey: config.token,
|
|
414
|
+
agentId: "proxy",
|
|
415
|
+
port: proxyPort,
|
|
416
|
+
endpoint: `http://localhost:${serverPort}/api/events`,
|
|
417
|
+
providerKeys,
|
|
418
|
+
db, // Rate limits are loaded from db
|
|
419
|
+
});
|
|
420
|
+
console.log(`
|
|
421
|
+
╔════════════════════════════════════════════════════╗
|
|
422
|
+
║ AgentGazer running ║
|
|
423
|
+
╠════════════════════════════════════════════════════╣
|
|
424
|
+
║ ║
|
|
425
|
+
║ Dashboard: http://localhost:${String(serverPort).padEnd(5)} ║
|
|
426
|
+
║ Proxy: http://localhost:${String(proxyPort).padEnd(5)} ║
|
|
427
|
+
║ ║
|
|
428
|
+
║ Token: ${config.token.padEnd(32)} ║
|
|
429
|
+
║ ║
|
|
430
|
+
╚════════════════════════════════════════════════════╝
|
|
431
|
+
|
|
432
|
+
Proxy routes: http://localhost:${proxyPort}/{provider}/...
|
|
433
|
+
Providers: ${shared_1.KNOWN_PROVIDER_NAMES.join(", ")}
|
|
434
|
+
`);
|
|
435
|
+
// Auto-open browser unless --no-open
|
|
436
|
+
if (!("no-open" in flags)) {
|
|
437
|
+
try {
|
|
438
|
+
const open = await import("open");
|
|
439
|
+
await open.default(`http://localhost:${serverPort}`);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Silently fail if browser can't be opened
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Graceful shutdown
|
|
446
|
+
let shuttingDown = false;
|
|
447
|
+
function handleShutdown() {
|
|
448
|
+
if (shuttingDown)
|
|
449
|
+
return;
|
|
450
|
+
shuttingDown = true;
|
|
451
|
+
console.log("\nShutting down...");
|
|
452
|
+
Promise.all([shutdownProxy(), shutdownServer()])
|
|
453
|
+
.then(() => {
|
|
454
|
+
console.log("Shutdown complete.");
|
|
455
|
+
process.exit(0);
|
|
456
|
+
})
|
|
457
|
+
.catch((err) => {
|
|
458
|
+
console.error("Error during shutdown:", err);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
process.on("SIGINT", handleShutdown);
|
|
463
|
+
process.on("SIGTERM", handleShutdown);
|
|
464
|
+
// Clean up listeners once shutdown completes to prevent leaks on restart
|
|
465
|
+
process.once("exit", () => {
|
|
466
|
+
process.removeListener("SIGINT", handleShutdown);
|
|
467
|
+
process.removeListener("SIGTERM", handleShutdown);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Formatting helpers
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
function formatNumber(n) {
|
|
474
|
+
return n.toLocaleString("en-US");
|
|
475
|
+
}
|
|
476
|
+
function timeAgo(iso) {
|
|
477
|
+
if (!iso)
|
|
478
|
+
return "—";
|
|
479
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
480
|
+
if (diff < 0)
|
|
481
|
+
return "just now";
|
|
482
|
+
const seconds = Math.floor(diff / 1000);
|
|
483
|
+
if (seconds < 60)
|
|
484
|
+
return `${seconds} seconds ago`;
|
|
485
|
+
const minutes = Math.floor(seconds / 60);
|
|
486
|
+
if (minutes < 60)
|
|
487
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
|
|
488
|
+
const hours = Math.floor(minutes / 60);
|
|
489
|
+
if (hours < 24)
|
|
490
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
491
|
+
const days = Math.floor(hours / 24);
|
|
492
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
493
|
+
}
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// API helper
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
async function apiGet(urlPath, port) {
|
|
498
|
+
const config = (0, config_js_1.readConfig)();
|
|
499
|
+
const token = config?.token ?? "";
|
|
500
|
+
try {
|
|
501
|
+
const res = await fetch(`http://localhost:${port}${urlPath}`, {
|
|
502
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
503
|
+
signal: AbortSignal.timeout(5000),
|
|
504
|
+
});
|
|
505
|
+
if (!res.ok) {
|
|
506
|
+
console.error(`Server responded with ${res.status} ${res.statusText}`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
return await res.json();
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
if (err instanceof TypeError &&
|
|
513
|
+
err.cause
|
|
514
|
+
?.code === "ECONNREFUSED") {
|
|
515
|
+
console.error('Server not running. Run "agentgazer start" first.');
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// New subcommands
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
function cmdVersion() {
|
|
525
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
526
|
+
const pkg = require(path.resolve(__dirname, "../package.json"));
|
|
527
|
+
console.log(`agentgazer ${pkg.version}`);
|
|
528
|
+
}
|
|
529
|
+
async function cmdDoctor(flags) {
|
|
530
|
+
const port = flags["port"] ? parseInt(flags["port"], 10) : 8080;
|
|
531
|
+
const proxyPort = flags["proxy-port"]
|
|
532
|
+
? parseInt(flags["proxy-port"], 10)
|
|
533
|
+
: 4000;
|
|
534
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
535
|
+
console.error("Error: --port must be a valid port number (1-65535)");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
539
|
+
console.error("Error: --proxy-port must be a valid port number (1-65535)");
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
console.log(`
|
|
543
|
+
AgentGazer — Doctor
|
|
544
|
+
───────────────────────────────────────
|
|
545
|
+
`);
|
|
546
|
+
let passed = 0;
|
|
547
|
+
const total = 6;
|
|
548
|
+
// 1. Config file exists
|
|
549
|
+
const config = (0, config_js_1.readConfig)();
|
|
550
|
+
if (config) {
|
|
551
|
+
console.log(" ✓ Config file exists");
|
|
552
|
+
passed++;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
console.log(" ✗ Config file missing");
|
|
556
|
+
}
|
|
557
|
+
// 2. Auth token set
|
|
558
|
+
if (config?.token) {
|
|
559
|
+
console.log(" ✓ Auth token set");
|
|
560
|
+
passed++;
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
console.log(" ✗ Auth token not set");
|
|
564
|
+
}
|
|
565
|
+
// 3. Database file exists
|
|
566
|
+
if (fs.existsSync((0, config_js_1.getDbPath)())) {
|
|
567
|
+
console.log(" ✓ Database file exists");
|
|
568
|
+
passed++;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
console.log(" ✗ Database file missing");
|
|
572
|
+
}
|
|
573
|
+
// 4. Secret store accessible
|
|
574
|
+
try {
|
|
575
|
+
const { store } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
|
|
576
|
+
if (await store.isAvailable()) {
|
|
577
|
+
console.log(" ✓ Secret store accessible");
|
|
578
|
+
passed++;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.log(" ✗ Secret store not accessible");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
console.log(" ✗ Secret store not accessible");
|
|
586
|
+
}
|
|
587
|
+
// 5. Server responding
|
|
588
|
+
try {
|
|
589
|
+
const res = await fetch(`http://localhost:${port}/api/health`, {
|
|
590
|
+
signal: AbortSignal.timeout(3000),
|
|
591
|
+
});
|
|
592
|
+
if (res.ok) {
|
|
593
|
+
console.log(` ✓ Server responding (http://localhost:${port})`);
|
|
594
|
+
passed++;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
console.log(` ✗ Server not responding (http://localhost:${port})`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
console.log(` ✗ Server not responding (http://localhost:${port})`);
|
|
602
|
+
}
|
|
603
|
+
// 6. Proxy responding
|
|
604
|
+
try {
|
|
605
|
+
await fetch(`http://localhost:${proxyPort}/`, {
|
|
606
|
+
signal: AbortSignal.timeout(3000),
|
|
607
|
+
});
|
|
608
|
+
// Any response (even 4xx) means it's up
|
|
609
|
+
console.log(` ✓ Proxy responding (http://localhost:${proxyPort})`);
|
|
610
|
+
passed++;
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
console.log(` ✗ Proxy not responding (http://localhost:${proxyPort})`);
|
|
614
|
+
}
|
|
615
|
+
console.log(`\n ${passed}/${total} checks passed.`);
|
|
616
|
+
}
|
|
617
|
+
async function cmdAgents(flags) {
|
|
618
|
+
const port = flags["port"] ? parseInt(flags["port"], 10) : 8080;
|
|
619
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
620
|
+
console.error("Error: --port must be a valid port number (1-65535)");
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
const resp = (await apiGet("/api/agents", port));
|
|
624
|
+
const agents = resp.agents;
|
|
625
|
+
if (!agents || agents.length === 0) {
|
|
626
|
+
console.log("No agents registered yet.");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const header = ` ${"Agent ID".padEnd(18)}${"Status".padEnd(11)}${"Events".padStart(8)} Last Heartbeat`;
|
|
630
|
+
console.log(header);
|
|
631
|
+
console.log(" " + "─".repeat(header.trimStart().length));
|
|
632
|
+
for (const a of agents) {
|
|
633
|
+
const id = (a.agent_id ?? "").padEnd(18);
|
|
634
|
+
const status = (a.status ?? "unknown").padEnd(11);
|
|
635
|
+
const events = formatNumber(a.total_events ?? 0).padStart(8);
|
|
636
|
+
const heartbeat = timeAgo(a.last_heartbeat);
|
|
637
|
+
console.log(` ${id}${status}${events} ${heartbeat}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function cmdStats(flags) {
|
|
641
|
+
const port = flags["port"] ? parseInt(flags["port"], 10) : 8080;
|
|
642
|
+
const range = flags["range"] || "24h";
|
|
643
|
+
const positional = parsePositional(process.argv.slice(3));
|
|
644
|
+
let agentId = positional[0];
|
|
645
|
+
// Auto-select agent if not specified
|
|
646
|
+
if (!agentId) {
|
|
647
|
+
const resp = (await apiGet("/api/agents", port));
|
|
648
|
+
const agents = resp.agents;
|
|
649
|
+
if (!agents || agents.length === 0) {
|
|
650
|
+
console.log("No agents registered yet.");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (agents.length === 1) {
|
|
654
|
+
agentId = agents[0].agent_id;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
console.log("Multiple agents found. Please specify one:\n");
|
|
658
|
+
for (const a of agents) {
|
|
659
|
+
console.log(` agentgazer stats ${a.agent_id}`);
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
let data;
|
|
666
|
+
try {
|
|
667
|
+
data = (await apiGet(`/api/stats/${encodeURIComponent(agentId)}?range=${encodeURIComponent(range)}`, port));
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
671
|
+
console.error(`Error fetching stats for "${agentId}": ${err.message}`);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
console.error(`Error fetching stats for "${agentId}".`);
|
|
675
|
+
}
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
const errorPct = data.total_requests > 0
|
|
679
|
+
? ((data.total_errors / data.total_requests) * 100).toFixed(2)
|
|
680
|
+
: "0.00";
|
|
681
|
+
console.log(`
|
|
682
|
+
AgentGazer — Stats for "${agentId}" (last ${range})
|
|
683
|
+
───────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
Requests: ${formatNumber(data.total_requests)}
|
|
686
|
+
Errors: ${formatNumber(data.total_errors)} (${errorPct}%)
|
|
687
|
+
Cost: $${data.total_cost.toFixed(2)}
|
|
688
|
+
Tokens: ${formatNumber(data.total_tokens)}
|
|
689
|
+
|
|
690
|
+
Latency: p50 = ${data.p50_latency != null ? formatNumber(data.p50_latency) : "--"}ms p99 = ${data.p99_latency != null ? formatNumber(data.p99_latency) : "--"}ms`);
|
|
691
|
+
if (data.cost_by_model && data.cost_by_model.length > 0) {
|
|
692
|
+
console.log("\n Cost by model:");
|
|
693
|
+
for (const m of data.cost_by_model) {
|
|
694
|
+
const model = m.model.padEnd(16);
|
|
695
|
+
const cost = `$${m.cost.toFixed(2)}`;
|
|
696
|
+
console.log(` ${model}${cost} (${formatNumber(m.count)} calls)`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
console.log();
|
|
700
|
+
}
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Uninstall
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
async function cmdUninstall(flags) {
|
|
705
|
+
const home = process.env.AGENTTRACE_HOME || path.join(require("os").homedir(), ".agentgazer");
|
|
706
|
+
const libDir = path.join(home, "lib");
|
|
707
|
+
const nodeDir = path.join(home, "node");
|
|
708
|
+
const wrapperPath = path.join(process.env.AGENTTRACE_BIN || "/usr/local/bin", "agentgazer");
|
|
709
|
+
// Detect install method
|
|
710
|
+
if (!fs.existsSync(libDir)) {
|
|
711
|
+
console.log('AgentGazer was not installed via the install script.');
|
|
712
|
+
console.log('');
|
|
713
|
+
console.log(' If installed via npm:');
|
|
714
|
+
console.log(' npm uninstall -g agentgazer');
|
|
715
|
+
console.log('');
|
|
716
|
+
console.log(' If installed via Homebrew:');
|
|
717
|
+
console.log(' brew uninstall agentgazer');
|
|
718
|
+
console.log('');
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const skipPrompt = "yes" in flags;
|
|
722
|
+
console.log(`
|
|
723
|
+
AgentGazer — Uninstall
|
|
724
|
+
───────────────────────────────────────
|
|
725
|
+
`);
|
|
726
|
+
// Remove embedded Node.js
|
|
727
|
+
if (fs.existsSync(nodeDir)) {
|
|
728
|
+
fs.rmSync(nodeDir, { recursive: true, force: true });
|
|
729
|
+
console.log(` ✓ Removed embedded Node.js (${nodeDir})`);
|
|
730
|
+
}
|
|
731
|
+
// Remove lib
|
|
732
|
+
fs.rmSync(libDir, { recursive: true, force: true });
|
|
733
|
+
console.log(` ✓ Removed installation (${libDir})`);
|
|
734
|
+
// Remove wrapper
|
|
735
|
+
if (fs.existsSync(wrapperPath)) {
|
|
736
|
+
try {
|
|
737
|
+
fs.unlinkSync(wrapperPath);
|
|
738
|
+
console.log(` ✓ Removed wrapper (${wrapperPath})`);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
console.log(` ! Could not remove ${wrapperPath} — try: sudo rm ${wrapperPath}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Handle user data
|
|
745
|
+
const configPath = path.join(home, "config.json");
|
|
746
|
+
const dbPath = path.join(home, "data.db");
|
|
747
|
+
const hasData = fs.existsSync(configPath) || fs.existsSync(dbPath);
|
|
748
|
+
if (hasData) {
|
|
749
|
+
let removeData = skipPrompt;
|
|
750
|
+
if (!skipPrompt) {
|
|
751
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
752
|
+
const answer = await ask(rl, "\n Remove user data (config.json, data.db)? [y/N] ");
|
|
753
|
+
rl.close();
|
|
754
|
+
removeData = /^y(es)?$/i.test(answer);
|
|
755
|
+
}
|
|
756
|
+
if (removeData) {
|
|
757
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
758
|
+
console.log(` ✓ Removed all data (${home})`);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.log(` → User data preserved at ${home}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
console.log("\n ✓ AgentGazer uninstalled.\n");
|
|
765
|
+
}
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// Main
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
async function main() {
|
|
770
|
+
const subcommand = process.argv[2];
|
|
771
|
+
const flags = parseFlags(process.argv.slice(3));
|
|
772
|
+
switch (subcommand) {
|
|
773
|
+
case "onboard":
|
|
774
|
+
await cmdOnboard();
|
|
775
|
+
break;
|
|
776
|
+
case "start":
|
|
777
|
+
await cmdStart(flags);
|
|
778
|
+
break;
|
|
779
|
+
case "status":
|
|
780
|
+
cmdStatus();
|
|
781
|
+
break;
|
|
782
|
+
case "reset-token":
|
|
783
|
+
cmdResetToken();
|
|
784
|
+
break;
|
|
785
|
+
case "providers":
|
|
786
|
+
await cmdProviders(process.argv.slice(3));
|
|
787
|
+
break;
|
|
788
|
+
case "version":
|
|
789
|
+
cmdVersion();
|
|
790
|
+
break;
|
|
791
|
+
case "doctor":
|
|
792
|
+
await cmdDoctor(flags);
|
|
793
|
+
break;
|
|
794
|
+
case "agents":
|
|
795
|
+
await cmdAgents(flags);
|
|
796
|
+
break;
|
|
797
|
+
case "stats":
|
|
798
|
+
await cmdStats(flags);
|
|
799
|
+
break;
|
|
800
|
+
case "uninstall":
|
|
801
|
+
await cmdUninstall(flags);
|
|
802
|
+
break;
|
|
803
|
+
case "--help":
|
|
804
|
+
case "-h":
|
|
805
|
+
case "help":
|
|
806
|
+
case undefined:
|
|
807
|
+
printUsage();
|
|
808
|
+
break;
|
|
809
|
+
default:
|
|
810
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
811
|
+
printUsage();
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
main().catch((err) => {
|
|
816
|
+
console.error("Fatal error:", err);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
});
|
|
819
|
+
//# sourceMappingURL=cli.js.map
|