@agentgazer/cli 0.2.1 → 0.3.1

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