@michael-joseph-miller/ant-bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/browser-OHRD7YI3.js +524 -0
- package/dist/browser-OHRD7YI3.js.map +7 -0
- package/dist/bundled-46DUK5PG.js +133 -0
- package/dist/bundled-46DUK5PG.js.map +7 -0
- package/dist/chunk-7BOHBPB2.js +287 -0
- package/dist/chunk-7BOHBPB2.js.map +7 -0
- package/dist/chunk-AHAON6J7.js +24 -0
- package/dist/chunk-AHAON6J7.js.map +7 -0
- package/dist/chunk-DYIJTUMY.js +321 -0
- package/dist/chunk-DYIJTUMY.js.map +7 -0
- package/dist/chunk-KSQOVWP5.js +310 -0
- package/dist/chunk-KSQOVWP5.js.map +7 -0
- package/dist/chunk-Z2YT2PZN.js +64 -0
- package/dist/chunk-Z2YT2PZN.js.map +7 -0
- package/dist/index.js +1562 -0
- package/dist/index.js.map +7 -0
- package/dist/install-IY2M3OUQ.js +36 -0
- package/dist/install-IY2M3OUQ.js.map +7 -0
- package/dist/plugin-POMHVGD4.js +53 -0
- package/dist/plugin-POMHVGD4.js.map +7 -0
- package/dist/scheduler-Q7OHNGP6.js +274 -0
- package/dist/scheduler-Q7OHNGP6.js.map +7 -0
- package/dist/server.js +3016 -0
- package/dist/server.js.map +7 -0
- package/dist/skills-66WRX64H.js +17 -0
- package/dist/skills-66WRX64H.js.map +7 -0
- package/dist/skills-spec.js +172 -0
- package/dist/skills-spec.js.map +7 -0
- package/dist/tools-P5537ASX.js +148 -0
- package/dist/tools-P5537ASX.js.map +7 -0
- package/package.json +51 -0
- package/skills/README.md +69 -0
- package/skills/SPEC.md +274 -0
- package/skills/bug-repro/SKILL.md +53 -0
- package/skills/deep-research/SKILL.md +451 -0
- package/skills/deep-research/references/V6_1_improvements.md +112 -0
- package/skills/deep-research/references/completeness_review_checklist.md +25 -0
- package/skills/deep-research/references/counter_review_team_guide.md +181 -0
- package/skills/deep-research/references/enterprise_analysis_frameworks.md +135 -0
- package/skills/deep-research/references/enterprise_mode.md +99 -0
- package/skills/deep-research/references/enterprise_quality_checklist.md +160 -0
- package/skills/deep-research/references/enterprise_research_methodology.md +164 -0
- package/skills/deep-research/references/formatting_rules.md +31 -0
- package/skills/deep-research/references/quality_gates.md +77 -0
- package/skills/deep-research/references/report_template_v6.md +82 -0
- package/skills/deep-research/references/research_notes_format.md +147 -0
- package/skills/deep-research/references/research_plan_checklist.md +26 -0
- package/skills/deep-research/references/research_report_template.md +49 -0
- package/skills/deep-research/references/source_accessibility_policy.md +179 -0
- package/skills/deep-research/references/source_quality_rubric.md +28 -0
- package/skills/deep-research/references/subagent_prompt.md +116 -0
- package/skills/inbox-digest/SKILL.md +51 -0
- package/skills/skill-author/SKILL.md +92 -0
- package/skills/weekly-report/SKILL.md +49 -0
- package/web/dist/assets/index-BLQ8rPiN.js +130 -0
- package/web/dist/assets/index-IEIkG_jd.css +2 -0
- package/web/dist/index.html +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1562 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __antbotCreateRequire } from 'node:module';
|
|
3
|
+
const require = __antbotCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
SettingsSchema
|
|
6
|
+
} from "./chunk-DYIJTUMY.js";
|
|
7
|
+
|
|
8
|
+
// packages/cli/src/index.ts
|
|
9
|
+
import fs10 from "node:fs";
|
|
10
|
+
import readline2 from "node:readline/promises";
|
|
11
|
+
import path11 from "node:path";
|
|
12
|
+
|
|
13
|
+
// packages/cli/src/args.ts
|
|
14
|
+
var KNOWN_COMMANDS = [
|
|
15
|
+
"start",
|
|
16
|
+
"stop",
|
|
17
|
+
"restart",
|
|
18
|
+
"status",
|
|
19
|
+
"doctor",
|
|
20
|
+
"open",
|
|
21
|
+
"skill",
|
|
22
|
+
"backup",
|
|
23
|
+
"restore",
|
|
24
|
+
"update"
|
|
25
|
+
];
|
|
26
|
+
var CliError = class extends Error {
|
|
27
|
+
};
|
|
28
|
+
var COMMAND_FLAGS = {
|
|
29
|
+
start: {
|
|
30
|
+
port: { type: "number" },
|
|
31
|
+
foreground: { type: "boolean", default: false },
|
|
32
|
+
open: { type: "boolean", default: false }
|
|
33
|
+
},
|
|
34
|
+
stop: {},
|
|
35
|
+
restart: {
|
|
36
|
+
port: { type: "number" }
|
|
37
|
+
},
|
|
38
|
+
status: {},
|
|
39
|
+
doctor: {},
|
|
40
|
+
open: {},
|
|
41
|
+
skill: {},
|
|
42
|
+
backup: {
|
|
43
|
+
out: { type: "string" }
|
|
44
|
+
},
|
|
45
|
+
restore: {
|
|
46
|
+
yes: { type: "boolean", default: false }
|
|
47
|
+
},
|
|
48
|
+
update: {
|
|
49
|
+
check: { type: "boolean", default: false },
|
|
50
|
+
yes: { type: "boolean", default: false }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function isKnownCommand(value) {
|
|
54
|
+
return KNOWN_COMMANDS.includes(value);
|
|
55
|
+
}
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
if (argv.length === 0) {
|
|
58
|
+
return { command: null, help: true, version: false, positionals: [], flags: {} };
|
|
59
|
+
}
|
|
60
|
+
const first = argv[0];
|
|
61
|
+
if (first === "--version" || first === "-v") {
|
|
62
|
+
return { command: null, help: false, version: true, positionals: [], flags: {} };
|
|
63
|
+
}
|
|
64
|
+
if (first === "--help" || first === "-h") {
|
|
65
|
+
return { command: null, help: true, version: false, positionals: [], flags: {} };
|
|
66
|
+
}
|
|
67
|
+
if (first.startsWith("-")) {
|
|
68
|
+
throw new CliError(`Unknown option: ${first}
|
|
69
|
+
Run "antbot --help" for usage.`);
|
|
70
|
+
}
|
|
71
|
+
if (!isKnownCommand(first)) {
|
|
72
|
+
throw new CliError(`Unknown command: "${first}"
|
|
73
|
+
Run "antbot --help" for usage.`);
|
|
74
|
+
}
|
|
75
|
+
const command = first;
|
|
76
|
+
const spec = COMMAND_FLAGS[command];
|
|
77
|
+
const rest = argv.slice(1);
|
|
78
|
+
const flags = {};
|
|
79
|
+
for (const [name, def] of Object.entries(spec)) {
|
|
80
|
+
if (def.default !== void 0) flags[name] = def.default;
|
|
81
|
+
}
|
|
82
|
+
const positionals = [];
|
|
83
|
+
let help = false;
|
|
84
|
+
for (let i = 0; i < rest.length; i++) {
|
|
85
|
+
const tok = rest[i];
|
|
86
|
+
if (tok === "--help" || tok === "-h") {
|
|
87
|
+
help = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (tok.startsWith("--")) {
|
|
91
|
+
let name = tok.slice(2);
|
|
92
|
+
let inlineValue;
|
|
93
|
+
const eq = name.indexOf("=");
|
|
94
|
+
if (eq !== -1) {
|
|
95
|
+
inlineValue = name.slice(eq + 1);
|
|
96
|
+
name = name.slice(0, eq);
|
|
97
|
+
}
|
|
98
|
+
const def = spec[name];
|
|
99
|
+
if (!def) {
|
|
100
|
+
throw new CliError(`Unknown flag "--${name}" for command "${command}"`);
|
|
101
|
+
}
|
|
102
|
+
if (def.type === "boolean") {
|
|
103
|
+
flags[name] = inlineValue === void 0 ? true : inlineValue !== "false";
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const raw = inlineValue !== void 0 ? inlineValue : rest[++i];
|
|
107
|
+
if (raw === void 0) {
|
|
108
|
+
throw new CliError(`Flag "--${name}" requires a value`);
|
|
109
|
+
}
|
|
110
|
+
if (def.type === "number") {
|
|
111
|
+
const num = Number(raw);
|
|
112
|
+
if (Number.isNaN(num)) {
|
|
113
|
+
throw new CliError(`Flag "--${name}" must be a number, got "${raw}"`);
|
|
114
|
+
}
|
|
115
|
+
flags[name] = num;
|
|
116
|
+
} else {
|
|
117
|
+
flags[name] = raw;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
positionals.push(tok);
|
|
122
|
+
}
|
|
123
|
+
return { command, help, version: false, positionals, flags };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// packages/cli/src/help.ts
|
|
127
|
+
var GLOBAL_HELP = `antbot \u2014 a local daemon and web UI for a roster of persistent Claude teammates.
|
|
128
|
+
|
|
129
|
+
Usage: antbot <command> [options]
|
|
130
|
+
|
|
131
|
+
Getting started:
|
|
132
|
+
antbot doctor Check your environment (Node, claude CLI + login, data dir, port)
|
|
133
|
+
antbot open Open the web UI, starting the daemon if it isn't running
|
|
134
|
+
|
|
135
|
+
Daemon:
|
|
136
|
+
start [options] Start the daemon in the background
|
|
137
|
+
stop Stop the daemon
|
|
138
|
+
restart [--port N] Stop and start again
|
|
139
|
+
status Show whether the daemon is running, and where
|
|
140
|
+
open Open the web UI (starts the daemon first if needed)
|
|
141
|
+
doctor Diagnose the local environment
|
|
142
|
+
|
|
143
|
+
Data:
|
|
144
|
+
skill <subcommand> Manage the skills your bots can use
|
|
145
|
+
backup [--out PATH] Archive the database, config, skills, and bot memory
|
|
146
|
+
restore <path> Restore from a backup archive
|
|
147
|
+
|
|
148
|
+
Maintenance:
|
|
149
|
+
update [--check] Update ant-bot to the latest published version
|
|
150
|
+
|
|
151
|
+
Options:
|
|
152
|
+
-h, --help Show help; "antbot <command> --help" for one command
|
|
153
|
+
-v, --version Show the CLI version
|
|
154
|
+
|
|
155
|
+
Environment:
|
|
156
|
+
ANTBOT_HOME Data directory (default: ~/.ant-bot)
|
|
157
|
+
|
|
158
|
+
The UI lives at http://127.0.0.1:4780 and is served by the daemon itself.`;
|
|
159
|
+
var COMMAND_HELP = {
|
|
160
|
+
start: `antbot start \u2014 start the daemon in the background
|
|
161
|
+
|
|
162
|
+
Usage: antbot start [--port N] [--open] [--foreground]
|
|
163
|
+
|
|
164
|
+
--port N Port to listen on (default: from config, usually 4780)
|
|
165
|
+
--open Open the web UI once the daemon is healthy
|
|
166
|
+
--foreground Run in this terminal instead of detaching (Ctrl-C stops it)
|
|
167
|
+
|
|
168
|
+
Detached output goes to the log in your data directory; "antbot status" tells
|
|
169
|
+
you where that is. Starting an already-running daemon is a no-op.`,
|
|
170
|
+
stop: `antbot stop \u2014 stop the daemon
|
|
171
|
+
|
|
172
|
+
Usage: antbot stop
|
|
173
|
+
|
|
174
|
+
Sends SIGTERM, then SIGKILL if it hasn't exited after 10 seconds. Stopping a
|
|
175
|
+
daemon that isn't running succeeds quietly.`,
|
|
176
|
+
restart: `antbot restart \u2014 stop the daemon and start it again
|
|
177
|
+
|
|
178
|
+
Usage: antbot restart [--port N]
|
|
179
|
+
|
|
180
|
+
--port N Port to listen on when starting back up
|
|
181
|
+
|
|
182
|
+
Bots, threads, and memory survive a restart.`,
|
|
183
|
+
status: `antbot status \u2014 show daemon status
|
|
184
|
+
|
|
185
|
+
Usage: antbot status
|
|
186
|
+
|
|
187
|
+
Prints the URL, version, data directory, and bot count. Exits non-zero when the
|
|
188
|
+
daemon is not running, so it works in scripts.`,
|
|
189
|
+
doctor: `antbot doctor \u2014 diagnose the local environment
|
|
190
|
+
|
|
191
|
+
Usage: antbot doctor
|
|
192
|
+
|
|
193
|
+
Checks Node.js version, the claude CLI, Claude authentication, ANTHROPIC_API_KEY,
|
|
194
|
+
data directory permissions, port availability, better-sqlite3, and Playwright's
|
|
195
|
+
Chromium install. Each failure prints the command that fixes it.
|
|
196
|
+
|
|
197
|
+
Exits non-zero if any check fails.`,
|
|
198
|
+
open: `antbot open \u2014 open the web UI
|
|
199
|
+
|
|
200
|
+
Usage: antbot open
|
|
201
|
+
|
|
202
|
+
Prints the URL and hands it to your browser. If the daemon isn't running yet,
|
|
203
|
+
it is started first \u2014 so this is the only command you need day to day.`,
|
|
204
|
+
skill: `antbot skill \u2014 manage the skills your bots can use
|
|
205
|
+
|
|
206
|
+
Usage:
|
|
207
|
+
antbot skill list
|
|
208
|
+
antbot skill add <source>
|
|
209
|
+
antbot skill remove <slug>
|
|
210
|
+
antbot skill lint [path]
|
|
211
|
+
|
|
212
|
+
A skill is a directory containing a SKILL.md. \`add\` accepts:
|
|
213
|
+
|
|
214
|
+
owner/repo a GitHub repository
|
|
215
|
+
github.com/owner/repo the same, written out
|
|
216
|
+
github.com/owner/repo#v2 pinned to a branch or tag
|
|
217
|
+
github.com/owner/repo/tree/main/x one skill inside a monorepo
|
|
218
|
+
git@github.com:owner/repo.git any git URL
|
|
219
|
+
./path/to/skill a local directory
|
|
220
|
+
https://host/path/SKILL.md a single skill file
|
|
221
|
+
|
|
222
|
+
A repository holding several skills installs all of them. Re-installing an
|
|
223
|
+
existing skill upgrades it in place and keeps it assigned to its bots.
|
|
224
|
+
|
|
225
|
+
Skills committed to the ant-bot project's own skills/ directory are installed
|
|
226
|
+
automatically on every start.
|
|
227
|
+
|
|
228
|
+
\`lint\` checks skills against the Agent Skills spec (skills/SPEC.md) and needs
|
|
229
|
+
no running daemon. With no path it checks everything installed; give it a path
|
|
230
|
+
to check one skill you are still writing, or a directory of them:
|
|
231
|
+
|
|
232
|
+
antbot skill lint every installed skill
|
|
233
|
+
antbot skill lint ./skills a directory of skills
|
|
234
|
+
antbot skill lint ./skills/my-skill one skill
|
|
235
|
+
|
|
236
|
+
Exit status is 1 if anything is an error, 0 if only warnings.`,
|
|
237
|
+
backup: `antbot backup \u2014 create a backup archive
|
|
238
|
+
|
|
239
|
+
Usage: antbot backup [--out PATH]
|
|
240
|
+
|
|
241
|
+
--out PATH Output path for the tar.gz (default: ~/.ant-bot/backups/<timestamp>.tar.gz)
|
|
242
|
+
|
|
243
|
+
Includes antbot.db, config.toml, skills, and each bot's memory directory.
|
|
244
|
+
Excludes browser-profile and attachments.`,
|
|
245
|
+
update: `antbot update \u2014 update ant-bot to the latest published version
|
|
246
|
+
|
|
247
|
+
Usage: antbot update [--check] [--yes]
|
|
248
|
+
|
|
249
|
+
--check Report whether a newer version exists and exit; change nothing
|
|
250
|
+
--yes Skip the confirmation prompt
|
|
251
|
+
|
|
252
|
+
Runs the package manager that installed this copy, then restarts the daemon if
|
|
253
|
+
it was running. The check is cached for a day, so it costs no network most of
|
|
254
|
+
the time. ant-bot never updates itself in the background: the daemon holds a
|
|
255
|
+
live database handle and may be mid-turn.
|
|
256
|
+
|
|
257
|
+
In a git checkout there is nothing for a package manager to update \u2014 use
|
|
258
|
+
\`git pull && pnpm install\` instead.`,
|
|
259
|
+
restore: `antbot restore \u2014 restore from a backup archive
|
|
260
|
+
|
|
261
|
+
Usage: antbot restore <path> [--yes]
|
|
262
|
+
|
|
263
|
+
--yes Skip the confirmation prompt
|
|
264
|
+
|
|
265
|
+
Overwrites files in your data directory. Stop the daemon first.`
|
|
266
|
+
};
|
|
267
|
+
function commandHelp(command) {
|
|
268
|
+
return COMMAND_HELP[command];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// packages/cli/src/version.ts
|
|
272
|
+
import fs from "node:fs";
|
|
273
|
+
import path from "node:path";
|
|
274
|
+
import { fileURLToPath } from "node:url";
|
|
275
|
+
function readVersionFrom(here, exists, read) {
|
|
276
|
+
let dir = path.resolve(here);
|
|
277
|
+
for (; ; ) {
|
|
278
|
+
const pkg = path.join(dir, "package.json");
|
|
279
|
+
if (exists(pkg)) {
|
|
280
|
+
try {
|
|
281
|
+
return JSON.parse(read(pkg)).version ?? "0.0.0";
|
|
282
|
+
} catch {
|
|
283
|
+
return "0.0.0";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const parent = path.dirname(dir);
|
|
287
|
+
if (parent === dir) return "0.0.0";
|
|
288
|
+
dir = parent;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function getCliVersion() {
|
|
292
|
+
return readVersionFrom(
|
|
293
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
294
|
+
(p) => fs.existsSync(p),
|
|
295
|
+
(p) => fs.readFileSync(p, "utf8")
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// packages/cli/src/doctor.ts
|
|
300
|
+
import path2 from "node:path";
|
|
301
|
+
function pass(name, message) {
|
|
302
|
+
return { name, status: "pass", message };
|
|
303
|
+
}
|
|
304
|
+
function warn(name, message, hint) {
|
|
305
|
+
return { name, status: "warn", message, hint };
|
|
306
|
+
}
|
|
307
|
+
function fail(name, message, hint) {
|
|
308
|
+
return { name, status: "fail", message, hint };
|
|
309
|
+
}
|
|
310
|
+
function checkNodeVersion(deps) {
|
|
311
|
+
const major = Number.parseInt(deps.nodeVersion.split(".")[0] ?? "0", 10);
|
|
312
|
+
if (Number.isFinite(major) && major >= 24) {
|
|
313
|
+
return pass("Node.js version", `v${deps.nodeVersion}`);
|
|
314
|
+
}
|
|
315
|
+
return fail(
|
|
316
|
+
"Node.js version",
|
|
317
|
+
`v${deps.nodeVersion} (ant-bot requires >= 24)`,
|
|
318
|
+
"Install Node.js 24 or later, e.g. via nvm: `nvm install 24`."
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
async function checkClaudeCli(deps) {
|
|
322
|
+
try {
|
|
323
|
+
const res = await deps.exec("claude", ["--version"]);
|
|
324
|
+
if (res.code === 0) {
|
|
325
|
+
return pass("claude CLI", res.stdout.trim() || "installed");
|
|
326
|
+
}
|
|
327
|
+
return fail(
|
|
328
|
+
"claude CLI",
|
|
329
|
+
`"claude --version" exited with code ${res.code}`,
|
|
330
|
+
"Install with `npm i -g @anthropic-ai/claude-code`."
|
|
331
|
+
);
|
|
332
|
+
} catch {
|
|
333
|
+
return fail(
|
|
334
|
+
"claude CLI",
|
|
335
|
+
"not found on PATH",
|
|
336
|
+
"Install with `npm i -g @anthropic-ai/claude-code`."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function checkClaudeAuth(deps) {
|
|
341
|
+
const credentialsPath = path2.join(deps.homeDir, ".claude", ".credentials.json");
|
|
342
|
+
const claudeJsonPath = path2.join(deps.homeDir, ".claude.json");
|
|
343
|
+
if (deps.fileExists(credentialsPath)) {
|
|
344
|
+
return pass("Claude authentication", `Found ${credentialsPath}`);
|
|
345
|
+
}
|
|
346
|
+
if (deps.fileExists(claudeJsonPath) && deps.fileSize(claudeJsonPath) > 50) {
|
|
347
|
+
return pass("Claude authentication", `Found ${claudeJsonPath}`);
|
|
348
|
+
}
|
|
349
|
+
return warn(
|
|
350
|
+
"Claude authentication",
|
|
351
|
+
"No Claude Code credentials found",
|
|
352
|
+
"Run `claude` once and log in."
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
function checkApiKeyEnv(deps) {
|
|
356
|
+
const value = deps.env.ANTHROPIC_API_KEY;
|
|
357
|
+
if (!value) {
|
|
358
|
+
return pass("ANTHROPIC_API_KEY", "Not set (turns bill to your Claude subscription)");
|
|
359
|
+
}
|
|
360
|
+
return warn(
|
|
361
|
+
"ANTHROPIC_API_KEY",
|
|
362
|
+
"Set in environment",
|
|
363
|
+
"Set, but ant-bot strips it so turns bill to your subscription. Set billingMode='api' in settings if you want API billing instead."
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
function checkDataDirWritable(deps) {
|
|
367
|
+
if (deps.isDirWritable(deps.dataDir)) {
|
|
368
|
+
return pass("Data directory", `${deps.dataDir} is writable`);
|
|
369
|
+
}
|
|
370
|
+
return fail(
|
|
371
|
+
"Data directory",
|
|
372
|
+
`${deps.dataDir} is not writable`,
|
|
373
|
+
`Check permissions on ${deps.dataDir}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
async function checkPortAvailability(deps) {
|
|
377
|
+
const state = await deps.probePort(deps.port);
|
|
378
|
+
if (state === "free") {
|
|
379
|
+
return pass("Port", `Port ${deps.port} is free`);
|
|
380
|
+
}
|
|
381
|
+
const health = await deps.healthCheck(deps.port);
|
|
382
|
+
if (health.ok) {
|
|
383
|
+
return pass(
|
|
384
|
+
"Port",
|
|
385
|
+
`Port ${deps.port} is in use by a running antbot daemon${health.version ? ` (v${health.version})` : ""}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return fail(
|
|
389
|
+
"Port",
|
|
390
|
+
`Port ${deps.port} is in use by another process`,
|
|
391
|
+
`Stop the other process or run \`antbot start --port <N>\`.`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
async function checkBetterSqlite3(deps) {
|
|
395
|
+
try {
|
|
396
|
+
await deps.importBetterSqlite3();
|
|
397
|
+
return pass("better-sqlite3", "native module loads");
|
|
398
|
+
} catch (err) {
|
|
399
|
+
return fail(
|
|
400
|
+
"better-sqlite3",
|
|
401
|
+
`failed to load (${err.message})`,
|
|
402
|
+
"Run `pnpm rebuild better-sqlite3`."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function checkPlaywrightChromium(deps) {
|
|
407
|
+
try {
|
|
408
|
+
const pw = await deps.importPlaywright();
|
|
409
|
+
const execPath = pw.chromium.executablePath();
|
|
410
|
+
if (deps.fileExists(execPath)) {
|
|
411
|
+
return pass("Playwright Chromium", execPath);
|
|
412
|
+
}
|
|
413
|
+
return warn(
|
|
414
|
+
"Playwright Chromium",
|
|
415
|
+
`Expected browser binary at ${execPath}`,
|
|
416
|
+
"Run `npx playwright install chromium`."
|
|
417
|
+
);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return warn(
|
|
420
|
+
"Playwright Chromium",
|
|
421
|
+
`Could not verify Chromium install (${err.message})`,
|
|
422
|
+
"Run `npx playwright install chromium`."
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function runDoctor(deps) {
|
|
427
|
+
const results = [
|
|
428
|
+
checkNodeVersion(deps),
|
|
429
|
+
await checkClaudeCli(deps),
|
|
430
|
+
checkClaudeAuth(deps),
|
|
431
|
+
checkApiKeyEnv(deps),
|
|
432
|
+
checkDataDirWritable(deps),
|
|
433
|
+
await checkPortAvailability(deps),
|
|
434
|
+
await checkBetterSqlite3(deps),
|
|
435
|
+
await checkPlaywrightChromium(deps)
|
|
436
|
+
];
|
|
437
|
+
const exitCode = results.some((r) => r.status === "fail") ? 1 : 0;
|
|
438
|
+
return { results, exitCode };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// packages/cli/src/proc.ts
|
|
442
|
+
import { spawn } from "node:child_process";
|
|
443
|
+
function runCommand(cmd, args, opts = {}) {
|
|
444
|
+
return new Promise((resolve, reject) => {
|
|
445
|
+
const child = spawn(cmd, args, { cwd: opts.cwd });
|
|
446
|
+
let stdout = "";
|
|
447
|
+
let stderr = "";
|
|
448
|
+
child.stdout?.on("data", (d) => {
|
|
449
|
+
stdout += d.toString();
|
|
450
|
+
});
|
|
451
|
+
child.stderr?.on("data", (d) => {
|
|
452
|
+
stderr += d.toString();
|
|
453
|
+
});
|
|
454
|
+
child.on("error", reject);
|
|
455
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// packages/cli/src/net.ts
|
|
460
|
+
import net from "node:net";
|
|
461
|
+
function sleep(ms) {
|
|
462
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
463
|
+
}
|
|
464
|
+
function probePort(port, host = "127.0.0.1") {
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
const socket = net.connect({ port, host });
|
|
467
|
+
let settled = false;
|
|
468
|
+
const done = (state) => {
|
|
469
|
+
if (settled) return;
|
|
470
|
+
settled = true;
|
|
471
|
+
socket.destroy();
|
|
472
|
+
resolve(state);
|
|
473
|
+
};
|
|
474
|
+
socket.setTimeout(1e3);
|
|
475
|
+
socket.once("connect", () => done("occupied"));
|
|
476
|
+
socket.once("timeout", () => done("free"));
|
|
477
|
+
socket.once("error", () => done("free"));
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async function getHealth(port) {
|
|
481
|
+
try {
|
|
482
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
483
|
+
signal: AbortSignal.timeout(2e3)
|
|
484
|
+
});
|
|
485
|
+
if (!res.ok) return { ok: false };
|
|
486
|
+
const body = await res.json().catch(() => null);
|
|
487
|
+
return { ok: true, version: typeof body?.version === "string" ? body.version : void 0 };
|
|
488
|
+
} catch {
|
|
489
|
+
return { ok: false };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function getBotCount(port) {
|
|
493
|
+
try {
|
|
494
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/bots`, {
|
|
495
|
+
signal: AbortSignal.timeout(2e3)
|
|
496
|
+
});
|
|
497
|
+
if (!res.ok) return null;
|
|
498
|
+
const body = await res.json().catch(() => null);
|
|
499
|
+
if (Array.isArray(body)) return body.length;
|
|
500
|
+
if (body && typeof body === "object" && Array.isArray(body.bots)) {
|
|
501
|
+
return body.bots.length;
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function waitForHealth(port, timeoutMs) {
|
|
509
|
+
const deadline = Date.now() + timeoutMs;
|
|
510
|
+
do {
|
|
511
|
+
const health = await getHealth(port);
|
|
512
|
+
if (health.ok) return true;
|
|
513
|
+
await sleep(300);
|
|
514
|
+
} while (Date.now() < deadline);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
var BASE = (port) => `http://127.0.0.1:${port}`;
|
|
518
|
+
async function unwrap(res) {
|
|
519
|
+
if (!res.ok) {
|
|
520
|
+
const body = await res.json().catch(() => null);
|
|
521
|
+
throw new Error(body?.error ?? `HTTP ${res.status}`);
|
|
522
|
+
}
|
|
523
|
+
return await res.json();
|
|
524
|
+
}
|
|
525
|
+
async function getJson(port, path12) {
|
|
526
|
+
return unwrap(await fetch(`${BASE(port)}${path12}`, { signal: AbortSignal.timeout(1e4) }));
|
|
527
|
+
}
|
|
528
|
+
async function postJson(port, path12, body) {
|
|
529
|
+
return unwrap(
|
|
530
|
+
await fetch(`${BASE(port)}${path12}`, {
|
|
531
|
+
method: "POST",
|
|
532
|
+
headers: { "content-type": "application/json" },
|
|
533
|
+
body: JSON.stringify(body),
|
|
534
|
+
// Cloning a repository can take a while on a slow link.
|
|
535
|
+
signal: AbortSignal.timeout(18e4)
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
async function deleteJson(port, path12) {
|
|
540
|
+
return unwrap(
|
|
541
|
+
await fetch(`${BASE(port)}${path12}`, { method: "DELETE", signal: AbortSignal.timeout(1e4) })
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// packages/server/dist/config/config.js
|
|
546
|
+
import fs3 from "node:fs";
|
|
547
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
548
|
+
|
|
549
|
+
// packages/server/dist/config/paths.js
|
|
550
|
+
import os from "node:os";
|
|
551
|
+
import path3 from "node:path";
|
|
552
|
+
import fs2 from "node:fs";
|
|
553
|
+
function resolvePaths(root) {
|
|
554
|
+
const base = root ?? process.env.ANTBOT_HOME ?? path3.join(os.homedir(), ".ant-bot");
|
|
555
|
+
return {
|
|
556
|
+
root: base,
|
|
557
|
+
db: path3.join(base, "antbot.db"),
|
|
558
|
+
config: path3.join(base, "config.toml"),
|
|
559
|
+
workspace: path3.join(base, "workspace"),
|
|
560
|
+
attachments: path3.join(base, "attachments"),
|
|
561
|
+
skills: path3.join(base, "skills"),
|
|
562
|
+
browserProfile: path3.join(base, "browser-profile"),
|
|
563
|
+
backups: path3.join(base, "backups"),
|
|
564
|
+
secrets: path3.join(base, "secrets.json"),
|
|
565
|
+
logs: path3.join(base, "logs")
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function ensureDirs(p) {
|
|
569
|
+
for (const d of [
|
|
570
|
+
p.root,
|
|
571
|
+
p.workspace,
|
|
572
|
+
p.attachments,
|
|
573
|
+
p.skills,
|
|
574
|
+
p.backups,
|
|
575
|
+
p.logs,
|
|
576
|
+
path3.join(p.workspace, "projects"),
|
|
577
|
+
path3.join(p.workspace, "bots")
|
|
578
|
+
]) {
|
|
579
|
+
fs2.mkdirSync(d, { recursive: true });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// packages/server/dist/config/config.js
|
|
584
|
+
var DEFAULT_PORT = 4780;
|
|
585
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
586
|
+
function loadConfig(root) {
|
|
587
|
+
const paths = resolvePaths(root);
|
|
588
|
+
ensureDirs(paths);
|
|
589
|
+
let raw = {};
|
|
590
|
+
if (fs3.existsSync(paths.config)) {
|
|
591
|
+
try {
|
|
592
|
+
raw = parseToml(fs3.readFileSync(paths.config, "utf8"));
|
|
593
|
+
} catch {
|
|
594
|
+
raw = {};
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const server = raw.server ?? {};
|
|
598
|
+
const settings = SettingsSchema.parse({
|
|
599
|
+
...typeof raw.settings === "object" && raw.settings ? raw.settings : {},
|
|
600
|
+
timezone: raw.settings?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC"
|
|
601
|
+
});
|
|
602
|
+
const cfg = {
|
|
603
|
+
paths,
|
|
604
|
+
settings,
|
|
605
|
+
port: Number(process.env.ANTBOT_PORT ?? server.port ?? DEFAULT_PORT),
|
|
606
|
+
host: String(server.host ?? DEFAULT_HOST)
|
|
607
|
+
};
|
|
608
|
+
if (!fs3.existsSync(paths.config))
|
|
609
|
+
writeConfig(cfg);
|
|
610
|
+
return cfg;
|
|
611
|
+
}
|
|
612
|
+
function writeConfig(cfg) {
|
|
613
|
+
fs3.writeFileSync(cfg.paths.config, stringifyToml({ server: { port: cfg.port, host: cfg.host }, settings: cfg.settings }));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// packages/cli/src/serverBridge.ts
|
|
617
|
+
import fs4 from "node:fs";
|
|
618
|
+
import path4 from "node:path";
|
|
619
|
+
import { createRequire } from "node:module";
|
|
620
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
621
|
+
function packageRootOf(from, exists) {
|
|
622
|
+
let dir = path4.resolve(from);
|
|
623
|
+
for (; ; ) {
|
|
624
|
+
if (exists(path4.join(dir, "package.json"))) return dir;
|
|
625
|
+
const parent = path4.dirname(dir);
|
|
626
|
+
if (parent === dir) return null;
|
|
627
|
+
dir = parent;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function resolveServerPackageDir(deps) {
|
|
631
|
+
try {
|
|
632
|
+
return deps.realpath(path4.dirname(deps.resolve("@antbot/server/package.json")));
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
const pkg = packageRootOf(deps.here, deps.exists);
|
|
636
|
+
if (pkg) {
|
|
637
|
+
const link = path4.join(pkg, "node_modules", "@antbot", "server");
|
|
638
|
+
if (deps.exists(link)) {
|
|
639
|
+
try {
|
|
640
|
+
return deps.realpath(link);
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return pkg;
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
var require_ = createRequire(import.meta.url);
|
|
649
|
+
function findServerPackageDir() {
|
|
650
|
+
return resolveServerPackageDir({
|
|
651
|
+
here: path4.dirname(fileURLToPath2(import.meta.url)),
|
|
652
|
+
resolve: (s) => require_.resolve(s),
|
|
653
|
+
realpath: (p) => fs4.realpathSync(p),
|
|
654
|
+
exists: (p) => fs4.existsSync(p)
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async function importServerDependency(specifier) {
|
|
658
|
+
const serverDir = findServerPackageDir();
|
|
659
|
+
if (!serverDir) {
|
|
660
|
+
throw new Error(`could not locate the @antbot/server package to resolve "${specifier}"`);
|
|
661
|
+
}
|
|
662
|
+
const req = createRequire(path4.join(serverDir, "package.json"));
|
|
663
|
+
const resolved = req.resolve(specifier);
|
|
664
|
+
return import(pathToFileURL(resolved).href);
|
|
665
|
+
}
|
|
666
|
+
function serverModuleCandidates(bareSpecifier, bundledFile, deps) {
|
|
667
|
+
const out = [bareSpecifier];
|
|
668
|
+
const pkg = packageRootOf(deps.here, deps.exists);
|
|
669
|
+
if (pkg) {
|
|
670
|
+
const sibling = path4.join(pkg, "dist", bundledFile);
|
|
671
|
+
if (deps.exists(sibling)) out.push(pathToFileURL(sibling).href);
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
function candidates(bare, bundled) {
|
|
676
|
+
return serverModuleCandidates(bare, bundled, {
|
|
677
|
+
here: path4.dirname(fileURLToPath2(import.meta.url)),
|
|
678
|
+
exists: (p) => fs4.existsSync(p)
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async function importFirst(specs, what) {
|
|
682
|
+
let firstError;
|
|
683
|
+
for (const spec of specs) {
|
|
684
|
+
try {
|
|
685
|
+
return await import(spec);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
firstError ??= err;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
throw new Error(`could not load ${what}: ${firstError?.message ?? "not found"}`);
|
|
691
|
+
}
|
|
692
|
+
async function startAntbotServer(opts) {
|
|
693
|
+
const mod = await importFirst(candidates("@antbot/server", "server.js"), "the ant-bot daemon");
|
|
694
|
+
if (typeof mod.startServer !== "function") {
|
|
695
|
+
throw new Error("@antbot/server does not export startServer() yet.");
|
|
696
|
+
}
|
|
697
|
+
return mod.startServer(opts);
|
|
698
|
+
}
|
|
699
|
+
async function importSkillSpec() {
|
|
700
|
+
return await importFirst(
|
|
701
|
+
candidates("@antbot/server/skills/spec.js", "skills-spec.js"),
|
|
702
|
+
"the skill spec validator"
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// packages/cli/src/color.ts
|
|
707
|
+
var noColor = Boolean(process.env.NO_COLOR) || !process.stdout.isTTY;
|
|
708
|
+
function wrap(code) {
|
|
709
|
+
return (s) => noColor ? s : `\x1B[${code}m${s}\x1B[0m`;
|
|
710
|
+
}
|
|
711
|
+
var bold = wrap("1");
|
|
712
|
+
var dim = wrap("2");
|
|
713
|
+
var red = wrap("31");
|
|
714
|
+
var green = wrap("32");
|
|
715
|
+
var yellow = wrap("33");
|
|
716
|
+
var cyan = wrap("36");
|
|
717
|
+
|
|
718
|
+
// packages/cli/src/daemon.ts
|
|
719
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
720
|
+
import fs6 from "node:fs";
|
|
721
|
+
import path6 from "node:path";
|
|
722
|
+
|
|
723
|
+
// packages/cli/src/pid.ts
|
|
724
|
+
import fs5 from "node:fs";
|
|
725
|
+
import path5 from "node:path";
|
|
726
|
+
function pidFilePath(dataDir) {
|
|
727
|
+
return path5.join(dataDir, "antbot.pid");
|
|
728
|
+
}
|
|
729
|
+
function readPidFile(file) {
|
|
730
|
+
try {
|
|
731
|
+
const raw = fs5.readFileSync(file, "utf8");
|
|
732
|
+
const data = JSON.parse(raw);
|
|
733
|
+
if (typeof data.pid === "number" && typeof data.port === "number") {
|
|
734
|
+
return { pid: data.pid, port: data.port, startedAt: data.startedAt ?? 0 };
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
} catch {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function writePidFile(file, data) {
|
|
742
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
743
|
+
fs5.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
744
|
+
}
|
|
745
|
+
function removePidFile(file) {
|
|
746
|
+
try {
|
|
747
|
+
fs5.unlinkSync(file);
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function isProcessAlive(pid) {
|
|
752
|
+
try {
|
|
753
|
+
process.kill(pid, 0);
|
|
754
|
+
return true;
|
|
755
|
+
} catch {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// packages/cli/src/daemon.ts
|
|
761
|
+
function sleep2(ms) {
|
|
762
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
763
|
+
}
|
|
764
|
+
function loadPaths() {
|
|
765
|
+
const paths = resolvePaths();
|
|
766
|
+
ensureDirs(paths);
|
|
767
|
+
return paths;
|
|
768
|
+
}
|
|
769
|
+
async function cmdStartForeground(port, openAfter = false) {
|
|
770
|
+
const cfg = loadConfig();
|
|
771
|
+
const resolvedPort = port ?? cfg.port;
|
|
772
|
+
try {
|
|
773
|
+
const instance = await startAntbotServer({ port: resolvedPort, host: cfg.host });
|
|
774
|
+
console.log(green(`antbot listening on http://${cfg.host}:${resolvedPort}`));
|
|
775
|
+
if (openAfter) openInBrowser(`http://127.0.0.1:${resolvedPort}`);
|
|
776
|
+
let shuttingDown = false;
|
|
777
|
+
const shutdown = async (signal) => {
|
|
778
|
+
if (shuttingDown) return;
|
|
779
|
+
shuttingDown = true;
|
|
780
|
+
console.log(dim(`Received ${signal}, shutting down...`));
|
|
781
|
+
try {
|
|
782
|
+
await instance.close?.();
|
|
783
|
+
} finally {
|
|
784
|
+
process.exit(0);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
788
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
789
|
+
return 0;
|
|
790
|
+
} catch (err) {
|
|
791
|
+
console.error(red(`Failed to start antbot: ${err.message}`));
|
|
792
|
+
return 1;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async function cmdStartDetached(port, openAfter = false) {
|
|
796
|
+
const cfg = loadConfig();
|
|
797
|
+
const paths = loadPaths();
|
|
798
|
+
const resolvedPort = port ?? cfg.port ?? DEFAULT_PORT;
|
|
799
|
+
const pidPath = pidFilePath(paths.root);
|
|
800
|
+
const existing = readPidFile(pidPath);
|
|
801
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
802
|
+
console.log(`antbot is already running at http://127.0.0.1:${existing.port} (pid ${existing.pid}).`);
|
|
803
|
+
if (openAfter) openInBrowser(`http://127.0.0.1:${existing.port}`);
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
if (existing) removePidFile(pidPath);
|
|
807
|
+
const logPath = path6.join(paths.logs, "antbot.log");
|
|
808
|
+
fs6.mkdirSync(paths.logs, { recursive: true });
|
|
809
|
+
const logFd = fs6.openSync(logPath, "a");
|
|
810
|
+
const selfEntry = process.argv[1];
|
|
811
|
+
if (!selfEntry) {
|
|
812
|
+
console.error(red("Could not determine the antbot entry point to spawn."));
|
|
813
|
+
return 1;
|
|
814
|
+
}
|
|
815
|
+
const child = spawn2(
|
|
816
|
+
process.execPath,
|
|
817
|
+
[selfEntry, "start", "--foreground", "--port", String(resolvedPort)],
|
|
818
|
+
{ detached: true, stdio: ["ignore", logFd, logFd] }
|
|
819
|
+
);
|
|
820
|
+
child.unref();
|
|
821
|
+
if (!child.pid) {
|
|
822
|
+
console.error(red("Failed to spawn the antbot daemon."));
|
|
823
|
+
return 1;
|
|
824
|
+
}
|
|
825
|
+
writePidFile(pidPath, { pid: child.pid, port: resolvedPort, startedAt: Date.now() });
|
|
826
|
+
console.log(`Starting antbot (pid ${child.pid}) on port ${resolvedPort}...`);
|
|
827
|
+
const ok = await waitForHealth(resolvedPort, 3e4);
|
|
828
|
+
if (!ok) {
|
|
829
|
+
console.error(red(`Timed out waiting for antbot to become healthy. Check ${logPath}`));
|
|
830
|
+
return 1;
|
|
831
|
+
}
|
|
832
|
+
console.log(green(`antbot is running at http://127.0.0.1:${resolvedPort}`));
|
|
833
|
+
if (openAfter) openInBrowser(`http://127.0.0.1:${resolvedPort}`);
|
|
834
|
+
return 0;
|
|
835
|
+
}
|
|
836
|
+
async function cmdStop() {
|
|
837
|
+
const paths = loadPaths();
|
|
838
|
+
const pidPath = pidFilePath(paths.root);
|
|
839
|
+
const data = readPidFile(pidPath);
|
|
840
|
+
if (!data || !isProcessAlive(data.pid)) {
|
|
841
|
+
console.log("antbot is not running.");
|
|
842
|
+
if (data) removePidFile(pidPath);
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
console.log(`Stopping antbot (pid ${data.pid})...`);
|
|
846
|
+
try {
|
|
847
|
+
process.kill(data.pid, "SIGTERM");
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
const deadline = Date.now() + 1e4;
|
|
851
|
+
while (Date.now() < deadline && isProcessAlive(data.pid)) {
|
|
852
|
+
await sleep2(200);
|
|
853
|
+
}
|
|
854
|
+
if (isProcessAlive(data.pid)) {
|
|
855
|
+
console.log(yellow("Process did not exit in time, sending SIGKILL."));
|
|
856
|
+
try {
|
|
857
|
+
process.kill(data.pid, "SIGKILL");
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
removePidFile(pidPath);
|
|
862
|
+
console.log(green("antbot stopped."));
|
|
863
|
+
return 0;
|
|
864
|
+
}
|
|
865
|
+
async function cmdStatus() {
|
|
866
|
+
const paths = loadPaths();
|
|
867
|
+
const pidPath = pidFilePath(paths.root);
|
|
868
|
+
const data = readPidFile(pidPath);
|
|
869
|
+
const alive = !!data && isProcessAlive(data.pid);
|
|
870
|
+
if (!alive || !data) {
|
|
871
|
+
const cfgPort = loadConfig().port ?? DEFAULT_PORT;
|
|
872
|
+
const probe = await getHealth(cfgPort);
|
|
873
|
+
if (!probe.ok) {
|
|
874
|
+
console.log(red("antbot is not running."));
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
const count = await getBotCount(cfgPort);
|
|
878
|
+
console.log(green("antbot is running (started outside the CLI \u2014 no pidfile)"));
|
|
879
|
+
console.log(` URL: http://127.0.0.1:${cfgPort}`);
|
|
880
|
+
console.log(` Version: ${probe.version ?? "unknown"}`);
|
|
881
|
+
console.log(` Data dir: ${paths.root}`);
|
|
882
|
+
console.log(` Bots: ${count ?? "unknown"}`);
|
|
883
|
+
console.log(yellow(" Note: `antbot stop` cannot stop it; stop it where it was started."));
|
|
884
|
+
return 0;
|
|
885
|
+
}
|
|
886
|
+
const health = await getHealth(data.port);
|
|
887
|
+
const botCount = health.ok ? await getBotCount(data.port) : null;
|
|
888
|
+
console.log(green(`antbot is running (pid ${data.pid})`));
|
|
889
|
+
console.log(` URL: http://127.0.0.1:${data.port}`);
|
|
890
|
+
console.log(` Version: ${health.version ?? "unknown"}`);
|
|
891
|
+
console.log(` Data dir: ${paths.root}`);
|
|
892
|
+
console.log(` Bots: ${botCount ?? "unknown"}`);
|
|
893
|
+
if (!health.ok) {
|
|
894
|
+
console.log(yellow(" Warning: process is alive but /api/health is not responding."));
|
|
895
|
+
}
|
|
896
|
+
return health.ok ? 0 : 1;
|
|
897
|
+
}
|
|
898
|
+
function openInBrowser(url) {
|
|
899
|
+
console.log(url);
|
|
900
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
901
|
+
try {
|
|
902
|
+
const child = spawn2(opener, [url], {
|
|
903
|
+
stdio: "ignore",
|
|
904
|
+
detached: true,
|
|
905
|
+
shell: process.platform === "win32"
|
|
906
|
+
});
|
|
907
|
+
child.on("error", () => {
|
|
908
|
+
console.log(dim(`Could not launch a browser automatically. Open ${url} manually.`));
|
|
909
|
+
});
|
|
910
|
+
child.unref();
|
|
911
|
+
} catch {
|
|
912
|
+
console.log(dim(`Could not launch a browser automatically. Open ${url} manually.`));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async function cmdRestart(port) {
|
|
916
|
+
const stopCode = await cmdStop();
|
|
917
|
+
if (stopCode !== 0) return stopCode;
|
|
918
|
+
return cmdStartDetached(port);
|
|
919
|
+
}
|
|
920
|
+
async function cmdOpen() {
|
|
921
|
+
const paths = loadPaths();
|
|
922
|
+
const cfg = loadConfig();
|
|
923
|
+
const pidPath = pidFilePath(paths.root);
|
|
924
|
+
const data = readPidFile(pidPath);
|
|
925
|
+
const port = data?.port ?? cfg.port ?? DEFAULT_PORT;
|
|
926
|
+
const health = await getHealth(port);
|
|
927
|
+
if (!health.ok) {
|
|
928
|
+
console.log(dim("antbot is not running \u2014 starting it first..."));
|
|
929
|
+
const code = await cmdStartDetached(port);
|
|
930
|
+
if (code !== 0) return code;
|
|
931
|
+
}
|
|
932
|
+
openInBrowser(`http://127.0.0.1:${port}`);
|
|
933
|
+
return 0;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// packages/cli/src/backup.ts
|
|
937
|
+
import fs7 from "node:fs";
|
|
938
|
+
import path7 from "node:path";
|
|
939
|
+
function computeBackupItems(input) {
|
|
940
|
+
const items = [
|
|
941
|
+
{ source: input.dbPath, archivePath: "antbot.db" },
|
|
942
|
+
{ source: input.configPath, archivePath: "config.toml" },
|
|
943
|
+
{ source: input.skillsDir, archivePath: "skills" }
|
|
944
|
+
];
|
|
945
|
+
for (const slug of input.botSlugs) {
|
|
946
|
+
items.push({
|
|
947
|
+
source: path7.join(input.botsDir, slug, "memory"),
|
|
948
|
+
archivePath: `workspace/bots/${slug}/memory`
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return items;
|
|
952
|
+
}
|
|
953
|
+
function listBotSlugs(botsDir) {
|
|
954
|
+
try {
|
|
955
|
+
return fs7.readdirSync(botsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
956
|
+
} catch {
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function createBackup(opts) {
|
|
961
|
+
const botSlugs = listBotSlugs(opts.botsDir);
|
|
962
|
+
const allItems = computeBackupItems({
|
|
963
|
+
dbPath: opts.dbPath,
|
|
964
|
+
configPath: opts.configPath,
|
|
965
|
+
skillsDir: opts.skillsDir,
|
|
966
|
+
botsDir: opts.botsDir,
|
|
967
|
+
botSlugs
|
|
968
|
+
});
|
|
969
|
+
const items = allItems.filter((item) => fs7.existsSync(item.source));
|
|
970
|
+
if (items.length === 0) {
|
|
971
|
+
throw new Error("Nothing to back up: no antbot.db, config.toml, skills, or bot memory found.");
|
|
972
|
+
}
|
|
973
|
+
fs7.mkdirSync(path7.dirname(opts.outPath), { recursive: true });
|
|
974
|
+
const relPaths = items.map((item) => path7.relative(opts.root, item.source));
|
|
975
|
+
const args = ["czf", opts.outPath, "-C", opts.root, ...relPaths];
|
|
976
|
+
let res;
|
|
977
|
+
try {
|
|
978
|
+
res = await runCommand("tar", args);
|
|
979
|
+
} catch (err) {
|
|
980
|
+
throw new Error(
|
|
981
|
+
`Could not run "tar": ${err.message}. Is tar installed and on PATH?`,
|
|
982
|
+
{ cause: err }
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (res.code !== 0) {
|
|
986
|
+
throw new Error(`tar exited with code ${res.code}: ${res.stderr.trim()}`);
|
|
987
|
+
}
|
|
988
|
+
const { size } = fs7.statSync(opts.outPath);
|
|
989
|
+
return { outPath: opts.outPath, bytes: size, items };
|
|
990
|
+
}
|
|
991
|
+
async function restoreBackup(opts) {
|
|
992
|
+
if (!fs7.existsSync(opts.archivePath)) {
|
|
993
|
+
throw new Error(`Backup file not found: ${opts.archivePath}`);
|
|
994
|
+
}
|
|
995
|
+
fs7.mkdirSync(opts.root, { recursive: true });
|
|
996
|
+
let res;
|
|
997
|
+
try {
|
|
998
|
+
res = await runCommand("tar", ["xzf", opts.archivePath, "-C", opts.root]);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Could not run "tar": ${err.message}. Is tar installed and on PATH?`,
|
|
1002
|
+
{ cause: err }
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
if (res.code !== 0) {
|
|
1006
|
+
throw new Error(`tar exited with code ${res.code}: ${res.stderr.trim()}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// packages/cli/src/skill.ts
|
|
1011
|
+
import fs8 from "node:fs";
|
|
1012
|
+
import path8 from "node:path";
|
|
1013
|
+
var USAGE = `Usage:
|
|
1014
|
+
antbot skill list
|
|
1015
|
+
antbot skill add <source>
|
|
1016
|
+
antbot skill remove <slug>
|
|
1017
|
+
antbot skill lint [path]`;
|
|
1018
|
+
async function runSkillCommand(argv, port) {
|
|
1019
|
+
const [sub, ...rest] = argv;
|
|
1020
|
+
if (!sub || sub === "list") return listSkills(port);
|
|
1021
|
+
if (sub === "add") return addSkill(rest, port);
|
|
1022
|
+
if (sub === "remove" || sub === "rm") return removeSkill(rest, port);
|
|
1023
|
+
if (sub === "lint" || sub === "check") return lintSkills(rest);
|
|
1024
|
+
console.error(red(`Unknown subcommand "${sub}".
|
|
1025
|
+
${USAGE}`));
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
function notRunning(err) {
|
|
1029
|
+
console.error(red(`Could not reach the antbot daemon: ${err.message}`));
|
|
1030
|
+
console.error(dim("Start it with `antbot start`."));
|
|
1031
|
+
return 1;
|
|
1032
|
+
}
|
|
1033
|
+
async function listSkills(port) {
|
|
1034
|
+
let skills;
|
|
1035
|
+
try {
|
|
1036
|
+
skills = await getJson(port, "/api/skills");
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
return notRunning(err);
|
|
1039
|
+
}
|
|
1040
|
+
if (!skills.length) {
|
|
1041
|
+
console.log("No skills installed. Add one with `antbot skill add <source>`.");
|
|
1042
|
+
return 0;
|
|
1043
|
+
}
|
|
1044
|
+
const width = Math.max(...skills.map((s) => s.slug.length));
|
|
1045
|
+
for (const s of skills) {
|
|
1046
|
+
console.log(`${bold(s.slug.padEnd(width))} ${s.description || s.name}`);
|
|
1047
|
+
}
|
|
1048
|
+
console.log("");
|
|
1049
|
+
console.log(dim(`${skills.length} skill(s). Assign them to a bot in Bot settings \u2192 Skills.`));
|
|
1050
|
+
return 0;
|
|
1051
|
+
}
|
|
1052
|
+
async function addSkill(args, port) {
|
|
1053
|
+
const source = args[0];
|
|
1054
|
+
if (!source) {
|
|
1055
|
+
console.error(red(`A source is required.
|
|
1056
|
+
${USAGE}`));
|
|
1057
|
+
return 2;
|
|
1058
|
+
}
|
|
1059
|
+
let res;
|
|
1060
|
+
try {
|
|
1061
|
+
res = await postJson(port, "/api/skills/install", { source });
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
const message = err.message;
|
|
1064
|
+
if (/ECONNREFUSED|not reach/i.test(message)) return notRunning(err);
|
|
1065
|
+
console.error(red(message));
|
|
1066
|
+
return 1;
|
|
1067
|
+
}
|
|
1068
|
+
for (const item of res.installed) {
|
|
1069
|
+
console.log(item.manifest.replace(/^✓ Installed/, green("\u2713 Installed")));
|
|
1070
|
+
if (item.replaced) console.log(dim(" (upgraded in place; existing bot assignments kept)"));
|
|
1071
|
+
console.log("");
|
|
1072
|
+
}
|
|
1073
|
+
const names = res.installed.map((i) => i.skill.slug).join(", ");
|
|
1074
|
+
console.log(dim(`Assign to a bot in Bot settings \u2192 Skills: ${names}`));
|
|
1075
|
+
return 0;
|
|
1076
|
+
}
|
|
1077
|
+
async function removeSkill(args, port) {
|
|
1078
|
+
const slug = args[0];
|
|
1079
|
+
if (!slug) {
|
|
1080
|
+
console.error(red(`A slug is required.
|
|
1081
|
+
${USAGE}`));
|
|
1082
|
+
return 2;
|
|
1083
|
+
}
|
|
1084
|
+
let skills;
|
|
1085
|
+
try {
|
|
1086
|
+
skills = await getJson(port, "/api/skills");
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
return notRunning(err);
|
|
1089
|
+
}
|
|
1090
|
+
const match = skills.find((s) => s.slug === slug || s.name === slug);
|
|
1091
|
+
if (!match) {
|
|
1092
|
+
console.error(red(`No skill named "${slug}".`));
|
|
1093
|
+
console.error(dim(`Installed: ${skills.map((s) => s.slug).join(", ") || "(none)"}`));
|
|
1094
|
+
return 1;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
await deleteJson(port, `/api/skills/${match.id}`);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
return notRunning(err);
|
|
1100
|
+
}
|
|
1101
|
+
console.log(green(`\u2713 Removed '${match.slug}'`));
|
|
1102
|
+
console.log(dim(yellow("Bots that had it assigned no longer have it.")));
|
|
1103
|
+
return 0;
|
|
1104
|
+
}
|
|
1105
|
+
async function lintSkills(args) {
|
|
1106
|
+
const target = args[0];
|
|
1107
|
+
let spec;
|
|
1108
|
+
try {
|
|
1109
|
+
spec = await importSkillSpec();
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
console.error(red(`Could not load the skill validator: ${err.message}`));
|
|
1112
|
+
return 1;
|
|
1113
|
+
}
|
|
1114
|
+
let results;
|
|
1115
|
+
let scanned;
|
|
1116
|
+
if (target) {
|
|
1117
|
+
const dir = path8.resolve(target);
|
|
1118
|
+
if (!fs8.existsSync(dir)) {
|
|
1119
|
+
console.error(red(`No such directory: ${dir}`));
|
|
1120
|
+
return 2;
|
|
1121
|
+
}
|
|
1122
|
+
scanned = dir;
|
|
1123
|
+
results = fs8.existsSync(path8.join(dir, "SKILL.md")) ? [{ slug: path8.basename(dir), violations: spec.validateSkillDir(dir) }] : spec.validateSkillsIn(dir);
|
|
1124
|
+
} else {
|
|
1125
|
+
scanned = path8.join(loadPaths().skills, "skills");
|
|
1126
|
+
results = spec.validateSkillsIn(scanned);
|
|
1127
|
+
}
|
|
1128
|
+
if (results.length === 0) {
|
|
1129
|
+
console.log(`No skills found in ${scanned}.`);
|
|
1130
|
+
return 0;
|
|
1131
|
+
}
|
|
1132
|
+
let errors = 0;
|
|
1133
|
+
let warnings = 0;
|
|
1134
|
+
const width = Math.max(...results.map((r) => r.slug.length));
|
|
1135
|
+
for (const { slug, violations } of results) {
|
|
1136
|
+
if (violations.length === 0) {
|
|
1137
|
+
console.log(`${green("\u2713")} ${bold(slug.padEnd(width))} ${dim("conforms to the spec")}`);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const worst = violations.some((v) => v.level === "error") ? red("\u2717") : yellow("\u26A0");
|
|
1141
|
+
console.log(`${worst} ${bold(slug.padEnd(width))}`);
|
|
1142
|
+
for (const v of violations) {
|
|
1143
|
+
if (v.level === "error") errors++;
|
|
1144
|
+
else warnings++;
|
|
1145
|
+
const tag = v.level === "error" ? red("error") : yellow("warn ");
|
|
1146
|
+
console.log(` ${tag} ${dim(v.code)} ${v.message}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
console.log("");
|
|
1150
|
+
if (errors > 0) {
|
|
1151
|
+
console.log(red(`${errors} error(s), ${warnings} warning(s) across ${results.length} skill(s).`));
|
|
1152
|
+
console.log(dim("See skills/SPEC.md, or the skill-author skill, for how to fix each one."));
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
if (warnings > 0) {
|
|
1156
|
+
console.log(yellow(`${warnings} warning(s) across ${results.length} skill(s).`));
|
|
1157
|
+
return 0;
|
|
1158
|
+
}
|
|
1159
|
+
console.log(green(`All ${results.length} skill(s) conform to the spec.`));
|
|
1160
|
+
return 0;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// packages/cli/src/updateCommand.ts
|
|
1164
|
+
import fs9 from "node:fs";
|
|
1165
|
+
import path10 from "node:path";
|
|
1166
|
+
import readline from "node:readline/promises";
|
|
1167
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1168
|
+
|
|
1169
|
+
// packages/cli/src/update.ts
|
|
1170
|
+
import path9 from "node:path";
|
|
1171
|
+
var PACKAGE_NAME = "@michael-joseph-miller/ant-bot";
|
|
1172
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
1173
|
+
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1174
|
+
function parse(version) {
|
|
1175
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim());
|
|
1176
|
+
if (!m) return null;
|
|
1177
|
+
return { parts: [Number(m[1]), Number(m[2]), Number(m[3])], prerelease: m[4] ?? null };
|
|
1178
|
+
}
|
|
1179
|
+
function compareVersions(a, b) {
|
|
1180
|
+
const pa = parse(a);
|
|
1181
|
+
const pb = parse(b);
|
|
1182
|
+
if (!pa || !pb) return 0;
|
|
1183
|
+
for (let i = 0; i < 3; i++) {
|
|
1184
|
+
if (pa.parts[i] !== pb.parts[i]) return pa.parts[i] - pb.parts[i];
|
|
1185
|
+
}
|
|
1186
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
1187
|
+
if (pa.prerelease === null) return 1;
|
|
1188
|
+
if (pb.prerelease === null) return -1;
|
|
1189
|
+
return pa.prerelease < pb.prerelease ? -1 : 1;
|
|
1190
|
+
}
|
|
1191
|
+
function isUpgrade(current, latest) {
|
|
1192
|
+
const c = parse(current);
|
|
1193
|
+
const l = parse(latest);
|
|
1194
|
+
if (!c || !l) return false;
|
|
1195
|
+
if (l.prerelease !== null && c.prerelease === null) return false;
|
|
1196
|
+
return compareVersions(latest, current) > 0;
|
|
1197
|
+
}
|
|
1198
|
+
function planUpdateCheck(cache, now, ttlMs = UPDATE_CHECK_TTL_MS) {
|
|
1199
|
+
if (!cache || typeof cache.checkedAt !== "number" || typeof cache.latest !== "string") return "fetch";
|
|
1200
|
+
if (cache.checkedAt > now) return "fetch";
|
|
1201
|
+
return now - cache.checkedAt < ttlMs ? "use-cache" : "fetch";
|
|
1202
|
+
}
|
|
1203
|
+
function detectPackageManager(installPath2) {
|
|
1204
|
+
const p = installPath2.split(path9.sep).join("/");
|
|
1205
|
+
if (/\/\.bun\//.test(p)) return "bun";
|
|
1206
|
+
if (/\/\.pnpm\//.test(p) || /\/pnpm\/(global|store)\//.test(p) || /\/Library\/pnpm\//.test(p)) return "pnpm";
|
|
1207
|
+
if (/\/\.yarn\//.test(p) || /\/yarn\/global\//.test(p) || /\/\.config\/yarn\//.test(p)) return "yarn";
|
|
1208
|
+
return "npm";
|
|
1209
|
+
}
|
|
1210
|
+
function updateCommand(pm, pkg = PACKAGE_NAME) {
|
|
1211
|
+
switch (pm) {
|
|
1212
|
+
case "pnpm":
|
|
1213
|
+
return { cmd: "pnpm", args: ["add", "-g", `${pkg}@latest`] };
|
|
1214
|
+
case "yarn":
|
|
1215
|
+
return { cmd: "yarn", args: ["global", "add", `${pkg}@latest`] };
|
|
1216
|
+
case "bun":
|
|
1217
|
+
return { cmd: "bun", args: ["add", "-g", `${pkg}@latest`] };
|
|
1218
|
+
case "npm":
|
|
1219
|
+
default:
|
|
1220
|
+
return { cmd: "npm", args: ["install", "-g", `${pkg}@latest`] };
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function isCheckoutInstall(installPath2, exists) {
|
|
1224
|
+
if (installPath2.split(path9.sep).includes("node_modules")) return false;
|
|
1225
|
+
let dir = path9.resolve(installPath2);
|
|
1226
|
+
for (; ; ) {
|
|
1227
|
+
if (exists(path9.join(dir, ".git"))) return true;
|
|
1228
|
+
const parent = path9.dirname(dir);
|
|
1229
|
+
if (parent === dir) return false;
|
|
1230
|
+
dir = parent;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function updateNotice(current, latest) {
|
|
1234
|
+
if (!latest || !isUpgrade(current, latest)) return null;
|
|
1235
|
+
return `A newer ant-bot is available: ${current} \u2192 ${latest}. Run \`antbot update\`.`;
|
|
1236
|
+
}
|
|
1237
|
+
async function checkForUpdate(deps) {
|
|
1238
|
+
let cache = null;
|
|
1239
|
+
try {
|
|
1240
|
+
const raw = deps.readCache();
|
|
1241
|
+
if (raw) cache = JSON.parse(raw);
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
let latest;
|
|
1245
|
+
let cached = false;
|
|
1246
|
+
if (planUpdateCheck(cache, deps.now, deps.ttlMs) === "use-cache") {
|
|
1247
|
+
latest = cache.latest;
|
|
1248
|
+
cached = true;
|
|
1249
|
+
} else {
|
|
1250
|
+
try {
|
|
1251
|
+
latest = await deps.fetchLatest();
|
|
1252
|
+
if (latest) {
|
|
1253
|
+
deps.writeCache(`${JSON.stringify({ latest, checkedAt: deps.now })}
|
|
1254
|
+
`);
|
|
1255
|
+
}
|
|
1256
|
+
} catch {
|
|
1257
|
+
latest = cache?.latest ?? null;
|
|
1258
|
+
cached = latest !== null;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
current: deps.currentVersion,
|
|
1263
|
+
latest,
|
|
1264
|
+
isUpgrade: latest !== null && isUpgrade(deps.currentVersion, latest),
|
|
1265
|
+
cached,
|
|
1266
|
+
notice: updateNotice(deps.currentVersion, latest)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
async function fetchLatestVersion(timeoutMs = 3e3) {
|
|
1270
|
+
const ac = new AbortController();
|
|
1271
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
1272
|
+
try {
|
|
1273
|
+
const res = await fetch(REGISTRY_URL, {
|
|
1274
|
+
signal: ac.signal,
|
|
1275
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
1276
|
+
});
|
|
1277
|
+
if (!res.ok) return null;
|
|
1278
|
+
const body = await res.json();
|
|
1279
|
+
return typeof body.version === "string" ? body.version : null;
|
|
1280
|
+
} catch {
|
|
1281
|
+
return null;
|
|
1282
|
+
} finally {
|
|
1283
|
+
clearTimeout(timer);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// packages/cli/src/updateCommand.ts
|
|
1288
|
+
var CACHE_FILE = "update-check.json";
|
|
1289
|
+
function cachePath() {
|
|
1290
|
+
return path10.join(loadPaths().root, CACHE_FILE);
|
|
1291
|
+
}
|
|
1292
|
+
function installPath() {
|
|
1293
|
+
return path10.dirname(fileURLToPath3(import.meta.url));
|
|
1294
|
+
}
|
|
1295
|
+
async function currentUpdateStatus() {
|
|
1296
|
+
const file = cachePath();
|
|
1297
|
+
return checkForUpdate({
|
|
1298
|
+
currentVersion: getCliVersion(),
|
|
1299
|
+
now: Date.now(),
|
|
1300
|
+
readCache: () => {
|
|
1301
|
+
try {
|
|
1302
|
+
return fs9.readFileSync(file, "utf8");
|
|
1303
|
+
} catch {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
},
|
|
1307
|
+
writeCache: (contents) => {
|
|
1308
|
+
try {
|
|
1309
|
+
fs9.mkdirSync(path10.dirname(file), { recursive: true });
|
|
1310
|
+
fs9.writeFileSync(file, contents, "utf8");
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
fetchLatest: () => fetchLatestVersion()
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
async function printUpdateNotice() {
|
|
1318
|
+
if (isCheckoutInstall(installPath(), (p) => fs9.existsSync(p))) return;
|
|
1319
|
+
const status = await currentUpdateStatus();
|
|
1320
|
+
if (status.notice) console.log(yellow(` ${status.notice}`));
|
|
1321
|
+
}
|
|
1322
|
+
async function runUpdateCommand(opts) {
|
|
1323
|
+
const here = installPath();
|
|
1324
|
+
const current = getCliVersion();
|
|
1325
|
+
if (isCheckoutInstall(here, (p) => fs9.existsSync(p))) {
|
|
1326
|
+
console.error(red("This is a git checkout, not a package-manager install."));
|
|
1327
|
+
console.error(dim("Update it with: git pull && pnpm install"));
|
|
1328
|
+
return 2;
|
|
1329
|
+
}
|
|
1330
|
+
const status = await currentUpdateStatus();
|
|
1331
|
+
if (status.latest === null) {
|
|
1332
|
+
console.error(red("Could not reach the npm registry to check for updates."));
|
|
1333
|
+
return 1;
|
|
1334
|
+
}
|
|
1335
|
+
if (!status.isUpgrade) {
|
|
1336
|
+
console.log(green(`ant-bot ${current} is up to date.`) + (status.cached ? dim(" (cached)") : ""));
|
|
1337
|
+
return 0;
|
|
1338
|
+
}
|
|
1339
|
+
console.log(bold(`ant-bot ${current} \u2192 ${status.latest}`));
|
|
1340
|
+
if (opts.check) return 0;
|
|
1341
|
+
const pm = detectPackageManager(here);
|
|
1342
|
+
const { cmd, args } = updateCommand(pm);
|
|
1343
|
+
if (!opts.yes) {
|
|
1344
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1345
|
+
const answer = await rl.question(`Run \`${cmd} ${args.join(" ")}\`? [y/N] `);
|
|
1346
|
+
rl.close();
|
|
1347
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
1348
|
+
console.log("Aborted.");
|
|
1349
|
+
return 0;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
console.log(dim(`$ ${cmd} ${args.join(" ")}`));
|
|
1353
|
+
const result = await runCommand(cmd, args);
|
|
1354
|
+
if (result.code !== 0) {
|
|
1355
|
+
console.error(red(`${cmd} exited with code ${result.code}`));
|
|
1356
|
+
if (result.stderr) console.error(dim(result.stderr.trim()));
|
|
1357
|
+
return 1;
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
fs9.rmSync(cachePath(), { force: true });
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
console.log(green(`Updated to ${status.latest}.`));
|
|
1364
|
+
console.log(dim("Restart a running daemon to pick it up: antbot restart"));
|
|
1365
|
+
return 0;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// packages/cli/src/index.ts
|
|
1369
|
+
async function runDoctorCommand() {
|
|
1370
|
+
const paths = loadPaths();
|
|
1371
|
+
const port = 4780;
|
|
1372
|
+
const report = await runDoctor({
|
|
1373
|
+
nodeVersion: process.versions.node,
|
|
1374
|
+
env: process.env,
|
|
1375
|
+
homeDir: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
1376
|
+
dataDir: paths.root,
|
|
1377
|
+
port,
|
|
1378
|
+
fileExists: (p) => fs10.existsSync(p),
|
|
1379
|
+
fileSize: (p) => {
|
|
1380
|
+
try {
|
|
1381
|
+
return fs10.statSync(p).size;
|
|
1382
|
+
} catch {
|
|
1383
|
+
return 0;
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
isDirWritable: (dir) => {
|
|
1387
|
+
try {
|
|
1388
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
1389
|
+
fs10.accessSync(dir, fs10.constants.W_OK);
|
|
1390
|
+
return true;
|
|
1391
|
+
} catch {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
exec: (cmd, args) => runCommand(cmd, args),
|
|
1396
|
+
probePort: (p) => probePort(p),
|
|
1397
|
+
healthCheck: (p) => getHealth(p),
|
|
1398
|
+
importBetterSqlite3: () => importServerDependency("better-sqlite3"),
|
|
1399
|
+
importPlaywright: async () => {
|
|
1400
|
+
const mod = await importServerDependency("playwright");
|
|
1401
|
+
const pw = mod.default ?? mod;
|
|
1402
|
+
if (!pw.chromium) throw new Error("playwright module has no chromium export");
|
|
1403
|
+
return { chromium: pw.chromium };
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
renderDoctorTable(report.results);
|
|
1407
|
+
return report.exitCode;
|
|
1408
|
+
}
|
|
1409
|
+
function symbolFor(status) {
|
|
1410
|
+
if (status === "pass") return green("\u2713");
|
|
1411
|
+
if (status === "warn") return yellow("\u26A0");
|
|
1412
|
+
return red("\u2717");
|
|
1413
|
+
}
|
|
1414
|
+
function renderDoctorTable(results) {
|
|
1415
|
+
const nameWidth = Math.max(...results.map((r) => r.name.length));
|
|
1416
|
+
for (const r of results) {
|
|
1417
|
+
console.log(`${symbolFor(r.status)} ${bold(r.name.padEnd(nameWidth))} ${r.message}`);
|
|
1418
|
+
if (r.hint && r.status !== "pass") {
|
|
1419
|
+
console.log(` ${dim(`-> ${r.hint}`)}`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
1423
|
+
const warned = results.filter((r) => r.status === "warn").length;
|
|
1424
|
+
console.log("");
|
|
1425
|
+
if (failed > 0) {
|
|
1426
|
+
console.log(red(`${failed} check(s) failed, ${warned} warning(s).`));
|
|
1427
|
+
} else if (warned > 0) {
|
|
1428
|
+
console.log(yellow(`All checks passed with ${warned} warning(s).`));
|
|
1429
|
+
} else {
|
|
1430
|
+
console.log(green("All checks passed."));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async function runBackupCommand(outFlag) {
|
|
1434
|
+
const paths = loadPaths();
|
|
1435
|
+
const outPath = outFlag ?? path11.join(paths.backups, `antbot-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.tar.gz`);
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await createBackup({
|
|
1438
|
+
root: paths.root,
|
|
1439
|
+
dbPath: paths.db,
|
|
1440
|
+
configPath: paths.config,
|
|
1441
|
+
skillsDir: paths.skills,
|
|
1442
|
+
botsDir: path11.join(paths.workspace, "bots"),
|
|
1443
|
+
outPath
|
|
1444
|
+
});
|
|
1445
|
+
const kb = (result.bytes / 1024).toFixed(1);
|
|
1446
|
+
console.log(green(`Backup written to ${result.outPath} (${kb} KB)`));
|
|
1447
|
+
return 0;
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
console.error(red(err.message));
|
|
1450
|
+
return 1;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
async function runRestoreCommand(archivePath, skipConfirm) {
|
|
1454
|
+
if (!archivePath) {
|
|
1455
|
+
console.error(red("Usage: antbot restore <path> [--yes]"));
|
|
1456
|
+
return 2;
|
|
1457
|
+
}
|
|
1458
|
+
const paths = loadPaths();
|
|
1459
|
+
if (!skipConfirm) {
|
|
1460
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1461
|
+
const answer = await rl.question(
|
|
1462
|
+
`This will overwrite files under ${paths.root} with the contents of ${archivePath}. Continue? [y/N] `
|
|
1463
|
+
);
|
|
1464
|
+
rl.close();
|
|
1465
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
1466
|
+
console.log("Aborted.");
|
|
1467
|
+
return 0;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
await restoreBackup({ archivePath, root: paths.root });
|
|
1472
|
+
console.log(green(`Restored ${archivePath} into ${paths.root}`));
|
|
1473
|
+
return 0;
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error(red(err.message));
|
|
1476
|
+
return 1;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function main() {
|
|
1480
|
+
let parsed;
|
|
1481
|
+
try {
|
|
1482
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
if (err instanceof CliError) {
|
|
1485
|
+
console.error(red(err.message));
|
|
1486
|
+
process.exit(2);
|
|
1487
|
+
}
|
|
1488
|
+
throw err;
|
|
1489
|
+
}
|
|
1490
|
+
if (parsed.version) {
|
|
1491
|
+
console.log(getCliVersion());
|
|
1492
|
+
process.exit(0);
|
|
1493
|
+
}
|
|
1494
|
+
if (parsed.help || !parsed.command) {
|
|
1495
|
+
console.log(parsed.command ? commandHelp(parsed.command) : GLOBAL_HELP);
|
|
1496
|
+
process.exit(0);
|
|
1497
|
+
}
|
|
1498
|
+
const command = parsed.command;
|
|
1499
|
+
switch (command) {
|
|
1500
|
+
case "start": {
|
|
1501
|
+
const port = typeof parsed.flags.port === "number" ? parsed.flags.port : void 0;
|
|
1502
|
+
const foreground = Boolean(parsed.flags.foreground);
|
|
1503
|
+
const openAfter = Boolean(parsed.flags.open);
|
|
1504
|
+
if (foreground) {
|
|
1505
|
+
const code = await cmdStartForeground(port, openAfter);
|
|
1506
|
+
if (code !== 0) process.exit(code);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
process.exit(await cmdStartDetached(port, openAfter));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
case "stop":
|
|
1513
|
+
process.exit(await cmdStop());
|
|
1514
|
+
return;
|
|
1515
|
+
case "restart":
|
|
1516
|
+
process.exit(await cmdRestart(typeof parsed.flags.port === "number" ? parsed.flags.port : void 0));
|
|
1517
|
+
return;
|
|
1518
|
+
// The update notice is printed here rather than inside the commands themselves: it is the
|
|
1519
|
+
// only thing in the CLI that touches the network, and keeping it at the dispatch layer means
|
|
1520
|
+
// neither cmdStatus nor runDoctor grows a dependency on it.
|
|
1521
|
+
case "status": {
|
|
1522
|
+
const code = await cmdStatus();
|
|
1523
|
+
await printUpdateNotice();
|
|
1524
|
+
process.exit(code);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
case "doctor": {
|
|
1528
|
+
const code = await runDoctorCommand();
|
|
1529
|
+
await printUpdateNotice();
|
|
1530
|
+
process.exit(code);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
case "open":
|
|
1534
|
+
process.exit(await cmdOpen());
|
|
1535
|
+
return;
|
|
1536
|
+
case "skill": {
|
|
1537
|
+
const cfg = loadConfig();
|
|
1538
|
+
process.exit(await runSkillCommand(parsed.positionals, cfg.port));
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
case "backup":
|
|
1542
|
+
process.exit(await runBackupCommand(typeof parsed.flags.out === "string" ? parsed.flags.out : void 0));
|
|
1543
|
+
return;
|
|
1544
|
+
case "restore":
|
|
1545
|
+
process.exit(await runRestoreCommand(parsed.positionals[0], Boolean(parsed.flags.yes)));
|
|
1546
|
+
return;
|
|
1547
|
+
case "update":
|
|
1548
|
+
process.exit(
|
|
1549
|
+
await runUpdateCommand({ check: Boolean(parsed.flags.check), yes: Boolean(parsed.flags.yes) })
|
|
1550
|
+
);
|
|
1551
|
+
return;
|
|
1552
|
+
default: {
|
|
1553
|
+
const _exhaustive = command;
|
|
1554
|
+
throw new Error(`Unhandled command: ${String(_exhaustive)}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
main().catch((err) => {
|
|
1559
|
+
console.error(red(`antbot: ${err.stack ?? err.message}`));
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
});
|
|
1562
|
+
//# sourceMappingURL=index.js.map
|