@michael-joseph-miller/ant-bot 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import { createRequire as __antbotCreateRequire } from 'node:module';
3
3
  const require = __antbotCreateRequire(import.meta.url);
4
4
  import {
5
5
  SettingsSchema
6
- } from "./chunk-I3GHIX3G.js";
6
+ } from "./chunk-BKF7CUVN.js";
7
7
 
8
8
  // cli/src/index.ts
9
9
  import fs10 from "node:fs";
10
- import readline2 from "node:readline/promises";
10
+ import readline3 from "node:readline/promises";
11
11
  import path11 from "node:path";
12
12
 
13
13
  // cli/src/args.ts
@@ -21,6 +21,7 @@ var KNOWN_COMMANDS = [
21
21
  "skill",
22
22
  "mcp",
23
23
  "connector",
24
+ "secret",
24
25
  "backup",
25
26
  "restore",
26
27
  "update"
@@ -43,6 +44,7 @@ var COMMAND_FLAGS = {
43
44
  skill: {},
44
45
  mcp: {},
45
46
  connector: {},
47
+ secret: {},
46
48
  backup: {
47
49
  out: { type: "string" }
48
50
  },
@@ -54,7 +56,7 @@ var COMMAND_FLAGS = {
54
56
  yes: { type: "boolean", default: false }
55
57
  }
56
58
  };
57
- var PASSTHROUGH_COMMANDS = /* @__PURE__ */ new Set(["mcp", "connector"]);
59
+ var PASSTHROUGH_COMMANDS = /* @__PURE__ */ new Set(["mcp", "connector", "secret"]);
58
60
  function isKnownCommand(value) {
59
61
  return KNOWN_COMMANDS.includes(value);
60
62
  }
@@ -152,6 +154,7 @@ Daemon:
152
154
  Data:
153
155
  skill <subcommand> Manage the skills your bots can use
154
156
  mcp <subcommand> Manage the MCP servers your bots can use
157
+ secret <subcommand> Credentials in your keychain (mcp add stores them for you)
155
158
  backup [--out PATH] Archive the database, config, skills, and bot memory
156
159
  restore <path> Restore from a backup archive
157
160
 
@@ -166,42 +169,40 @@ Environment:
166
169
  ANTBOT_HOME Data directory (default: ~/.ant-bot)
167
170
 
168
171
  The UI lives at http://127.0.0.1:4780 and is served by the daemon itself.`;
169
- var MCP_HELP = `antbot mcp \u2014 manage the MCP servers your bots can use
172
+ var MCP_HELP = `antbot mcp \u2014 the MCP servers your bots can use
170
173
 
171
174
  Usage: antbot mcp <subcommand>
172
175
 
173
- list Show every server, with warnings for anything unusable
174
- add <name> ... Add one: --stdio "<cmd>" or --url <url>
175
- login <name> Sign in interactively (http/sse servers that need OAuth)
176
- logout <name> Forget a stored sign-in
177
- enable|disable <name> Turn one on or off for every bot at once
178
- remove <name> Delete it, and every bot's assignment to it
179
- test <name> Connect and list the tools it offers
180
-
181
- A server is registered once for the account and then assigned to individual bots in
182
- Bot settings \u2014 a bot with no assignment cannot see its tools at all. Its tools reach
183
- bots as \`mcp__<name>__<tool>\` and pass the permission gateway like any other tool,
184
- so the first call asks you for approval.
185
-
186
- Credentials, two ways. A static token goes in a header or an env var, written as
187
- {{secret:NAME}} so the value stays in your keychain:
188
-
189
- antbot mcp add fs --stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
190
- antbot mcp add gh --url https://api.example.com/mcp \\
191
- --header "Authorization=Bearer {{secret:GH_TOKEN}}"
192
-
193
- A server that wants an interactive sign-in instead uses \`login\`, which discovers
194
- what it accepts and prints a URL to open:
176
+ add <name> [<command> | <url>] Add one. No command or URL means a built-in (e.g. gmail).
177
+ --env VAR[=value] stdio: an env var. Without =value you are prompted, and the
178
+ value goes to the keychain \u2014 it never appears in config.
179
+ --header K[=V] http: a header, same rule.
180
+ --bots a,b | all | none Which bots get it. Asked interactively if omitted.
181
+ check <name> One honest verdict: ready / needs sign-in / needs credential /
182
+ unreachable, plus the tools it offers.
183
+ login <name> Sign in (again). --client-id / --client-secret for providers
184
+ that will not register apps themselves (Google).
185
+ logout <name> Forget a stored sign-in.
186
+ enable|disable <name> Turn one off for every bot at once, or back on.
187
+ remove <name> Delete it, and every bot's assignment to it.
188
+ list Everything, with state and which bots have each.
189
+
190
+ Examples:
191
+ antbot mcp add gmail built-in; guided setup
192
+ antbot mcp add github "npx -y @modelcontextprotocol/server-github" --env GITHUB_TOKEN
193
+ antbot mcp add vercel https://mcp.vercel.com signs in automatically
194
+
195
+ A connector is registered once and assigned to bots in Bot settings \u2014 a bot with no assignment
196
+ cannot see its tools at all. Tools reach bots as \`mcp__<name>__<tool>\` and pass the permission
197
+ gateway like any other tool, so the first call asks you for approval.
195
198
 
196
- antbot mcp login gmail
197
-
198
- Providers that support dynamic client registration need nothing else. Some \u2014 Google
199
- among them \u2014 do not, and want a client ID you create in their console, with
200
- http://127.0.0.1:4780/api/connectors/oauth/callback as an authorised redirect URI:
199
+ \`connector\` is the older name for this command and still works.`;
200
+ var SECRET_HELP = `antbot secret \u2014 credentials in your keychain
201
201
 
202
- antbot mcp login gmail --client-id YOUR_ID.apps.googleusercontent.com
202
+ Usage: antbot secret list | set <NAME> | remove <NAME>
203
203
 
204
- \`connector\` is the older name for this command and still works.`;
204
+ Values are prompted for, never passed on the command line, and never shown again. Most people
205
+ never need this directly: \`antbot mcp add \u2026 --env VAR\` stores what a connector needs for you.`;
205
206
  var COMMAND_HELP = {
206
207
  start: `antbot start \u2014 start the daemon in the background
207
208
 
@@ -304,6 +305,7 @@ In a git checkout there is nothing for a package manager to update \u2014 use
304
305
  \`git pull && pnpm install\` instead.`,
305
306
  connector: MCP_HELP,
306
307
  mcp: MCP_HELP,
308
+ secret: SECRET_HELP,
307
309
  restore: `antbot restore \u2014 restore from a backup archive
308
310
 
309
311
  Usage: antbot restore <path> [--yes]
@@ -1218,22 +1220,9 @@ async function lintSkills(args) {
1218
1220
  return 0;
1219
1221
  }
1220
1222
 
1221
- // cli/src/connector.ts
1222
- function collectPairs(argv, flag) {
1223
- const out = {};
1224
- for (let i = 0; i < argv.length; i++) {
1225
- if (argv[i] !== flag) continue;
1226
- const raw = argv[i + 1] ?? "";
1227
- const eq = raw.indexOf("=");
1228
- if (eq > 0) out[raw.slice(0, eq)] = raw.slice(eq + 1);
1229
- i++;
1230
- }
1231
- return out;
1232
- }
1233
- function flagValue(argv, flag) {
1234
- const i = argv.indexOf(flag);
1235
- return i >= 0 ? argv[i + 1] : void 0;
1236
- }
1223
+ // cli/src/mcp.ts
1224
+ import readline from "node:readline/promises";
1225
+ import { spawn as spawn3 } from "node:child_process";
1237
1226
  function splitCommand(input) {
1238
1227
  const parts = input.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
1239
1228
  const clean = parts.map(
@@ -1241,131 +1230,258 @@ function splitCommand(input) {
1241
1230
  );
1242
1231
  return { command: clean[0] ?? "", args: clean.slice(1) };
1243
1232
  }
1244
- function buildConfigFromFlags(argv) {
1245
- const stdio = flagValue(argv, "--stdio");
1246
- const url = flagValue(argv, "--url");
1247
- if (stdio && url) return { error: "Use either --stdio or --url, not both." };
1248
- if (stdio) {
1249
- const { command, args } = splitCommand(stdio);
1250
- if (!command) return { error: '--stdio needs a command, e.g. --stdio "npx -y @scope/server"' };
1251
- return { config: { transport: "stdio", command, args, env: collectPairs(argv, "--env") } };
1252
- }
1253
- if (url) {
1254
- const transport = flagValue(argv, "--transport") ?? "http";
1255
- if (transport !== "http" && transport !== "sse") return { error: "--transport must be http or sse" };
1256
- const tools = flagValue(argv, "--tools");
1257
- return {
1258
- config: {
1259
- transport,
1260
- url,
1261
- headers: collectPairs(argv, "--header"),
1262
- ...tools ? { tools: tools.split(",").map((t) => t.trim()).filter(Boolean) } : {}
1263
- }
1264
- };
1233
+ var looksLikeUrl = (s) => /^https?:\/\//i.test(s.trim());
1234
+ function collectFlag(argv, flag) {
1235
+ const out = [];
1236
+ for (let i = 0; i < argv.length; i++) if (argv[i] === flag && argv[i + 1] !== void 0) out.push(argv[++i]);
1237
+ return out;
1238
+ }
1239
+ function flagValue(argv, flag) {
1240
+ const i = argv.indexOf(flag);
1241
+ return i >= 0 ? argv[i + 1] : void 0;
1242
+ }
1243
+ var secretNameFor = (connector, key) => `mcp/${connector}/${key}`;
1244
+ function planPairs(raw) {
1245
+ return raw.map((r) => {
1246
+ const eq = r.indexOf("=");
1247
+ if (eq > 0) return { key: r.slice(0, eq), value: r.slice(eq + 1), secret: false };
1248
+ return { key: r, value: "", secret: true };
1249
+ });
1250
+ }
1251
+ function resolveBots(spec, roster) {
1252
+ if (spec === void 0) return "ask";
1253
+ if (spec === "none" || spec === "") return null;
1254
+ if (spec === "all") return roster.map((r) => r.bot.id);
1255
+ const wanted = spec.split(",").map((x) => x.trim().toLowerCase()).filter(Boolean);
1256
+ return roster.filter((r) => wanted.includes(r.bot.slug) || wanted.includes(r.bot.name.toLowerCase())).map((r) => r.bot.id);
1257
+ }
1258
+ function describeVerdict(name, check) {
1259
+ const n = check.tools.length;
1260
+ switch (check.status) {
1261
+ case "ready":
1262
+ return green(`\u2713 ${name}: ready, ${n} tool${n === 1 ? "" : "s"}`);
1263
+ case "needs-sign-in":
1264
+ return yellow(`! ${name}: needs sign-in${check.provider ? ` (${check.provider})` : ""}${n ? `, ${n} tools` : ""}`);
1265
+ case "needs-credential":
1266
+ return yellow(`! ${name}: ${check.detail ?? "needs a credential"}`);
1267
+ case "unreachable":
1268
+ return red(`\u2717 ${name}: unreachable${check.detail ? ` \u2014 ${check.detail}` : ""}`);
1269
+ }
1270
+ }
1271
+ async function ask(question, hidden = false) {
1272
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1273
+ if (hidden) {
1274
+ const out = process.stdout;
1275
+ const orig = out.write.bind(out);
1276
+ process.stdout.write(question);
1277
+ let muted = true;
1278
+ out.write = ((s) => muted ? true : orig(s));
1279
+ try {
1280
+ const v = await rl.question("");
1281
+ return v.trim();
1282
+ } finally {
1283
+ muted = false;
1284
+ out.write = orig;
1285
+ process.stdout.write("\n");
1286
+ rl.close();
1287
+ }
1288
+ }
1289
+ try {
1290
+ return (await rl.question(question)).trim();
1291
+ } finally {
1292
+ rl.close();
1265
1293
  }
1266
- return { error: 'Give either --stdio "<command>" or --url <url>.' };
1267
1294
  }
1268
- var transportOf = (c) => String(c.config.transport ?? "?");
1269
- async function findByName(port, name) {
1270
- return (await getJson(port, "/api/connectors")).find((c) => c.name === name);
1295
+ function openBrowser(url) {
1296
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1297
+ try {
1298
+ const child = spawn3(cmd, [url], { stdio: "ignore", detached: true });
1299
+ child.on("error", () => {
1300
+ });
1301
+ child.unref();
1302
+ } catch {
1303
+ }
1304
+ console.log(dim(" If a browser did not open, use this link:"));
1305
+ console.log(` ${url}`);
1306
+ }
1307
+ async function load(port) {
1308
+ try {
1309
+ return await getJson(port, "/api/connectors");
1310
+ } catch (err) {
1311
+ console.error(red(`Could not reach the daemon on port ${port}: ${err.message}`));
1312
+ console.error(dim("Start it with `antbot start`."));
1313
+ return null;
1314
+ }
1315
+ }
1316
+ async function storePairs(port, connector, pairs) {
1317
+ const out = {};
1318
+ for (const p of pairs) {
1319
+ if (!p.secret) {
1320
+ out[p.key] = p.value;
1321
+ continue;
1322
+ }
1323
+ const value = await ask(` ${p.key}: `, true);
1324
+ if (!value) throw new Error(`${p.key} needs a value.`);
1325
+ const name = secretNameFor(connector, p.key);
1326
+ await postJson(port, "/api/secrets", { name, value });
1327
+ out[p.key] = `{{secret:${name}}}`;
1328
+ }
1329
+ return out;
1271
1330
  }
1272
- async function runConnectorCommand(argv, port) {
1331
+ async function signIn(port, row, opts) {
1332
+ let res;
1333
+ try {
1334
+ res = await postJson(port, `/api/connectors/${row.id}/login`, opts);
1335
+ } catch (err) {
1336
+ console.error(yellow(` ${err.message}`));
1337
+ return false;
1338
+ }
1339
+ console.log(dim(" Opening your browser to sign in\u2026"));
1340
+ openBrowser(res.authorizeUrl);
1341
+ const deadline = Date.now() + 5 * 6e4;
1342
+ while (Date.now() < deadline) {
1343
+ await new Promise((r) => setTimeout(r, 2e3));
1344
+ const rows = await getJson(port, "/api/connectors").catch(() => []);
1345
+ const cur = rows.find((r) => r.id === row.id);
1346
+ if (cur?.signedIn) return true;
1347
+ }
1348
+ console.log(yellow(" Still waiting for the sign-in. Finish it in the browser, then run: antbot mcp check " + row.name));
1349
+ return false;
1350
+ }
1351
+ async function collectClientCredentials(entry, provider) {
1352
+ console.log(yellow(` ${provider ?? "This provider"} needs an OAuth client you create yourself \u2014 a one-time setup:`));
1353
+ for (const [i, step] of (entry?.setupSteps ?? []).entries()) console.log(` ${i + 1}. ${step}`);
1354
+ const clientId = await ask(" Client ID (blank to do this later): ");
1355
+ if (!clientId) return null;
1356
+ const clientSecret = await ask(" Client secret (blank if none): ", true);
1357
+ return { clientId, ...clientSecret ? { clientSecret } : {} };
1358
+ }
1359
+ async function runMcpCommand(argv, port) {
1273
1360
  const sub = argv[0];
1274
1361
  const hint = () => {
1275
- console.error(dim("Subcommands: list, add, login, logout, enable, disable, remove, test."));
1362
+ console.error(dim("Subcommands: list, add, check, login, logout, enable, disable, remove."));
1276
1363
  console.error(dim("Run `antbot mcp --help` for the full usage."));
1277
1364
  };
1278
1365
  if (!sub) {
1279
- console.error(red("antbot connector needs a subcommand."));
1366
+ console.error(red("antbot mcp needs a subcommand."));
1280
1367
  hint();
1281
1368
  return 2;
1282
1369
  }
1283
- let rows;
1284
- const load = async () => {
1285
- try {
1286
- rows = await getJson(port, "/api/connectors");
1287
- return true;
1288
- } catch (err) {
1289
- console.error(red(`Could not reach the daemon on port ${port}: ${err.message}`));
1290
- console.error(dim("Start it with `antbot start`."));
1291
- return false;
1292
- }
1370
+ const byName = async (name) => {
1371
+ const rows = await load(port);
1372
+ if (!rows) return null;
1373
+ const m = rows.find((c) => c.name === name);
1374
+ if (!m) console.error(red(`No connector named "${name}".`));
1375
+ return m ?? null;
1293
1376
  };
1294
1377
  switch (sub) {
1295
1378
  case "list": {
1296
- if (!await load()) return 1;
1379
+ const rows = await load(port);
1380
+ if (!rows) return 1;
1297
1381
  if (!rows.length) {
1298
- console.log(dim("No connectors yet. Add one with `antbot connector add`."));
1382
+ console.log(dim("No connectors yet. Add one with `antbot mcp add`."));
1299
1383
  return 0;
1300
1384
  }
1385
+ const roster = await getJson(port, "/api/bots").catch(() => []);
1386
+ const assigned = /* @__PURE__ */ new Map();
1387
+ for (const r of roster) {
1388
+ const cs = await getJson(port, `/api/bots/${r.bot.id}/connectors`).catch(() => []);
1389
+ for (const c of cs) assigned.set(c.name, [...assigned.get(c.name) ?? [], r.bot.name]);
1390
+ }
1301
1391
  for (const c of rows) {
1302
- const state = c.enabled ? "" : dim(" (disabled)");
1303
- const signed = c.signedIn ? green(" signed in") : "";
1304
- console.log(`${bold(c.name)} ${dim(transportOf(c))}${state}${signed}`);
1305
- if (c.description) console.log(` ${c.description}`);
1306
- if (c.missingSecrets.length) {
1307
- console.log(yellow(` missing secret(s): ${c.missingSecrets.join(", ")} \u2014 this connector will not mount`));
1308
- }
1392
+ const kind = c.kind === "builtin" ? "built-in" : String(c.config.transport ?? "?");
1393
+ const state = !c.enabled ? dim("disabled") : c.signedIn ? green("signed in") : c.missingSecrets.length ? yellow(`missing secret: ${c.missingSecrets.join(", ")}`) : c.lastStatus === "ready" || c.lastStatus === "connected" ? green("ready") : c.lastStatus ? yellow(c.lastStatus) : dim("unchecked");
1394
+ const bots = assigned.get(c.name)?.length ? dim(`\u2192 ${assigned.get(c.name).join(", ")}`) : dim("\u2192 no bots");
1395
+ console.log(`${bold(c.name.padEnd(14))} ${kind.padEnd(9)} ${state} ${bots}`);
1396
+ if (c.description) console.log(` ${dim(c.description)}`);
1397
+ if (c.lastError && c.lastStatus !== "ready") console.log(` ${dim(c.lastError)}`);
1309
1398
  }
1310
1399
  return 0;
1311
1400
  }
1312
1401
  case "add": {
1313
1402
  const name = argv[1];
1403
+ const target = argv[2] && !argv[2].startsWith("--") ? argv[2] : void 0;
1314
1404
  if (!name || name.startsWith("--")) {
1315
- console.error(red('Usage: antbot connector add <name> (--stdio "<cmd>" | --url <url>)'));
1405
+ console.error(red("Usage: antbot mcp add <name> [<command> | <url>] [--env VAR[=value]]... [--header K=V]... [--bots a,b|all]"));
1316
1406
  return 2;
1317
1407
  }
1318
- const built = buildConfigFromFlags(argv);
1319
- if ("error" in built) {
1320
- console.error(red(built.error));
1321
- return 2;
1408
+ const catalog = await getJson(port, "/api/connectors/catalog").catch(() => []);
1409
+ const entry = catalog.find((e) => e.name === name) ?? null;
1410
+ let body;
1411
+ if (!target) {
1412
+ if (!entry) {
1413
+ console.error(red(`"${name}" is not a built-in connector. Give a command or a URL:`));
1414
+ console.error(dim(` antbot mcp add ${name} "npx -y some-server" or antbot mcp add ${name} https://\u2026/mcp`));
1415
+ if (catalog.length) console.error(dim(` Built-in: ${catalog.map((e) => e.name).join(", ")}`));
1416
+ return 2;
1417
+ }
1418
+ console.log(`${bold(entry.displayName)} is built into ant-bot. ${entry.description}`);
1419
+ body = { name, builtin: name };
1420
+ } else if (looksLikeUrl(target)) {
1421
+ const headers = await storePairs(port, name, planPairs(collectFlag(argv, "--header")));
1422
+ const transport = flagValue(argv, "--transport") === "sse" ? "sse" : "http";
1423
+ body = { name, config: { transport, url: target.trim(), headers } };
1424
+ } else {
1425
+ const { command, args } = splitCommand(target);
1426
+ if (!command) {
1427
+ console.error(red("The command is empty."));
1428
+ return 2;
1429
+ }
1430
+ const env = await storePairs(port, name, planPairs(collectFlag(argv, "--env")));
1431
+ body = { name, config: { transport: "stdio", command, args, env } };
1432
+ }
1433
+ if (flagValue(argv, "--desc")) body.description = flagValue(argv, "--desc");
1434
+ const roster = await getJson(port, "/api/bots").catch(() => []);
1435
+ let botIds = resolveBots(flagValue(argv, "--bots"), roster);
1436
+ if (botIds === "ask") {
1437
+ if (roster.length) {
1438
+ const names = roster.map((r) => r.bot.name).join(", ");
1439
+ const ans = await ask(` Give it to which bots? [${names}, all, none] `);
1440
+ botIds = resolveBots(ans || "none", roster);
1441
+ } else botIds = null;
1322
1442
  }
1443
+ if (botIds && botIds.length) body.botIds = botIds;
1444
+ let created;
1323
1445
  try {
1324
- const created = await postJson(port, "/api/connectors", {
1325
- name,
1326
- description: flagValue(argv, "--desc") ?? "",
1327
- config: built.config
1328
- });
1329
- console.log(green(`Added connector "${created.name}".`));
1330
- console.log(dim("Assign it to a bot in Bot settings, then `antbot connector test` to check it."));
1331
- return 0;
1446
+ created = await postJson(port, "/api/connectors", body);
1332
1447
  } catch (err) {
1333
1448
  console.error(red(err.message));
1334
1449
  return 1;
1335
1450
  }
1336
- }
1337
- case "enable":
1338
- case "disable": {
1339
- const name = argv[1];
1340
- if (!name) {
1341
- console.error(red(`Usage: antbot connector ${sub} <name>`));
1342
- return 2;
1343
- }
1344
- if (!await load()) return 1;
1345
- const match = rows.find((c) => c.name === name);
1346
- if (!match) {
1347
- console.error(red(`No connector named "${name}".`));
1348
- return 1;
1451
+ console.log(describeVerdict(name, created.check));
1452
+ if (created.check.status === "needs-sign-in") {
1453
+ let creds = {};
1454
+ if (!created.check.selfRegistration) {
1455
+ const c = await collectClientCredentials(entry, created.check.provider);
1456
+ if (!c) {
1457
+ console.log(dim(` Later: antbot mcp login ${name} --client-id ID --client-secret SECRET`));
1458
+ return 0;
1459
+ }
1460
+ creds = c;
1461
+ }
1462
+ const ok = await signIn(port, created, creds);
1463
+ if (ok) {
1464
+ const check = await postJson(port, `/api/connectors/${created.id}/check`, {});
1465
+ console.log(describeVerdict(name, check));
1466
+ }
1349
1467
  }
1350
- await patchJson(port, `/api/connectors/${match.id}`, { enabled: sub === "enable" });
1351
- console.log(green(`Connector "${name}" ${sub}d.`));
1468
+ if (botIds && botIds.length) console.log(dim(` Assigned to ${botIds.length} bot${botIds.length === 1 ? "" : "s"}.`));
1469
+ else console.log(dim(" Not assigned to any bot yet \u2014 tick it in Bot settings when you want a bot to have it."));
1352
1470
  return 0;
1353
1471
  }
1354
- case "remove": {
1472
+ case "check": {
1355
1473
  const name = argv[1];
1356
1474
  if (!name) {
1357
- console.error(red("Usage: antbot connector remove <name>"));
1475
+ console.error(red("Usage: antbot mcp check <name>"));
1358
1476
  return 2;
1359
1477
  }
1360
- if (!await load()) return 1;
1361
- const match = rows.find((c) => c.name === name);
1362
- if (!match) {
1363
- console.error(red(`No connector named "${name}".`));
1364
- return 1;
1365
- }
1366
- await deleteJson(port, `/api/connectors/${match.id}`);
1367
- console.log(green(`Removed connector "${name}". Bot assignments for it are gone too.`));
1368
- return 0;
1478
+ const row = await byName(name);
1479
+ if (!row) return 1;
1480
+ const check = await postJson(port, `/api/connectors/${row.id}/check`, {});
1481
+ console.log(describeVerdict(name, check));
1482
+ for (const t of check.tools) console.log(` ${bold(`mcp__${name}__${t.name}`)}${t.description ? dim(` ${t.description.slice(0, 90)}`) : ""}`);
1483
+ if (check.status === "needs-sign-in") console.log(dim(` Sign in with: antbot mcp login ${name}`));
1484
+ return check.status === "ready" ? 0 : 1;
1369
1485
  }
1370
1486
  case "login": {
1371
1487
  const name = argv[1];
@@ -1373,29 +1489,20 @@ async function runConnectorCommand(argv, port) {
1373
1489
  console.error(red("Usage: antbot mcp login <name> [--client-id ID] [--client-secret SECRET]"));
1374
1490
  return 2;
1375
1491
  }
1376
- if (!await load()) return 1;
1377
- const match = rows.find((c) => c.name === name);
1378
- if (!match) {
1379
- console.error(red(`No connector named "${name}".`));
1380
- return 1;
1381
- }
1382
- const body = {};
1492
+ const row = await byName(name);
1493
+ if (!row) return 1;
1494
+ const creds = {};
1383
1495
  const cid = flagValue(argv, "--client-id");
1384
1496
  const csec = flagValue(argv, "--client-secret");
1385
- const scopes = flagValue(argv, "--scopes");
1386
- if (cid) body.clientId = cid;
1387
- if (csec) body.clientSecret = csec;
1388
- if (scopes) body.scopes = scopes.split(",").map((x) => x.trim()).filter(Boolean);
1389
- try {
1390
- const res = await postJson(port, `/api/connectors/${match.id}/login`, body);
1391
- console.log(bold("Open this URL to sign in:"));
1392
- console.log(` ${res.authorizeUrl}`);
1393
- console.log(dim("The browser returns to ant-bot when you are done. Then: antbot mcp test " + name));
1497
+ if (cid) creds.clientId = cid;
1498
+ if (csec) creds.clientSecret = csec;
1499
+ const ok = await signIn(port, row, creds);
1500
+ if (ok) {
1501
+ const check = await postJson(port, `/api/connectors/${row.id}/check`, {});
1502
+ console.log(describeVerdict(name, check));
1394
1503
  return 0;
1395
- } catch (err) {
1396
- console.error(red(err.message));
1397
- return 1;
1398
1504
  }
1505
+ return 1;
1399
1506
  }
1400
1507
  case "logout": {
1401
1508
  const name = argv[1];
@@ -1403,49 +1510,89 @@ async function runConnectorCommand(argv, port) {
1403
1510
  console.error(red("Usage: antbot mcp logout <name>"));
1404
1511
  return 2;
1405
1512
  }
1406
- if (!await load()) return 1;
1407
- const match = rows.find((c) => c.name === name);
1408
- if (!match) {
1409
- console.error(red(`No connector named "${name}".`));
1410
- return 1;
1411
- }
1412
- await deleteJson(port, `/api/connectors/${match.id}/login`);
1513
+ const row = await byName(name);
1514
+ if (!row) return 1;
1515
+ await deleteJson(port, `/api/connectors/${row.id}/login`);
1413
1516
  console.log(green(`Signed out of "${name}".`));
1414
1517
  return 0;
1415
1518
  }
1416
- case "test": {
1519
+ case "enable":
1520
+ case "disable": {
1521
+ const name = argv[1];
1522
+ if (!name) {
1523
+ console.error(red(`Usage: antbot mcp ${sub} <name>`));
1524
+ return 2;
1525
+ }
1526
+ const row = await byName(name);
1527
+ if (!row) return 1;
1528
+ await patchJson(port, `/api/connectors/${row.id}`, { enabled: sub === "enable" });
1529
+ console.log(green(`Connector "${name}" ${sub}d.`));
1530
+ return 0;
1531
+ }
1532
+ case "remove": {
1417
1533
  const name = argv[1];
1418
1534
  if (!name) {
1419
- console.error(red("Usage: antbot connector test <name>"));
1535
+ console.error(red("Usage: antbot mcp remove <name>"));
1420
1536
  return 2;
1421
1537
  }
1422
- let match;
1538
+ const row = await byName(name);
1539
+ if (!row) return 1;
1540
+ await deleteJson(port, `/api/connectors/${row.id}`);
1541
+ console.log(green(`Removed "${name}". Bot assignments for it are gone too.`));
1542
+ return 0;
1543
+ }
1544
+ default:
1545
+ console.error(red(`Unknown subcommand "${sub}".`));
1546
+ hint();
1547
+ return 2;
1548
+ }
1549
+ }
1550
+ async function runSecretCommand(argv, port) {
1551
+ const sub = argv[0];
1552
+ switch (sub) {
1553
+ case "list": {
1423
1554
  try {
1424
- match = await findByName(port, name);
1555
+ const r = await getJson(port, "/api/secrets");
1556
+ if (!r.names.length) console.log(dim(`No secrets stored (backend: ${r.backend}).`));
1557
+ for (const n of r.names) console.log(n);
1558
+ return 0;
1425
1559
  } catch (err) {
1426
- console.error(red(`Could not reach the daemon on port ${port}: ${err.message}`));
1560
+ console.error(red(err.message));
1427
1561
  return 1;
1428
1562
  }
1429
- if (!match) {
1430
- console.error(red(`No connector named "${name}".`));
1431
- return 1;
1563
+ }
1564
+ case "set": {
1565
+ const name = argv[1];
1566
+ if (!name) {
1567
+ console.error(red("Usage: antbot secret set <NAME>"));
1568
+ return 2;
1569
+ }
1570
+ const value = await ask(` Value for ${name}: `, true);
1571
+ if (!value) {
1572
+ console.error(red("A value is required."));
1573
+ return 2;
1432
1574
  }
1433
- const result = await postJson(port, `/api/connectors/${match.id}/test`, {});
1434
- if (!result.ok) {
1435
- console.error(red(`${name}: ${result.error ?? "could not connect"}`));
1575
+ try {
1576
+ await postJson(port, "/api/secrets", { name, value });
1577
+ console.log(green(`Stored ${name} in the keychain.`));
1578
+ return 0;
1579
+ } catch (err) {
1580
+ console.error(red(err.message));
1436
1581
  return 1;
1437
1582
  }
1438
- console.log(green(`${name}: connected, ${result.tools.length} tool(s)`));
1439
- if (result.authHint) console.log(yellow(` ${result.authHint}`));
1440
- for (const t of result.tools) {
1441
- console.log(` ${bold(`mcp__${name}__${t.name}`)}`);
1442
- if (t.description) console.log(` ${dim(t.description)}`);
1583
+ }
1584
+ case "remove": {
1585
+ const name = argv[1];
1586
+ if (!name) {
1587
+ console.error(red("Usage: antbot secret remove <NAME>"));
1588
+ return 2;
1443
1589
  }
1590
+ await deleteJson(port, `/api/secrets/${encodeURIComponent(name)}`);
1591
+ console.log(green(`Removed ${name}.`));
1444
1592
  return 0;
1445
1593
  }
1446
1594
  default:
1447
- console.error(red(`Unknown subcommand "${sub}".`));
1448
- hint();
1595
+ console.error(red("Usage: antbot secret list | set <NAME> | remove <NAME>"));
1449
1596
  return 2;
1450
1597
  }
1451
1598
  }
@@ -1453,7 +1600,7 @@ async function runConnectorCommand(argv, port) {
1453
1600
  // cli/src/updateCommand.ts
1454
1601
  import fs9 from "node:fs";
1455
1602
  import path10 from "node:path";
1456
- import readline from "node:readline/promises";
1603
+ import readline2 from "node:readline/promises";
1457
1604
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1458
1605
 
1459
1606
  // cli/src/update.ts
@@ -1628,7 +1775,7 @@ async function runUpdateCommand(opts) {
1628
1775
  const pm = detectPackageManager(here);
1629
1776
  const { cmd, args } = updateCommand(pm);
1630
1777
  if (!opts.yes) {
1631
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1778
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1632
1779
  const answer = await rl.question(`Run \`${cmd} ${args.join(" ")}\`? [y/N] `);
1633
1780
  rl.close();
1634
1781
  if (!/^y(es)?$/i.test(answer.trim())) {
@@ -1744,7 +1891,7 @@ async function runRestoreCommand(archivePath, skipConfirm) {
1744
1891
  }
1745
1892
  const paths = loadPaths();
1746
1893
  if (!skipConfirm) {
1747
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1894
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
1748
1895
  const answer = await rl.question(
1749
1896
  `This will overwrite files under ${paths.root} with the contents of ${archivePath}. Continue? [y/N] `
1750
1897
  );
@@ -1829,7 +1976,12 @@ async function main() {
1829
1976
  case "mcp":
1830
1977
  case "connector": {
1831
1978
  const cfg = loadConfig();
1832
- process.exit(await runConnectorCommand(parsed.positionals, cfg.port));
1979
+ process.exit(await runMcpCommand(parsed.positionals, cfg.port));
1980
+ return;
1981
+ }
1982
+ case "secret": {
1983
+ const cfg = loadConfig();
1984
+ process.exit(await runSecretCommand(parsed.positionals, cfg.port));
1833
1985
  return;
1834
1986
  }
1835
1987
  case "backup":