@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.
- package/README.md +1 -1
- package/dist/cli.js +786 -408
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +469 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +70 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/events.d.ts +21 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +247 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/overview.d.ts +2 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +8 -0
- package/dist/commands/overview.js.map +1 -0
- package/dist/commands/provider.d.ts +2 -0
- package/dist/commands/provider.d.ts.map +1 -0
- package/dist/commands/provider.js +210 -0
- package/dist/commands/provider.js.map +1 -0
- package/dist/commands/providers.d.ts +2 -0
- package/dist/commands/providers.d.ts.map +1 -0
- package/dist/commands/providers.js +77 -0
- package/dist/commands/providers.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +97 -60
- package/dist/config.js.map +1 -1
- package/dist/dashboard/assets/index-5AtWzXiJ.js +143 -0
- package/dist/dashboard/assets/index-B7NvhJW9.js +143 -0
- package/dist/dashboard/assets/index-B9izZ4lr.js +139 -0
- package/dist/dashboard/assets/index-BADwu2fU.js +143 -0
- package/dist/dashboard/assets/index-BBonREkg.js +143 -0
- package/dist/dashboard/assets/index-BGdw1Ss1.js +139 -0
- package/dist/dashboard/assets/index-BLIagyHw.css +1 -0
- package/dist/dashboard/assets/index-BN2gLOCd.js +143 -0
- package/dist/dashboard/assets/index-BOcemxE3.js +143 -0
- package/dist/dashboard/assets/index-BYYSUJ-G.js +143 -0
- package/dist/dashboard/assets/index-BhGhcADW.js +143 -0
- package/dist/dashboard/assets/index-Bjr8f_u_.js +143 -0
- package/dist/dashboard/assets/index-BsaxUL45.js +143 -0
- package/dist/dashboard/assets/index-C-DguelG.css +1 -0
- package/dist/dashboard/assets/index-C-QgKsob.js +143 -0
- package/dist/dashboard/assets/index-C6t4YWSY.js +143 -0
- package/dist/dashboard/assets/index-C9EQrGpe.js +143 -0
- package/dist/dashboard/assets/index-CB6k7nz0.js +143 -0
- package/dist/dashboard/assets/index-CBCLOBvd.js +139 -0
- package/dist/dashboard/assets/index-CGwUmIVh.js +139 -0
- package/dist/dashboard/assets/index-CLMyGFxA.js +145 -0
- package/dist/dashboard/assets/index-CYnfgO3k.js +143 -0
- package/dist/dashboard/assets/index-CiGGhaUw.js +143 -0
- package/dist/dashboard/assets/index-Cj3GjdoQ.js +143 -0
- package/dist/dashboard/assets/index-CxEl0O9p.js +143 -0
- package/dist/dashboard/assets/index-D2mLKlRw.css +1 -0
- package/dist/dashboard/assets/index-D5AUmtdo.js +139 -0
- package/dist/dashboard/assets/index-DACbampq.css +1 -0
- package/dist/dashboard/assets/index-DB0fFjgr.js +139 -0
- package/dist/dashboard/assets/index-DI-JqWdh.css +1 -0
- package/dist/dashboard/assets/index-DRcLEFcl.js +143 -0
- package/dist/dashboard/assets/index-DaUEJqLn.css +1 -0
- package/dist/dashboard/assets/index-Db5ik9Wo.js +143 -0
- package/dist/dashboard/assets/index-DfV-2ewH.css +1 -0
- package/dist/dashboard/assets/index-DgEq2MBB.css +1 -0
- package/dist/dashboard/assets/index-DhmVjP8v.js +143 -0
- package/dist/dashboard/assets/index-Dipd1pd3.js +143 -0
- package/dist/dashboard/assets/index-DirRSEOl.js +143 -0
- package/dist/dashboard/assets/index-Dr9wOl47.js +143 -0
- package/dist/dashboard/assets/index-ee-BCW5i.js +143 -0
- package/dist/dashboard/assets/index-g4KzDTwu.js +139 -0
- package/dist/dashboard/assets/index-odup_N3G.css +1 -0
- package/dist/dashboard/assets/index-wHHHG0bR.css +1 -0
- package/dist/dashboard/assets/index-wNn2zMSu.js +143 -0
- package/dist/dashboard/assets/index-wvU3UdVf.js +143 -0
- package/dist/dashboard/assets/index-yHwKni1U.css +1 -0
- package/dist/dashboard/assets/index-z-YzEeVY.js +143 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +1 -11
- package/dist/index.js.map +1 -1
- package/dist/secret-store.js +26 -68
- package/dist/secret-store.js.map +1 -1
- package/dist/tui/AgentTable.d.ts +15 -0
- package/dist/tui/AgentTable.d.ts.map +1 -0
- package/dist/tui/AgentTable.js +35 -0
- package/dist/tui/AgentTable.js.map +1 -0
- package/dist/tui/EventLog.d.ts +15 -0
- package/dist/tui/EventLog.d.ts.map +1 -0
- package/dist/tui/EventLog.js +15 -0
- package/dist/tui/EventLog.js.map +1 -0
- package/dist/tui/HelpOverlay.d.ts +7 -0
- package/dist/tui/HelpOverlay.d.ts.map +1 -0
- package/dist/tui/HelpOverlay.js +11 -0
- package/dist/tui/HelpOverlay.js.map +1 -0
- package/dist/tui/Overview.d.ts +7 -0
- package/dist/tui/Overview.d.ts.map +1 -0
- package/dist/tui/Overview.js +69 -0
- package/dist/tui/Overview.js.map +1 -0
- package/dist/tui/StatusBar.d.ts +11 -0
- package/dist/tui/StatusBar.d.ts.map +1 -0
- package/dist/tui/StatusBar.js +27 -0
- package/dist/tui/StatusBar.js.map +1 -0
- package/dist/utils/api.d.ts +10 -0
- package/dist/utils/api.d.ts.map +1 -0
- package/dist/utils/api.js +114 -0
- package/dist/utils/api.js.map +1 -0
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +56 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/prompt.d.ts +4 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +40 -0
- package/dist/utils/prompt.js.map +1 -0
- package/package.json +13 -6
package/dist/cli.js
CHANGED
|
@@ -1,47 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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:
|
|
109
|
-
--proxy-port <number> LLM proxy port (default:
|
|
110
|
-
--retention-days <number> Data retention
|
|
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
|
|
114
|
-
--
|
|
115
|
-
|
|
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
|
|
118
|
-
--
|
|
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
|
|
125
|
-
agentgazer start
|
|
126
|
-
agentgazer start
|
|
127
|
-
agentgazer
|
|
128
|
-
agentgazer
|
|
129
|
-
agentgazer
|
|
130
|
-
agentgazer
|
|
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 =
|
|
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 =
|
|
144
|
-
console.log(
|
|
145
|
-
|
|
146
|
-
───────────────────────────────────────
|
|
293
|
+
const saved = ensureConfig();
|
|
294
|
+
console.log(ASCII_LOGO);
|
|
295
|
+
console.log(` ───────────────────────────────────────
|
|
147
296
|
|
|
148
297
|
Token: ${saved.token}
|
|
149
|
-
Config: ${
|
|
150
|
-
Database: ${
|
|
151
|
-
Server: http://localhost:
|
|
152
|
-
Proxy: http://localhost:
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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: ${
|
|
336
|
-
Database: ${
|
|
337
|
-
Server: http://localhost:
|
|
338
|
-
Proxy: http://localhost:
|
|
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 =
|
|
385
|
+
const config = resetToken();
|
|
343
386
|
console.log(`Token reset. New token: ${config.token}`);
|
|
344
387
|
}
|
|
345
388
|
async function cmdStart(flags) {
|
|
346
|
-
const
|
|
347
|
-
const
|
|
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
|
-
:
|
|
351
|
-
if (isNaN(
|
|
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 (
|
|
360
|
-
console.warn("Warning: port %d may require elevated privileges on Unix systems.",
|
|
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
|
-
:
|
|
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
|
|
526
|
+
const { db, shutdown: shutdownServer } = await startServer({
|
|
393
527
|
port: serverPort,
|
|
394
528
|
token: config.token,
|
|
395
|
-
dbPath:
|
|
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
|
|
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
|
|
542
|
+
const providerKeys = await loadProviderKeys(store);
|
|
411
543
|
// Start the LLM proxy (with db for policy enforcement and rate limits)
|
|
412
|
-
const { shutdown: shutdownProxy } =
|
|
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
|
|
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: ${
|
|
567
|
+
Providers: ${KNOWN_PROVIDER_NAMES.join(", ")}
|
|
434
568
|
`);
|
|
435
|
-
// Auto-open browser unless --no-open
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
526
|
-
const pkg =
|
|
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) :
|
|
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
|
-
:
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
758
|
+
// Force kill
|
|
666
759
|
try {
|
|
667
|
-
|
|
760
|
+
process.kill(pid, "SIGKILL");
|
|
668
761
|
}
|
|
669
|
-
catch
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
674
|
-
console.
|
|
783
|
+
catch (err) {
|
|
784
|
+
console.log(` ✗ ${provider}: ${err}`);
|
|
675
785
|
}
|
|
676
|
-
process.exit(1);
|
|
677
786
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
683
|
-
───────────────────────────────────────
|
|
831
|
+
To remove the agentgazer binary:
|
|
684
832
|
|
|
685
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
741
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
761
|
-
console.log(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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;
|