@agentgazer/cli 0.2.1 → 0.3.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.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +726 -403
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/agent.d.ts +2 -0
  5. package/dist/commands/agent.d.ts.map +1 -0
  6. package/dist/commands/agent.js +469 -0
  7. package/dist/commands/agent.js.map +1 -0
  8. package/dist/commands/agents.d.ts +2 -0
  9. package/dist/commands/agents.d.ts.map +1 -0
  10. package/dist/commands/agents.js +70 -0
  11. package/dist/commands/agents.js.map +1 -0
  12. package/dist/commands/events.d.ts +21 -0
  13. package/dist/commands/events.d.ts.map +1 -0
  14. package/dist/commands/events.js +247 -0
  15. package/dist/commands/events.js.map +1 -0
  16. package/dist/commands/overview.d.ts +2 -0
  17. package/dist/commands/overview.d.ts.map +1 -0
  18. package/dist/commands/overview.js +8 -0
  19. package/dist/commands/overview.js.map +1 -0
  20. package/dist/commands/provider.d.ts +2 -0
  21. package/dist/commands/provider.d.ts.map +1 -0
  22. package/dist/commands/provider.js +210 -0
  23. package/dist/commands/provider.js.map +1 -0
  24. package/dist/commands/providers.d.ts +2 -0
  25. package/dist/commands/providers.d.ts.map +1 -0
  26. package/dist/commands/providers.js +77 -0
  27. package/dist/commands/providers.js.map +1 -0
  28. package/dist/config.d.ts +46 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +97 -60
  31. package/dist/config.js.map +1 -1
  32. package/dist/dashboard/assets/index-5AtWzXiJ.js +143 -0
  33. package/dist/dashboard/assets/index-B7NvhJW9.js +143 -0
  34. package/dist/dashboard/assets/index-B9izZ4lr.js +139 -0
  35. package/dist/dashboard/assets/index-BADwu2fU.js +143 -0
  36. package/dist/dashboard/assets/index-BBonREkg.js +143 -0
  37. package/dist/dashboard/assets/index-BGdw1Ss1.js +139 -0
  38. package/dist/dashboard/assets/index-BLIagyHw.css +1 -0
  39. package/dist/dashboard/assets/index-BN2gLOCd.js +143 -0
  40. package/dist/dashboard/assets/index-BOcemxE3.js +143 -0
  41. package/dist/dashboard/assets/index-BYYSUJ-G.js +143 -0
  42. package/dist/dashboard/assets/index-BhGhcADW.js +143 -0
  43. package/dist/dashboard/assets/index-Bjr8f_u_.js +143 -0
  44. package/dist/dashboard/assets/index-BsaxUL45.js +143 -0
  45. package/dist/dashboard/assets/index-C-DguelG.css +1 -0
  46. package/dist/dashboard/assets/index-C-QgKsob.js +143 -0
  47. package/dist/dashboard/assets/index-C6t4YWSY.js +143 -0
  48. package/dist/dashboard/assets/index-C9EQrGpe.js +143 -0
  49. package/dist/dashboard/assets/index-CB6k7nz0.js +143 -0
  50. package/dist/dashboard/assets/index-CBCLOBvd.js +139 -0
  51. package/dist/dashboard/assets/index-CGwUmIVh.js +139 -0
  52. package/dist/dashboard/assets/index-CLMyGFxA.js +145 -0
  53. package/dist/dashboard/assets/index-CYnfgO3k.js +143 -0
  54. package/dist/dashboard/assets/index-CiGGhaUw.js +143 -0
  55. package/dist/dashboard/assets/index-Cj3GjdoQ.js +143 -0
  56. package/dist/dashboard/assets/index-CxEl0O9p.js +143 -0
  57. package/dist/dashboard/assets/index-D2mLKlRw.css +1 -0
  58. package/dist/dashboard/assets/index-D5AUmtdo.js +139 -0
  59. package/dist/dashboard/assets/index-DACbampq.css +1 -0
  60. package/dist/dashboard/assets/index-DB0fFjgr.js +139 -0
  61. package/dist/dashboard/assets/index-DI-JqWdh.css +1 -0
  62. package/dist/dashboard/assets/index-DRcLEFcl.js +143 -0
  63. package/dist/dashboard/assets/index-DaUEJqLn.css +1 -0
  64. package/dist/dashboard/assets/index-Db5ik9Wo.js +143 -0
  65. package/dist/dashboard/assets/index-DfV-2ewH.css +1 -0
  66. package/dist/dashboard/assets/index-DgEq2MBB.css +1 -0
  67. package/dist/dashboard/assets/index-DhmVjP8v.js +143 -0
  68. package/dist/dashboard/assets/index-Dipd1pd3.js +143 -0
  69. package/dist/dashboard/assets/index-DirRSEOl.js +143 -0
  70. package/dist/dashboard/assets/index-Dr9wOl47.js +143 -0
  71. package/dist/dashboard/assets/index-ee-BCW5i.js +143 -0
  72. package/dist/dashboard/assets/index-g4KzDTwu.js +139 -0
  73. package/dist/dashboard/assets/index-odup_N3G.css +1 -0
  74. package/dist/dashboard/assets/index-wHHHG0bR.css +1 -0
  75. package/dist/dashboard/assets/index-wNn2zMSu.js +143 -0
  76. package/dist/dashboard/assets/index-wvU3UdVf.js +143 -0
  77. package/dist/dashboard/assets/index-yHwKni1U.css +1 -0
  78. package/dist/dashboard/assets/index-z-YzEeVY.js +143 -0
  79. package/dist/dashboard/index.html +2 -2
  80. package/dist/index.js +1 -11
  81. package/dist/index.js.map +1 -1
  82. package/dist/secret-store.js +26 -68
  83. package/dist/secret-store.js.map +1 -1
  84. package/dist/tui/AgentTable.d.ts +15 -0
  85. package/dist/tui/AgentTable.d.ts.map +1 -0
  86. package/dist/tui/AgentTable.js +35 -0
  87. package/dist/tui/AgentTable.js.map +1 -0
  88. package/dist/tui/EventLog.d.ts +15 -0
  89. package/dist/tui/EventLog.d.ts.map +1 -0
  90. package/dist/tui/EventLog.js +15 -0
  91. package/dist/tui/EventLog.js.map +1 -0
  92. package/dist/tui/HelpOverlay.d.ts +7 -0
  93. package/dist/tui/HelpOverlay.d.ts.map +1 -0
  94. package/dist/tui/HelpOverlay.js +11 -0
  95. package/dist/tui/HelpOverlay.js.map +1 -0
  96. package/dist/tui/Overview.d.ts +7 -0
  97. package/dist/tui/Overview.d.ts.map +1 -0
  98. package/dist/tui/Overview.js +69 -0
  99. package/dist/tui/Overview.js.map +1 -0
  100. package/dist/tui/StatusBar.d.ts +11 -0
  101. package/dist/tui/StatusBar.d.ts.map +1 -0
  102. package/dist/tui/StatusBar.js +27 -0
  103. package/dist/tui/StatusBar.js.map +1 -0
  104. package/dist/utils/api.d.ts +10 -0
  105. package/dist/utils/api.d.ts.map +1 -0
  106. package/dist/utils/api.js +114 -0
  107. package/dist/utils/api.js.map +1 -0
  108. package/dist/utils/format.d.ts +7 -0
  109. package/dist/utils/format.d.ts.map +1 -0
  110. package/dist/utils/format.js +56 -0
  111. package/dist/utils/format.js.map +1 -0
  112. package/dist/utils/prompt.d.ts +4 -0
  113. package/dist/utils/prompt.d.ts.map +1 -0
  114. package/dist/utils/prompt.js +35 -0
  115. package/dist/utils/prompt.js.map +1 -0
  116. package/package.json +13 -6
package/dist/cli.js CHANGED
@@ -1,47 +1,24 @@
1
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");
2
+ import { execSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as net from "node:net";
5
+ import * as path from "node:path";
6
+ import * as readline from "node:readline";
7
+ import { fileURLToPath } from "node:url";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ import { ensureConfig, readConfig, resetToken, getDbPath, getConfigDir, setProvider, } from "./config.js";
11
+ import { detectSecretStore, migrateFromPlaintextConfig, loadProviderKeys, PROVIDER_SERVICE, } from "./secret-store.js";
12
+ import { startServer } from "@agentgazer/server";
13
+ import { startProxy } from "@agentgazer/proxy";
14
+ import { KNOWN_PROVIDER_NAMES } from "@agentgazer/shared";
15
+ // New command imports
16
+ import { cmdAgents } from "./commands/agents.js";
17
+ import { cmdAgent } from "./commands/agent.js";
18
+ import { cmdProviders } from "./commands/providers.js";
19
+ import { cmdProvider } from "./commands/provider.js";
20
+ import { cmdEvents } from "./commands/events.js";
21
+ // cmdOverview is imported dynamically to avoid ESM top-level await issues
45
22
  // ---------------------------------------------------------------------------
46
23
  // Arg parsing
47
24
  // ---------------------------------------------------------------------------
@@ -52,7 +29,19 @@ function parseFlags(argv) {
52
29
  if (arg.startsWith("--")) {
53
30
  const key = arg.slice(2);
54
31
  const next = argv[i + 1];
55
- if (next !== undefined && !next.startsWith("--")) {
32
+ if (next !== undefined && !next.startsWith("-")) {
33
+ flags[key] = next;
34
+ i++;
35
+ }
36
+ else {
37
+ flags[key] = "";
38
+ }
39
+ }
40
+ else if (arg.startsWith("-") && arg.length === 2) {
41
+ // Short flags like -v, -d, or short flags with values like -o json
42
+ const key = arg.slice(1);
43
+ const next = argv[i + 1];
44
+ if (next !== undefined && !next.startsWith("-")) {
56
45
  flags[key] = next;
57
46
  i++;
58
47
  }
@@ -80,6 +69,50 @@ function parsePositional(argv) {
80
69
  return positional;
81
70
  }
82
71
  // ---------------------------------------------------------------------------
72
+ // Port utilities
73
+ // ---------------------------------------------------------------------------
74
+ function isPortAvailable(port) {
75
+ return new Promise((resolve) => {
76
+ const server = net.createServer();
77
+ server.once("error", () => resolve(false));
78
+ server.once("listening", () => {
79
+ server.close(() => resolve(true));
80
+ });
81
+ server.listen(port, "0.0.0.0");
82
+ });
83
+ }
84
+ async function findAvailablePort(startPort, maxAttempts = 10) {
85
+ for (let i = 0; i < maxAttempts; i++) {
86
+ const port = startPort + i;
87
+ if (await isPortAvailable(port)) {
88
+ return port;
89
+ }
90
+ }
91
+ throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
92
+ }
93
+ /**
94
+ * Find PIDs of processes listening on the given ports.
95
+ * Uses lsof on macOS/Linux. Returns empty array on error.
96
+ */
97
+ function findPidsOnPorts(ports) {
98
+ const pids = new Set();
99
+ for (const port of ports) {
100
+ try {
101
+ const result = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf-8" });
102
+ for (const line of result.trim().split("\n")) {
103
+ const pid = parseInt(line.trim(), 10);
104
+ if (!isNaN(pid) && pid > 0) {
105
+ pids.add(pid);
106
+ }
107
+ }
108
+ }
109
+ catch {
110
+ // No process on this port or lsof not available
111
+ }
112
+ }
113
+ return Array.from(pids);
114
+ }
115
+ // ---------------------------------------------------------------------------
83
116
  // Help
84
117
  // ---------------------------------------------------------------------------
85
118
  function printUsage() {
@@ -91,70 +124,133 @@ Usage: agentgazer <command> [options]
91
124
  Commands:
92
125
  onboard First-time setup — generate token and configure providers
93
126
  start Start the server, proxy, and dashboard
127
+ stop Stop the daemon process
128
+ logs Show daemon logs (use -f to follow)
94
129
  status Show current configuration
95
130
  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
131
+ overview Launch real-time TUI dashboard
132
+ events Query and display agent events
133
+
134
+ agents List all registered agents
135
+ agent <name> active Activate an agent
136
+ agent <name> deactive Deactivate an agent
137
+ agent <name> killswitch on|off Toggle kill switch
138
+ agent <name> delete Delete agent and all data
139
+ agent <name> stat Show agent statistics
140
+ agent <name> model List model overrides
141
+ agent <name> model-override <model> Set model override
142
+ agent <name> alerts List alert rules for agent
143
+ agent <name> alert add <type> Add alert rule (agent_down, error_rate, budget)
144
+ agent <name> alert delete <id> Delete alert rule
145
+ agent <name> alert reset <id> Reset alert state to normal
146
+
147
+ providers List all configured providers
148
+ provider add [name] [key] Add provider (interactive if args omitted)
149
+ provider <name> active Activate a provider
150
+ provider <name> deactive Deactivate a provider
151
+ provider <name> test-connection Test API key validity
152
+ provider <name> delete Delete provider and key
153
+ provider <name> models List available models
154
+ provider <name> stat Show provider statistics
155
+
100
156
  version Show version
101
157
  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)
158
+ uninstall Remove AgentGazer data (interactive menu)
105
159
  help Show this help message
106
160
 
107
161
  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
162
+ --port <number> Server/dashboard port (default: 18800, or config.server.port)
163
+ --proxy-port <number> LLM proxy port (default: 18900, or config.server.proxyPort)
164
+ --retention-days <number> Data retention in days (default: 30, or config.data.retentionDays)
165
+ --no-open Don't auto-open browser (or set config.server.autoOpen: false)
166
+ -v, --verbose Print verbose logs to console
167
+ -d, --daemon Print info and token, then run in background
168
+
169
+ Config file: ~/.agentgazer/config.json (optional settings: port, proxyPort, autoOpen, retentionDays)
170
+
171
+ Options (for agent/provider stat):
172
+ --range <period> Time range: 1h, 24h, 7d, 30d (default: 24h)
173
+
174
+ Options (for delete commands):
175
+ --yes Skip confirmation prompts
176
+
177
+ Options (for alert add):
178
+ --threshold <percent> Error rate threshold (error_rate only, default: 10)
179
+ --timeout <seconds> Timeout in seconds (agent_down only, default: 300)
180
+ --limit <amount> Budget limit in USD (budget only, required)
181
+ --period <period> Budget period: daily, weekly, monthly (budget only)
182
+ --repeat Enable repeat notifications (default: enabled)
183
+ --no-repeat Disable repeat notifications (one-time only)
184
+ --interval <minutes> Repeat interval in minutes (default: 15)
185
+ --recovery-notify Send notification when alert recovers
186
+ --webhook <url> Webhook URL for notifications
187
+ --telegram <chat_id> Telegram chat ID for notifications
112
188
 
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)
189
+ Options (for logs):
190
+ -f, --follow Follow log output (like tail -f)
191
+ -n, --lines <number> Number of lines to show (default: 50)
116
192
 
117
- Options (for stats):
118
- --range <period> Time range (default: 24h)
193
+ Options (for events):
194
+ -a, --agent <name> Filter by agent ID
195
+ -t, --type <type> Filter by event type
196
+ -p, --provider <name> Filter by provider
197
+ -s, --since <duration> Time range: 1h, 24h, 7d, 30d (default: 24h)
198
+ -n, --limit <number> Max events (default: 50, max: 1000)
199
+ -o, --output <format> Output: table, json, csv (default: table)
200
+ --search <term> Search in model/provider/error
201
+ -f, --follow Poll for new events every 3s
119
202
 
120
203
  Options (for uninstall):
204
+ --all Remove everything (keys, config, data)
205
+ --config Remove config only
206
+ --keys Remove provider keys only
207
+ --data Remove agent data only
121
208
  --yes Skip confirmation prompts
122
209
 
123
210
  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
211
+ agentgazer onboard First-time setup
212
+ agentgazer start Start with defaults
213
+ agentgazer start -d Start as daemon (background)
214
+ agentgazer stop Stop the daemon
215
+ agentgazer logs -f Follow daemon logs
216
+ agentgazer overview Launch TUI dashboard
217
+ agentgazer provider add openai Add OpenAI (prompts for key)
218
+ agentgazer agent my-bot stat Show stats for my-bot
219
+ agentgazer agent my-bot killswitch on Enable kill switch
220
+ agentgazer events Show recent events
221
+ agentgazer events -a my-bot -t error Filter by agent and type
222
+ agentgazer events -f Follow new events live
223
+ agentgazer agent my-bot alerts List alerts for my-bot
224
+ agentgazer agent my-bot alert add error_rate --threshold 20
225
+ agentgazer agent my-bot alert add budget --limit 50 --period daily
226
+ agentgazer agent my-bot alert reset abc Reset alert state
131
227
  `);
132
228
  }
133
229
  // ---------------------------------------------------------------------------
134
230
  // Subcommands
135
231
  // ---------------------------------------------------------------------------
136
- const KNOWN_PROVIDERS = shared_1.KNOWN_PROVIDER_NAMES;
232
+ const KNOWN_PROVIDERS = KNOWN_PROVIDER_NAMES;
137
233
  function ask(rl, question) {
138
234
  return new Promise((resolve) => {
139
235
  rl.question(question, (answer) => resolve(answer.trim()));
140
236
  });
141
237
  }
142
238
  async function cmdOnboard() {
143
- const saved = (0, config_js_1.ensureConfig)();
239
+ const saved = ensureConfig();
144
240
  console.log(`
145
241
  AgentGazer — Setup
146
242
  ───────────────────────────────────────
147
243
 
148
244
  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
245
+ Config: ${getConfigDir()}/config.json
246
+ Database: ${getDbPath()}
247
+ Server: http://localhost:18800
248
+ Proxy: http://localhost:18900
153
249
 
154
250
  ───────────────────────────────────────
155
251
  `);
156
252
  // Initialize secret store
157
- const { store, backendName } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
253
+ const { store, backendName } = await detectSecretStore(getConfigDir());
158
254
  console.log(` Secret backend: ${backendName}\n`);
159
255
  const rl = readline.createInterface({
160
256
  input: process.stdin,
@@ -169,10 +265,10 @@ async function cmdOnboard() {
169
265
  if (!key)
170
266
  continue;
171
267
  // Store API key in secret store
172
- await store.set(secret_store_js_1.PROVIDER_SERVICE, provider, key);
268
+ await store.set(PROVIDER_SERVICE, provider, key);
173
269
  // Store provider entry in config.json (apiKey is empty — actual key is in secret store)
174
270
  const providerConfig = { apiKey: "" };
175
- (0, config_js_1.setProvider)(provider, providerConfig);
271
+ setProvider(provider, providerConfig);
176
272
  providerCount++;
177
273
  console.log(` ✓ ${provider} configured.\n`);
178
274
  }
@@ -208,121 +304,13 @@ async function cmdOnboard() {
208
304
 
209
305
  Or point your LLM client at the proxy (with auto API key injection):
210
306
 
211
- export OPENAI_BASE_URL=http://localhost:4000/openai/v1
307
+ export OPENAI_BASE_URL=http://localhost:18900/openai/v1
212
308
 
213
309
  Next: run "agentgazer start" to launch.
214
310
  `);
215
311
  }
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
312
  function cmdStatus() {
325
- const config = (0, config_js_1.readConfig)();
313
+ const config = readConfig();
326
314
  if (!config) {
327
315
  console.log("No configuration found. Run \"agentgazer onboard\" first.");
328
316
  process.exit(1);
@@ -332,23 +320,90 @@ function cmdStatus() {
332
320
  ───────────────────────────────────────
333
321
 
334
322
  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)
323
+ Config: ${getConfigDir()}/config.json
324
+ Database: ${getDbPath()}
325
+ Server: http://localhost:18800 (default)
326
+ Proxy: http://localhost:18900 (default)
339
327
  `);
340
328
  }
341
329
  function cmdResetToken() {
342
- const config = (0, config_js_1.resetToken)();
330
+ const config = resetToken();
343
331
  console.log(`Token reset. New token: ${config.token}`);
344
332
  }
345
333
  async function cmdStart(flags) {
346
- const config = (0, config_js_1.ensureConfig)();
347
- const serverPort = flags["port"] ? parseInt(flags["port"], 10) : 8080;
334
+ const verbose = "v" in flags || "verbose" in flags;
335
+ const daemon = "d" in flags || "daemon" in flags;
336
+ // Set log level for verbose mode
337
+ if (verbose) {
338
+ process.env.LOG_LEVEL = "debug";
339
+ }
340
+ // Handle daemon mode: fork process and exit
341
+ if (daemon && !process.env.AGENTGAZER_DAEMON_CHILD) {
342
+ const { spawn } = await import("node:child_process");
343
+ const configDir = getConfigDir();
344
+ const pidFile = path.join(configDir, "agentgazer.pid");
345
+ const logFile = path.join(configDir, "agentgazer.log");
346
+ // Check if already running
347
+ if (fs.existsSync(pidFile)) {
348
+ const existingPid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
349
+ try {
350
+ process.kill(existingPid, 0); // Check if process exists
351
+ console.error(`AgentGazer is already running (PID: ${existingPid})`);
352
+ console.error(`Use "agentgazer stop" to stop it first.`);
353
+ process.exit(1);
354
+ }
355
+ catch {
356
+ // Process doesn't exist, clean up stale PID file
357
+ fs.unlinkSync(pidFile);
358
+ }
359
+ }
360
+ // Open log file for output
361
+ const logFd = fs.openSync(logFile, "a");
362
+ const args = process.argv.slice(2).filter((a) => a !== "-d" && a !== "--daemon");
363
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
364
+ detached: true,
365
+ stdio: ["ignore", logFd, logFd],
366
+ env: { ...process.env, AGENTGAZER_DAEMON_CHILD: "1" },
367
+ });
368
+ child.unref();
369
+ fs.closeSync(logFd);
370
+ // Write PID file
371
+ fs.writeFileSync(pidFile, String(child.pid));
372
+ const config = ensureConfig();
373
+ // Use config values as defaults for daemon mode display
374
+ const serverPort = flags["port"] ? parseInt(flags["port"], 10) : (config.server?.port ?? 18800);
375
+ const proxyPort = flags["proxy-port"] ? parseInt(flags["proxy-port"], 10) : (config.server?.proxyPort ?? 18900);
376
+ console.log(`
377
+ ╔════════════════════════════════════════════════════╗
378
+ ║ AgentGazer started (daemon) ║
379
+ ╠════════════════════════════════════════════════════╣
380
+ ║ ║
381
+ ║ Dashboard: http://localhost:${String(serverPort).padEnd(5)} ║
382
+ ║ Proxy: http://localhost:${String(proxyPort).padEnd(5)} ║
383
+ ║ ║
384
+ ║ Token: ${config.token.padEnd(32)} ║
385
+ ║ ║
386
+ ╚════════════════════════════════════════════════════╝
387
+
388
+ Process running in background (PID: ${child.pid})
389
+ Logs: ${logFile}
390
+
391
+ To stop: agentgazer stop
392
+ To logs: agentgazer logs -f
393
+ `);
394
+ process.exit(0);
395
+ }
396
+ const config = ensureConfig();
397
+ // Use config values as defaults, CLI flags override
398
+ const defaultPort = config.server?.port ?? 18800;
399
+ const defaultProxyPort = config.server?.proxyPort ?? 18900;
400
+ const defaultRetentionDays = config.data?.retentionDays ?? 30;
401
+ const defaultAutoOpen = config.server?.autoOpen ?? true;
402
+ const requestedServerPort = flags["port"] ? parseInt(flags["port"], 10) : defaultPort;
348
403
  const proxyPort = flags["proxy-port"]
349
404
  ? parseInt(flags["proxy-port"], 10)
350
- : 4000;
351
- if (isNaN(serverPort) || serverPort < 1 || serverPort > 65535) {
405
+ : defaultProxyPort;
406
+ if (isNaN(requestedServerPort) || requestedServerPort < 1 || requestedServerPort > 65535) {
352
407
  console.error("Error: --port must be a valid port number (1-65535)");
353
408
  process.exit(1);
354
409
  }
@@ -356,15 +411,35 @@ async function cmdStart(flags) {
356
411
  console.error("Error: --proxy-port must be a valid port number (1-65535)");
357
412
  process.exit(1);
358
413
  }
359
- if (serverPort < 1024) {
360
- console.warn("Warning: port %d may require elevated privileges on Unix systems.", serverPort);
414
+ if (requestedServerPort < 1024) {
415
+ console.warn("Warning: port %d may require elevated privileges on Unix systems.", requestedServerPort);
361
416
  }
362
417
  if (proxyPort < 1024) {
363
418
  console.warn("Warning: proxy port %d may require elevated privileges on Unix systems.", proxyPort);
364
419
  }
420
+ // Check if proxy port is available (no auto-switching for proxy)
421
+ if (!(await isPortAvailable(proxyPort))) {
422
+ console.error(`Error: Proxy port ${proxyPort} is already in use.`);
423
+ console.error(" The proxy port must be fixed for OpenClaw configuration to work.");
424
+ console.error(" Please stop the process using this port or specify a different port with --proxy-port <number>");
425
+ process.exit(1);
426
+ }
427
+ // Find available port for dashboard (auto-increment if in use)
428
+ let serverPort;
429
+ try {
430
+ serverPort = await findAvailablePort(requestedServerPort);
431
+ if (serverPort !== requestedServerPort) {
432
+ console.log(` Dashboard port ${requestedServerPort} is in use, using ${serverPort} instead.`);
433
+ }
434
+ }
435
+ catch {
436
+ console.error(`Error: Could not find available port starting from ${requestedServerPort}`);
437
+ console.error(" Try specifying a different port with --port <number>");
438
+ process.exit(1);
439
+ }
365
440
  const retentionDays = flags["retention-days"]
366
441
  ? parseInt(flags["retention-days"], 10)
367
- : 30;
442
+ : defaultRetentionDays;
368
443
  if (isNaN(retentionDays) || retentionDays < 1) {
369
444
  console.error("Error: --retention-days must be a positive integer");
370
445
  process.exit(1);
@@ -388,38 +463,42 @@ async function cmdStart(flags) {
388
463
  // ignore
389
464
  }
390
465
  }
466
+ // Initialize secret store first (needed for both server and proxy)
467
+ const configDir = getConfigDir();
468
+ const configPath = path.join(configDir, "config.json");
469
+ const { store, backendName } = await detectSecretStore(configDir);
391
470
  // Start the local server
392
- const { db, shutdown: shutdownServer } = await (0, server_1.startServer)({
471
+ const { db, shutdown: shutdownServer } = await startServer({
393
472
  port: serverPort,
394
473
  token: config.token,
395
- dbPath: (0, config_js_1.getDbPath)(),
474
+ dbPath: getDbPath(),
396
475
  dashboardDir,
397
476
  retentionDays,
477
+ secretStore: store,
478
+ configPath,
398
479
  });
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
480
  console.log(` Secret backend: ${backendName}`);
404
481
  // Auto-migrate plaintext keys from config.json to secret store
405
- const migratedCount = await (0, secret_store_js_1.migrateFromPlaintextConfig)(configPath, store);
482
+ const migratedCount = await migrateFromPlaintextConfig(configPath, store);
406
483
  if (migratedCount > 0) {
407
484
  console.log(` Migrated ${migratedCount} provider key(s) from config.json to secret store.`);
408
485
  }
409
486
  // Load provider keys from secret store
410
- const providerKeys = await (0, secret_store_js_1.loadProviderKeys)(store);
487
+ const providerKeys = await loadProviderKeys(store);
411
488
  // Start the LLM proxy (with db for policy enforcement and rate limits)
412
- const { shutdown: shutdownProxy } = (0, proxy_1.startProxy)({
489
+ const { shutdown: shutdownProxy } = startProxy({
413
490
  apiKey: config.token,
414
491
  agentId: "proxy",
415
492
  port: proxyPort,
416
493
  endpoint: `http://localhost:${serverPort}/api/events`,
417
494
  providerKeys,
418
495
  db, // Rate limits are loaded from db
496
+ secretStore: store, // For hot-reloading provider keys
419
497
  });
498
+ const modeLabel = verbose ? "running (verbose)" : "running";
420
499
  console.log(`
421
500
  ╔════════════════════════════════════════════════════╗
422
- ║ AgentGazer running
501
+ ║ AgentGazer ${modeLabel.padEnd(21)}
423
502
  ╠════════════════════════════════════════════════════╣
424
503
  ║ ║
425
504
  ║ Dashboard: http://localhost:${String(serverPort).padEnd(5)} ║
@@ -430,10 +509,11 @@ async function cmdStart(flags) {
430
509
  ╚════════════════════════════════════════════════════╝
431
510
 
432
511
  Proxy routes: http://localhost:${proxyPort}/{provider}/...
433
- Providers: ${shared_1.KNOWN_PROVIDER_NAMES.join(", ")}
512
+ Providers: ${KNOWN_PROVIDER_NAMES.join(", ")}
434
513
  `);
435
- // Auto-open browser unless --no-open
436
- if (!("no-open" in flags)) {
514
+ // Auto-open browser unless --no-open flag or autoOpen=false in config
515
+ const shouldAutoOpen = !("no-open" in flags) && defaultAutoOpen;
516
+ if (shouldAutoOpen) {
437
517
  try {
438
518
  const open = await import("open");
439
519
  await open.default(`http://localhost:${serverPort}`);
@@ -444,17 +524,36 @@ async function cmdStart(flags) {
444
524
  }
445
525
  // Graceful shutdown
446
526
  let shuttingDown = false;
527
+ let forceExitTimeout = null;
528
+ const pidFile = path.join(configDir, "agentgazer.pid");
447
529
  function handleShutdown() {
448
- if (shuttingDown)
449
- return;
530
+ if (shuttingDown) {
531
+ // Second Ctrl+C: force exit immediately
532
+ console.log("\nForce exiting...");
533
+ process.exit(1);
534
+ }
450
535
  shuttingDown = true;
451
- console.log("\nShutting down...");
536
+ console.log("\nShutting down... (press Ctrl+C again to force exit)");
537
+ // Force exit after 5 seconds if graceful shutdown hangs
538
+ forceExitTimeout = setTimeout(() => {
539
+ console.error("Shutdown timed out, forcing exit.");
540
+ process.exit(1);
541
+ }, 5000);
542
+ forceExitTimeout.unref();
452
543
  Promise.all([shutdownProxy(), shutdownServer()])
453
544
  .then(() => {
545
+ if (forceExitTimeout)
546
+ clearTimeout(forceExitTimeout);
547
+ // Clean up PID file if we're a daemon child
548
+ if (process.env.AGENTGAZER_DAEMON_CHILD && fs.existsSync(pidFile)) {
549
+ fs.unlinkSync(pidFile);
550
+ }
454
551
  console.log("Shutdown complete.");
455
552
  process.exit(0);
456
553
  })
457
554
  .catch((err) => {
555
+ if (forceExitTimeout)
556
+ clearTimeout(forceExitTimeout);
458
557
  console.error("Error during shutdown:", err);
459
558
  process.exit(1);
460
559
  });
@@ -468,69 +567,18 @@ async function cmdStart(flags) {
468
567
  });
469
568
  }
470
569
  // ---------------------------------------------------------------------------
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
570
+ // Subcommands
523
571
  // ---------------------------------------------------------------------------
524
- function cmdVersion() {
525
- // eslint-disable-next-line @typescript-eslint/no-var-requires
526
- const pkg = require(path.resolve(__dirname, "../package.json"));
572
+ async function cmdVersion() {
573
+ const pkgPath = path.resolve(__dirname, "../package.json");
574
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
527
575
  console.log(`agentgazer ${pkg.version}`);
528
576
  }
529
577
  async function cmdDoctor(flags) {
530
- const port = flags["port"] ? parseInt(flags["port"], 10) : 8080;
578
+ const port = flags["port"] ? parseInt(flags["port"], 10) : 18800;
531
579
  const proxyPort = flags["proxy-port"]
532
580
  ? parseInt(flags["proxy-port"], 10)
533
- : 4000;
581
+ : 18900;
534
582
  if (isNaN(port) || port < 1 || port > 65535) {
535
583
  console.error("Error: --port must be a valid port number (1-65535)");
536
584
  process.exit(1);
@@ -546,7 +594,7 @@ async function cmdDoctor(flags) {
546
594
  let passed = 0;
547
595
  const total = 6;
548
596
  // 1. Config file exists
549
- const config = (0, config_js_1.readConfig)();
597
+ const config = readConfig();
550
598
  if (config) {
551
599
  console.log(" ✓ Config file exists");
552
600
  passed++;
@@ -563,7 +611,7 @@ async function cmdDoctor(flags) {
563
611
  console.log(" ✗ Auth token not set");
564
612
  }
565
613
  // 3. Database file exists
566
- if (fs.existsSync((0, config_js_1.getDbPath)())) {
614
+ if (fs.existsSync(getDbPath())) {
567
615
  console.log(" ✓ Database file exists");
568
616
  passed++;
569
617
  }
@@ -572,7 +620,7 @@ async function cmdDoctor(flags) {
572
620
  }
573
621
  // 4. Secret store accessible
574
622
  try {
575
- const { store } = await (0, secret_store_js_1.detectSecretStore)((0, config_js_1.getConfigDir)());
623
+ const { store } = await detectSecretStore(getConfigDir());
576
624
  if (await store.isAvailable()) {
577
625
  console.log(" ✓ Secret store accessible");
578
626
  passed++;
@@ -614,161 +662,417 @@ async function cmdDoctor(flags) {
614
662
  }
615
663
  console.log(`\n ${passed}/${total} checks passed.`);
616
664
  }
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.");
665
+ // ---------------------------------------------------------------------------
666
+ // Uninstall
667
+ // ---------------------------------------------------------------------------
668
+ async function confirmPrompt(message) {
669
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
670
+ const answer = await ask(rl, message);
671
+ rl.close();
672
+ return /^y(es)?$/i.test(answer);
673
+ }
674
+ async function stopDaemonIfRunning() {
675
+ const configDir = getConfigDir();
676
+ const pidFile = path.join(configDir, "agentgazer.pid");
677
+ if (!fs.existsSync(pidFile))
627
678
  return;
679
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
680
+ try {
681
+ process.kill(pid, 0); // Check if process exists
628
682
  }
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}`);
683
+ catch {
684
+ // Process doesn't exist, clean up stale PID file
685
+ fs.unlinkSync(pidFile);
686
+ return;
638
687
  }
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;
688
+ console.log(" Stopping AgentGazer daemon...");
689
+ process.kill(pid, "SIGTERM");
690
+ // Wait for process to exit (poll for up to 3 seconds)
691
+ for (let i = 0; i < 30; i++) {
692
+ await new Promise((r) => setTimeout(r, 100));
693
+ try {
694
+ process.kill(pid, 0);
655
695
  }
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);
696
+ catch {
697
+ if (fs.existsSync(pidFile))
698
+ fs.unlinkSync(pidFile);
699
+ console.log(" Daemon stopped");
700
+ return;
663
701
  }
664
702
  }
665
- let data;
703
+ // Force kill
666
704
  try {
667
- data = (await apiGet(`/api/stats/${encodeURIComponent(agentId)}?range=${encodeURIComponent(range)}`, port));
705
+ process.kill(pid, "SIGKILL");
668
706
  }
669
- catch (err) {
670
- if (err && typeof err === "object" && "message" in err) {
671
- console.error(`Error fetching stats for "${agentId}": ${err.message}`);
707
+ catch {
708
+ // Already dead
709
+ }
710
+ if (fs.existsSync(pidFile))
711
+ fs.unlinkSync(pidFile);
712
+ console.log(" ✓ Daemon stopped (forced)");
713
+ }
714
+ async function removeProviderKeys() {
715
+ const configDir = getConfigDir();
716
+ const { store } = await detectSecretStore(configDir);
717
+ const providers = await store.list(PROVIDER_SERVICE);
718
+ if (providers.length === 0) {
719
+ console.log(" No provider keys found.");
720
+ return;
721
+ }
722
+ console.log(" Removing provider keys...");
723
+ for (const provider of providers) {
724
+ try {
725
+ await store.delete(PROVIDER_SERVICE, provider);
726
+ console.log(` ✓ ${provider}`);
672
727
  }
673
- else {
674
- console.error(`Error fetching stats for "${agentId}".`);
728
+ catch (err) {
729
+ console.log(` ${provider}: ${err}`);
675
730
  }
676
- process.exit(1);
677
731
  }
678
- const errorPct = data.total_requests > 0
679
- ? ((data.total_errors / data.total_requests) * 100).toFixed(2)
680
- : "0.00";
732
+ }
733
+ function removeConfig() {
734
+ const configDir = getConfigDir();
735
+ const configPath = path.join(configDir, "config.json");
736
+ if (fs.existsSync(configPath)) {
737
+ fs.unlinkSync(configPath);
738
+ console.log(` ✓ Removed config (${configPath})`);
739
+ }
740
+ else {
741
+ console.log(" Config file not found.");
742
+ }
743
+ }
744
+ function removeAgentData() {
745
+ const configDir = getConfigDir();
746
+ const dbPath = path.join(configDir, "data.db");
747
+ if (fs.existsSync(dbPath)) {
748
+ const stats = fs.statSync(dbPath);
749
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
750
+ fs.unlinkSync(dbPath);
751
+ console.log(` ✓ Removed database (${sizeMB} MB)`);
752
+ }
753
+ else {
754
+ console.log(" Database file not found.");
755
+ }
756
+ }
757
+ function removeLogFiles() {
758
+ const configDir = getConfigDir();
759
+ const logFile = path.join(configDir, "agentgazer.log");
760
+ const pidFile = path.join(configDir, "agentgazer.pid");
761
+ let removed = 0;
762
+ if (fs.existsSync(logFile)) {
763
+ fs.unlinkSync(logFile);
764
+ removed++;
765
+ }
766
+ if (fs.existsSync(pidFile)) {
767
+ fs.unlinkSync(pidFile);
768
+ removed++;
769
+ }
770
+ if (removed > 0) {
771
+ console.log(` ✓ Removed log files`);
772
+ }
773
+ }
774
+ function showBinaryRemovalCommands() {
681
775
  console.log(`
682
- AgentGazer Stats for "${agentId}" (last ${range})
683
- ───────────────────────────────────────
776
+ To remove the agentgazer binary:
684
777
 
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)}
778
+ npm uninstall -g @agentgazer/cli
689
779
 
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();
780
+ Or if installed via Homebrew:
781
+
782
+ brew uninstall agentgazer
783
+ `);
700
784
  }
701
- // ---------------------------------------------------------------------------
702
- // Uninstall
703
- // ---------------------------------------------------------------------------
704
785
  async function cmdUninstall(flags) {
705
- const home = process.env.AGENTGAZER_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.AGENTGAZER_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('');
786
+ const configDir = getConfigDir();
787
+ const skipPrompt = "yes" in flags;
788
+ // Handle flags for scripting
789
+ if ("all" in flags) {
790
+ if (!skipPrompt) {
791
+ const confirmed = await confirmPrompt("\n This will remove ALL AgentGazer data. Continue? [y/N] ");
792
+ if (!confirmed) {
793
+ console.log(" Cancelled.");
794
+ return;
795
+ }
796
+ }
797
+ await stopDaemonIfRunning();
798
+ await removeProviderKeys();
799
+ removeConfig();
800
+ removeAgentData();
801
+ removeLogFiles();
802
+ showBinaryRemovalCommands();
719
803
  return;
720
804
  }
721
- const skipPrompt = "yes" in flags;
805
+ if ("config" in flags) {
806
+ if (!skipPrompt) {
807
+ const confirmed = await confirmPrompt("\n Remove config file? [y/N] ");
808
+ if (!confirmed) {
809
+ console.log(" Cancelled.");
810
+ return;
811
+ }
812
+ }
813
+ await stopDaemonIfRunning();
814
+ removeConfig();
815
+ return;
816
+ }
817
+ if ("keys" in flags) {
818
+ if (!skipPrompt) {
819
+ const { store } = await detectSecretStore(configDir);
820
+ const providers = await store.list(PROVIDER_SERVICE);
821
+ if (providers.length === 0) {
822
+ console.log(" No provider keys to remove.");
823
+ return;
824
+ }
825
+ console.log(`\n Provider keys to remove: ${providers.join(", ")}`);
826
+ const confirmed = await confirmPrompt(" Continue? [y/N] ");
827
+ if (!confirmed) {
828
+ console.log(" Cancelled.");
829
+ return;
830
+ }
831
+ }
832
+ await removeProviderKeys();
833
+ return;
834
+ }
835
+ if ("data" in flags) {
836
+ if (!skipPrompt) {
837
+ const dbPath = path.join(configDir, "data.db");
838
+ if (!fs.existsSync(dbPath)) {
839
+ console.log(" No database to remove.");
840
+ return;
841
+ }
842
+ const confirmed = await confirmPrompt("\n Remove agent data (database)? [y/N] ");
843
+ if (!confirmed) {
844
+ console.log(" Cancelled.");
845
+ return;
846
+ }
847
+ }
848
+ await stopDaemonIfRunning();
849
+ removeAgentData();
850
+ return;
851
+ }
852
+ // Interactive menu
722
853
  console.log(`
723
854
  AgentGazer — Uninstall
724
855
  ───────────────────────────────────────
856
+
857
+ What would you like to remove?
858
+
859
+ 1. Complete uninstall (everything)
860
+ 2. Binary only (show npm/brew command)
861
+ 3. Config only (~/.agentgazer/config.json)
862
+ 4. Provider keys only (from secret store)
863
+ 5. Agent data only (~/.agentgazer/data.db)
864
+
725
865
  `);
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})`);
866
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
867
+ const choice = await ask(rl, " Select [1-5]: ");
868
+ rl.close();
869
+ console.log("");
870
+ switch (choice) {
871
+ case "1": {
872
+ // Complete uninstall
873
+ const confirmed = await confirmPrompt(" This will remove ALL AgentGazer data. Continue? [y/N] ");
874
+ if (!confirmed) {
875
+ console.log(" Cancelled.");
876
+ return;
877
+ }
878
+ await stopDaemonIfRunning();
879
+ await removeProviderKeys();
880
+ removeConfig();
881
+ removeAgentData();
882
+ removeLogFiles();
883
+ showBinaryRemovalCommands();
884
+ break;
739
885
  }
740
- catch {
741
- console.log(` ! Could not remove ${wrapperPath} — try: sudo rm ${wrapperPath}`);
886
+ case "2":
887
+ // Binary only
888
+ showBinaryRemovalCommands();
889
+ break;
890
+ case "3": {
891
+ // Config only
892
+ const confirmed = await confirmPrompt(" Remove config file? [y/N] ");
893
+ if (!confirmed) {
894
+ console.log(" Cancelled.");
895
+ return;
896
+ }
897
+ await stopDaemonIfRunning();
898
+ removeConfig();
899
+ break;
742
900
  }
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);
901
+ case "4": {
902
+ // Provider keys only
903
+ const { store } = await detectSecretStore(configDir);
904
+ const providers = await store.list(PROVIDER_SERVICE);
905
+ if (providers.length === 0) {
906
+ console.log(" No provider keys to remove.");
907
+ return;
908
+ }
909
+ console.log(` Provider keys to remove: ${providers.join(", ")}`);
910
+ const confirmed = await confirmPrompt(" Continue? [y/N] ");
911
+ if (!confirmed) {
912
+ console.log(" Cancelled.");
913
+ return;
914
+ }
915
+ await removeProviderKeys();
916
+ break;
755
917
  }
756
- if (removeData) {
757
- fs.rmSync(home, { recursive: true, force: true });
758
- console.log(` ✓ Removed all data (${home})`);
918
+ case "5": {
919
+ // Agent data only
920
+ const dbPath = path.join(configDir, "data.db");
921
+ if (!fs.existsSync(dbPath)) {
922
+ console.log(" No database to remove.");
923
+ return;
924
+ }
925
+ const confirmed = await confirmPrompt(" Remove agent data (database)? [y/N] ");
926
+ if (!confirmed) {
927
+ console.log(" Cancelled.");
928
+ return;
929
+ }
930
+ await stopDaemonIfRunning();
931
+ removeAgentData();
932
+ break;
759
933
  }
760
- else {
761
- console.log(` User data preserved at ${home}`);
934
+ default:
935
+ console.log(" Invalid option.");
936
+ }
937
+ }
938
+ // ---------------------------------------------------------------------------
939
+ // Stop command
940
+ // ---------------------------------------------------------------------------
941
+ function cmdStop() {
942
+ const configDir = getConfigDir();
943
+ const pidFile = path.join(configDir, "agentgazer.pid");
944
+ const config = readConfig();
945
+ const port = config?.server?.port ?? 18800;
946
+ const proxyPort = config?.server?.proxyPort ?? 18900;
947
+ if (!fs.existsSync(pidFile)) {
948
+ // No PID file, but check if processes are running on ports
949
+ const pidsOnPorts = findPidsOnPorts([port, proxyPort]);
950
+ if (pidsOnPorts.length > 0) {
951
+ console.log("AgentGazer PID file not found, but processes detected on ports.");
952
+ console.log(`Killing processes: ${pidsOnPorts.join(", ")}...`);
953
+ for (const pid of pidsOnPorts) {
954
+ try {
955
+ process.kill(pid, "SIGTERM");
956
+ }
957
+ catch {
958
+ // Ignore errors
959
+ }
960
+ }
961
+ // Wait a bit and force kill if needed
962
+ setTimeout(() => {
963
+ for (const pid of pidsOnPorts) {
964
+ try {
965
+ process.kill(pid, 0);
966
+ process.kill(pid, "SIGKILL");
967
+ }
968
+ catch {
969
+ // Already dead
970
+ }
971
+ }
972
+ console.log("AgentGazer stopped.");
973
+ }, 1000);
974
+ return;
762
975
  }
976
+ console.log("AgentGazer is not running.");
977
+ return;
978
+ }
979
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
980
+ try {
981
+ process.kill(pid, 0); // Check if process exists
982
+ }
983
+ catch {
984
+ // Process doesn't exist
985
+ fs.unlinkSync(pidFile);
986
+ console.log("AgentGazer is not running (stale PID file removed).");
987
+ return;
988
+ }
989
+ // Send SIGTERM
990
+ try {
991
+ process.kill(pid, "SIGTERM");
992
+ console.log(`Stopping AgentGazer (PID: ${pid})...`);
993
+ // Wait for process to exit (poll for up to 5 seconds)
994
+ let attempts = 0;
995
+ const maxAttempts = 50;
996
+ const checkInterval = 100;
997
+ const waitForExit = () => {
998
+ try {
999
+ process.kill(pid, 0);
1000
+ attempts++;
1001
+ if (attempts < maxAttempts) {
1002
+ setTimeout(waitForExit, checkInterval);
1003
+ }
1004
+ else {
1005
+ console.log("Process did not exit gracefully, sending SIGKILL...");
1006
+ try {
1007
+ process.kill(pid, "SIGKILL");
1008
+ }
1009
+ catch {
1010
+ // Already dead
1011
+ }
1012
+ if (fs.existsSync(pidFile))
1013
+ fs.unlinkSync(pidFile);
1014
+ console.log("AgentGazer stopped.");
1015
+ }
1016
+ }
1017
+ catch {
1018
+ // Process exited
1019
+ if (fs.existsSync(pidFile))
1020
+ fs.unlinkSync(pidFile);
1021
+ console.log("AgentGazer stopped.");
1022
+ }
1023
+ };
1024
+ waitForExit();
1025
+ }
1026
+ catch (err) {
1027
+ console.error(`Failed to stop AgentGazer: ${err}`);
1028
+ process.exit(1);
1029
+ }
1030
+ }
1031
+ // ---------------------------------------------------------------------------
1032
+ // Logs command
1033
+ // ---------------------------------------------------------------------------
1034
+ async function cmdLogs(flags) {
1035
+ const configDir = getConfigDir();
1036
+ const logFile = path.join(configDir, "agentgazer.log");
1037
+ if (!fs.existsSync(logFile)) {
1038
+ console.log("No log file found. Start AgentGazer with -d flag first.");
1039
+ console.log(` agentgazer start -d`);
1040
+ return;
1041
+ }
1042
+ const follow = "f" in flags || "follow" in flags;
1043
+ const lines = flags["n"] ? parseInt(flags["n"], 10) : (flags["lines"] ? parseInt(flags["lines"], 10) : 50);
1044
+ if (follow) {
1045
+ // Follow mode: tail -f equivalent
1046
+ const { spawn } = await import("node:child_process");
1047
+ const tail = spawn("tail", ["-f", "-n", String(lines), logFile], {
1048
+ stdio: "inherit",
1049
+ });
1050
+ // Handle Ctrl+C gracefully
1051
+ process.on("SIGINT", () => {
1052
+ tail.kill();
1053
+ process.exit(0);
1054
+ });
1055
+ await new Promise((resolve) => {
1056
+ tail.on("close", () => resolve());
1057
+ });
1058
+ }
1059
+ else {
1060
+ // Just show last N lines
1061
+ const content = fs.readFileSync(logFile, "utf-8");
1062
+ const allLines = content.split("\n");
1063
+ const lastLines = allLines.slice(-lines).join("\n");
1064
+ console.log(lastLines);
763
1065
  }
764
- console.log("\n ✓ AgentGazer uninstalled.\n");
765
1066
  }
766
1067
  // ---------------------------------------------------------------------------
767
1068
  // Main
768
1069
  // ---------------------------------------------------------------------------
769
1070
  async function main() {
770
1071
  const subcommand = process.argv[2];
771
- const flags = parseFlags(process.argv.slice(3));
1072
+ const allArgs = process.argv.slice(3);
1073
+ const flags = parseFlags(allArgs);
1074
+ const positional = parsePositional(allArgs);
1075
+ const port = flags["port"] ? parseInt(flags["port"], 10) : 18800;
772
1076
  switch (subcommand) {
773
1077
  case "onboard":
774
1078
  await cmdOnboard();
@@ -776,27 +1080,46 @@ async function main() {
776
1080
  case "start":
777
1081
  await cmdStart(flags);
778
1082
  break;
1083
+ case "stop":
1084
+ cmdStop();
1085
+ break;
1086
+ case "logs":
1087
+ await cmdLogs(flags);
1088
+ break;
779
1089
  case "status":
780
1090
  cmdStatus();
781
1091
  break;
782
1092
  case "reset-token":
783
1093
  cmdResetToken();
784
1094
  break;
1095
+ case "overview": {
1096
+ const { cmdOverview } = await import("./commands/overview.js");
1097
+ await cmdOverview(port);
1098
+ break;
1099
+ }
1100
+ case "agents":
1101
+ await cmdAgents(port);
1102
+ break;
1103
+ case "agent":
1104
+ await cmdAgent(positional[0], positional[1], positional.slice(2), flags);
1105
+ break;
785
1106
  case "providers":
786
- await cmdProviders(process.argv.slice(3));
1107
+ await cmdProviders(port);
1108
+ break;
1109
+ case "provider":
1110
+ await cmdProvider(positional[0], positional.slice(1), flags);
1111
+ break;
1112
+ case "events":
1113
+ await cmdEvents(flags);
787
1114
  break;
788
1115
  case "version":
789
- cmdVersion();
1116
+ case "--version":
1117
+ case "-V":
1118
+ await cmdVersion();
790
1119
  break;
791
1120
  case "doctor":
792
1121
  await cmdDoctor(flags);
793
1122
  break;
794
- case "agents":
795
- await cmdAgents(flags);
796
- break;
797
- case "stats":
798
- await cmdStats(flags);
799
- break;
800
1123
  case "uninstall":
801
1124
  await cmdUninstall(flags);
802
1125
  break;