@michael-joseph-miller/ant-bot 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-G4IIENWF.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,259 @@ 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
+ if (check.alternative) return red(`\u2717 ${name}: ${check.detail ?? `use the built-in instead: antbot mcp add ${check.alternative}`}`);
1265
+ return yellow(`! ${name}: needs sign-in${check.provider ? ` (${check.provider})` : ""}${n ? `, ${n} tools` : ""}`);
1266
+ case "needs-credential":
1267
+ return yellow(`! ${name}: ${check.detail ?? "needs a credential"}`);
1268
+ case "unreachable":
1269
+ return red(`\u2717 ${name}: unreachable${check.detail ? ` \u2014 ${check.detail}` : ""}`);
1270
+ }
1271
+ }
1272
+ async function ask(question, hidden = false) {
1273
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1274
+ if (hidden) {
1275
+ const out = process.stdout;
1276
+ const orig = out.write.bind(out);
1277
+ process.stdout.write(question);
1278
+ let muted = true;
1279
+ out.write = ((s) => muted ? true : orig(s));
1280
+ try {
1281
+ const v = await rl.question("");
1282
+ return v.trim();
1283
+ } finally {
1284
+ muted = false;
1285
+ out.write = orig;
1286
+ process.stdout.write("\n");
1287
+ rl.close();
1288
+ }
1289
+ }
1290
+ try {
1291
+ return (await rl.question(question)).trim();
1292
+ } finally {
1293
+ rl.close();
1265
1294
  }
1266
- return { error: 'Give either --stdio "<command>" or --url <url>.' };
1267
1295
  }
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);
1296
+ function openBrowser(url) {
1297
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1298
+ try {
1299
+ const child = spawn3(cmd, [url], { stdio: "ignore", detached: true });
1300
+ child.on("error", () => {
1301
+ });
1302
+ child.unref();
1303
+ } catch {
1304
+ }
1305
+ console.log(dim(" If a browser did not open, use this link:"));
1306
+ console.log(` ${url}`);
1307
+ }
1308
+ async function load(port) {
1309
+ try {
1310
+ return await getJson(port, "/api/connectors");
1311
+ } catch (err) {
1312
+ console.error(red(`Could not reach the daemon on port ${port}: ${err.message}`));
1313
+ console.error(dim("Start it with `antbot start`."));
1314
+ return null;
1315
+ }
1316
+ }
1317
+ async function storePairs(port, connector, pairs) {
1318
+ const out = {};
1319
+ for (const p of pairs) {
1320
+ if (!p.secret) {
1321
+ out[p.key] = p.value;
1322
+ continue;
1323
+ }
1324
+ const value = await ask(` ${p.key}: `, true);
1325
+ if (!value) throw new Error(`${p.key} needs a value.`);
1326
+ const name = secretNameFor(connector, p.key);
1327
+ await postJson(port, "/api/secrets", { name, value });
1328
+ out[p.key] = `{{secret:${name}}}`;
1329
+ }
1330
+ return out;
1271
1331
  }
1272
- async function runConnectorCommand(argv, port) {
1332
+ async function signIn(port, row, opts) {
1333
+ let res;
1334
+ try {
1335
+ res = await postJson(port, `/api/connectors/${row.id}/login`, opts);
1336
+ } catch (err) {
1337
+ console.error(yellow(` ${err.message}`));
1338
+ return false;
1339
+ }
1340
+ console.log(dim(" Opening your browser to sign in\u2026"));
1341
+ openBrowser(res.authorizeUrl);
1342
+ const deadline = Date.now() + 5 * 6e4;
1343
+ while (Date.now() < deadline) {
1344
+ await new Promise((r) => setTimeout(r, 2e3));
1345
+ const rows = await getJson(port, "/api/connectors").catch(() => []);
1346
+ const cur = rows.find((r) => r.id === row.id);
1347
+ if (cur?.signedIn) return true;
1348
+ }
1349
+ console.log(yellow(" Still waiting for the sign-in. Finish it in the browser, then run: antbot mcp check " + row.name));
1350
+ return false;
1351
+ }
1352
+ async function collectClientCredentials(entry, provider) {
1353
+ console.log(yellow(` ${provider ?? "This provider"} needs an OAuth client you create yourself \u2014 a one-time setup:`));
1354
+ for (const [i, step] of (entry?.setupSteps ?? []).entries()) console.log(` ${i + 1}. ${step}`);
1355
+ const clientId = await ask(" Client ID (blank to do this later): ");
1356
+ if (!clientId) return null;
1357
+ const clientSecret = await ask(" Client secret (blank if none): ", true);
1358
+ return { clientId, ...clientSecret ? { clientSecret } : {} };
1359
+ }
1360
+ async function runMcpCommand(argv, port) {
1273
1361
  const sub = argv[0];
1274
1362
  const hint = () => {
1275
- console.error(dim("Subcommands: list, add, login, logout, enable, disable, remove, test."));
1363
+ console.error(dim("Subcommands: list, add, check, login, logout, enable, disable, remove."));
1276
1364
  console.error(dim("Run `antbot mcp --help` for the full usage."));
1277
1365
  };
1278
1366
  if (!sub) {
1279
- console.error(red("antbot connector needs a subcommand."));
1367
+ console.error(red("antbot mcp needs a subcommand."));
1280
1368
  hint();
1281
1369
  return 2;
1282
1370
  }
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
- }
1371
+ const byName = async (name) => {
1372
+ const rows = await load(port);
1373
+ if (!rows) return null;
1374
+ const m = rows.find((c) => c.name === name);
1375
+ if (!m) console.error(red(`No connector named "${name}".`));
1376
+ return m ?? null;
1293
1377
  };
1294
1378
  switch (sub) {
1295
1379
  case "list": {
1296
- if (!await load()) return 1;
1380
+ const rows = await load(port);
1381
+ if (!rows) return 1;
1297
1382
  if (!rows.length) {
1298
- console.log(dim("No connectors yet. Add one with `antbot connector add`."));
1383
+ console.log(dim("No connectors yet. Add one with `antbot mcp add`."));
1299
1384
  return 0;
1300
1385
  }
1386
+ const roster = await getJson(port, "/api/bots").catch(() => []);
1387
+ const assigned = /* @__PURE__ */ new Map();
1388
+ for (const r of roster) {
1389
+ const cs = await getJson(port, `/api/bots/${r.bot.id}/connectors`).catch(() => []);
1390
+ for (const c of cs) assigned.set(c.name, [...assigned.get(c.name) ?? [], r.bot.name]);
1391
+ }
1301
1392
  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
- }
1393
+ const kind = c.kind === "builtin" ? "built-in" : String(c.config.transport ?? "?");
1394
+ 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");
1395
+ const bots = assigned.get(c.name)?.length ? dim(`\u2192 ${assigned.get(c.name).join(", ")}`) : dim("\u2192 no bots");
1396
+ console.log(`${bold(c.name.padEnd(14))} ${kind.padEnd(9)} ${state} ${bots}`);
1397
+ if (c.description) console.log(` ${dim(c.description)}`);
1398
+ if (c.lastError && c.lastStatus !== "ready") console.log(` ${dim(c.lastError)}`);
1309
1399
  }
1310
1400
  return 0;
1311
1401
  }
1312
1402
  case "add": {
1313
1403
  const name = argv[1];
1404
+ const target = argv[2] && !argv[2].startsWith("--") ? argv[2] : void 0;
1314
1405
  if (!name || name.startsWith("--")) {
1315
- console.error(red('Usage: antbot connector add <name> (--stdio "<cmd>" | --url <url>)'));
1406
+ console.error(red("Usage: antbot mcp add <name> [<command> | <url>] [--env VAR[=value]]... [--header K=V]... [--bots a,b|all]"));
1316
1407
  return 2;
1317
1408
  }
1318
- const built = buildConfigFromFlags(argv);
1319
- if ("error" in built) {
1320
- console.error(red(built.error));
1321
- return 2;
1409
+ const catalog = await getJson(port, "/api/connectors/catalog").catch(() => []);
1410
+ const entry = catalog.find((e) => e.name === name) ?? null;
1411
+ let body;
1412
+ if (!target) {
1413
+ if (!entry) {
1414
+ console.error(red(`"${name}" is not a built-in connector. Give a command or a URL:`));
1415
+ console.error(dim(` antbot mcp add ${name} "npx -y some-server" or antbot mcp add ${name} https://\u2026/mcp`));
1416
+ if (catalog.length) console.error(dim(` Built-in: ${catalog.map((e) => e.name).join(", ")}`));
1417
+ return 2;
1418
+ }
1419
+ console.log(`${bold(entry.displayName)} is built into ant-bot. ${entry.description}`);
1420
+ body = { name, builtin: name };
1421
+ } else if (looksLikeUrl(target)) {
1422
+ const headers = await storePairs(port, name, planPairs(collectFlag(argv, "--header")));
1423
+ const transport = flagValue(argv, "--transport") === "sse" ? "sse" : "http";
1424
+ body = { name, config: { transport, url: target.trim(), headers } };
1425
+ } else {
1426
+ const { command, args } = splitCommand(target);
1427
+ if (!command) {
1428
+ console.error(red("The command is empty."));
1429
+ return 2;
1430
+ }
1431
+ const env = await storePairs(port, name, planPairs(collectFlag(argv, "--env")));
1432
+ body = { name, config: { transport: "stdio", command, args, env } };
1433
+ }
1434
+ if (flagValue(argv, "--desc")) body.description = flagValue(argv, "--desc");
1435
+ const roster = await getJson(port, "/api/bots").catch(() => []);
1436
+ let botIds = resolveBots(flagValue(argv, "--bots"), roster);
1437
+ if (botIds === "ask") {
1438
+ if (roster.length) {
1439
+ const names = roster.map((r) => r.bot.name).join(", ");
1440
+ const ans = await ask(` Give it to which bots? [${names}, all, none] `);
1441
+ botIds = resolveBots(ans || "none", roster);
1442
+ } else botIds = null;
1322
1443
  }
1444
+ if (botIds && botIds.length) body.botIds = botIds;
1445
+ let created;
1323
1446
  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;
1447
+ created = await postJson(port, "/api/connectors", body);
1332
1448
  } catch (err) {
1333
1449
  console.error(red(err.message));
1334
1450
  return 1;
1335
1451
  }
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;
1452
+ console.log(describeVerdict(name, created.check));
1453
+ if (created.check.status === "needs-sign-in" && !created.check.alternative) {
1454
+ let creds = {};
1455
+ if (!created.check.selfRegistration) {
1456
+ const c = await collectClientCredentials(entry, created.check.provider);
1457
+ if (!c) {
1458
+ console.log(dim(` Later: antbot mcp login ${name} --client-id ID --client-secret SECRET`));
1459
+ return 0;
1460
+ }
1461
+ creds = c;
1462
+ }
1463
+ const ok = await signIn(port, created, creds);
1464
+ if (ok) {
1465
+ const check = await postJson(port, `/api/connectors/${created.id}/check`, {});
1466
+ console.log(describeVerdict(name, check));
1467
+ }
1349
1468
  }
1350
- await patchJson(port, `/api/connectors/${match.id}`, { enabled: sub === "enable" });
1351
- console.log(green(`Connector "${name}" ${sub}d.`));
1469
+ if (botIds && botIds.length) console.log(dim(` Assigned to ${botIds.length} bot${botIds.length === 1 ? "" : "s"}.`));
1470
+ 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
1471
  return 0;
1353
1472
  }
1354
- case "remove": {
1473
+ case "check": {
1355
1474
  const name = argv[1];
1356
1475
  if (!name) {
1357
- console.error(red("Usage: antbot connector remove <name>"));
1476
+ console.error(red("Usage: antbot mcp check <name>"));
1358
1477
  return 2;
1359
1478
  }
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;
1479
+ const row = await byName(name);
1480
+ if (!row) return 1;
1481
+ const check = await postJson(port, `/api/connectors/${row.id}/check`, {});
1482
+ console.log(describeVerdict(name, check));
1483
+ for (const t of check.tools) console.log(` ${bold(`mcp__${name}__${t.name}`)}${t.description ? dim(` ${t.description.slice(0, 90)}`) : ""}`);
1484
+ if (check.status === "needs-sign-in" && !check.alternative) console.log(dim(` Sign in with: antbot mcp login ${name}`));
1485
+ return check.status === "ready" ? 0 : 1;
1369
1486
  }
1370
1487
  case "login": {
1371
1488
  const name = argv[1];
@@ -1373,29 +1490,20 @@ async function runConnectorCommand(argv, port) {
1373
1490
  console.error(red("Usage: antbot mcp login <name> [--client-id ID] [--client-secret SECRET]"));
1374
1491
  return 2;
1375
1492
  }
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 = {};
1493
+ const row = await byName(name);
1494
+ if (!row) return 1;
1495
+ const creds = {};
1383
1496
  const cid = flagValue(argv, "--client-id");
1384
1497
  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));
1498
+ if (cid) creds.clientId = cid;
1499
+ if (csec) creds.clientSecret = csec;
1500
+ const ok = await signIn(port, row, creds);
1501
+ if (ok) {
1502
+ const check = await postJson(port, `/api/connectors/${row.id}/check`, {});
1503
+ console.log(describeVerdict(name, check));
1394
1504
  return 0;
1395
- } catch (err) {
1396
- console.error(red(err.message));
1397
- return 1;
1398
1505
  }
1506
+ return 1;
1399
1507
  }
1400
1508
  case "logout": {
1401
1509
  const name = argv[1];
@@ -1403,49 +1511,89 @@ async function runConnectorCommand(argv, port) {
1403
1511
  console.error(red("Usage: antbot mcp logout <name>"));
1404
1512
  return 2;
1405
1513
  }
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`);
1514
+ const row = await byName(name);
1515
+ if (!row) return 1;
1516
+ await deleteJson(port, `/api/connectors/${row.id}/login`);
1413
1517
  console.log(green(`Signed out of "${name}".`));
1414
1518
  return 0;
1415
1519
  }
1416
- case "test": {
1520
+ case "enable":
1521
+ case "disable": {
1522
+ const name = argv[1];
1523
+ if (!name) {
1524
+ console.error(red(`Usage: antbot mcp ${sub} <name>`));
1525
+ return 2;
1526
+ }
1527
+ const row = await byName(name);
1528
+ if (!row) return 1;
1529
+ await patchJson(port, `/api/connectors/${row.id}`, { enabled: sub === "enable" });
1530
+ console.log(green(`Connector "${name}" ${sub}d.`));
1531
+ return 0;
1532
+ }
1533
+ case "remove": {
1417
1534
  const name = argv[1];
1418
1535
  if (!name) {
1419
- console.error(red("Usage: antbot connector test <name>"));
1536
+ console.error(red("Usage: antbot mcp remove <name>"));
1420
1537
  return 2;
1421
1538
  }
1422
- let match;
1539
+ const row = await byName(name);
1540
+ if (!row) return 1;
1541
+ await deleteJson(port, `/api/connectors/${row.id}`);
1542
+ console.log(green(`Removed "${name}". Bot assignments for it are gone too.`));
1543
+ return 0;
1544
+ }
1545
+ default:
1546
+ console.error(red(`Unknown subcommand "${sub}".`));
1547
+ hint();
1548
+ return 2;
1549
+ }
1550
+ }
1551
+ async function runSecretCommand(argv, port) {
1552
+ const sub = argv[0];
1553
+ switch (sub) {
1554
+ case "list": {
1423
1555
  try {
1424
- match = await findByName(port, name);
1556
+ const r = await getJson(port, "/api/secrets");
1557
+ if (!r.names.length) console.log(dim(`No secrets stored (backend: ${r.backend}).`));
1558
+ for (const n of r.names) console.log(n);
1559
+ return 0;
1425
1560
  } catch (err) {
1426
- console.error(red(`Could not reach the daemon on port ${port}: ${err.message}`));
1561
+ console.error(red(err.message));
1427
1562
  return 1;
1428
1563
  }
1429
- if (!match) {
1430
- console.error(red(`No connector named "${name}".`));
1431
- return 1;
1564
+ }
1565
+ case "set": {
1566
+ const name = argv[1];
1567
+ if (!name) {
1568
+ console.error(red("Usage: antbot secret set <NAME>"));
1569
+ return 2;
1570
+ }
1571
+ const value = await ask(` Value for ${name}: `, true);
1572
+ if (!value) {
1573
+ console.error(red("A value is required."));
1574
+ return 2;
1432
1575
  }
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"}`));
1576
+ try {
1577
+ await postJson(port, "/api/secrets", { name, value });
1578
+ console.log(green(`Stored ${name} in the keychain.`));
1579
+ return 0;
1580
+ } catch (err) {
1581
+ console.error(red(err.message));
1436
1582
  return 1;
1437
1583
  }
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)}`);
1584
+ }
1585
+ case "remove": {
1586
+ const name = argv[1];
1587
+ if (!name) {
1588
+ console.error(red("Usage: antbot secret remove <NAME>"));
1589
+ return 2;
1443
1590
  }
1591
+ await deleteJson(port, `/api/secrets/${encodeURIComponent(name)}`);
1592
+ console.log(green(`Removed ${name}.`));
1444
1593
  return 0;
1445
1594
  }
1446
1595
  default:
1447
- console.error(red(`Unknown subcommand "${sub}".`));
1448
- hint();
1596
+ console.error(red("Usage: antbot secret list | set <NAME> | remove <NAME>"));
1449
1597
  return 2;
1450
1598
  }
1451
1599
  }
@@ -1453,7 +1601,7 @@ async function runConnectorCommand(argv, port) {
1453
1601
  // cli/src/updateCommand.ts
1454
1602
  import fs9 from "node:fs";
1455
1603
  import path10 from "node:path";
1456
- import readline from "node:readline/promises";
1604
+ import readline2 from "node:readline/promises";
1457
1605
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1458
1606
 
1459
1607
  // cli/src/update.ts
@@ -1628,7 +1776,7 @@ async function runUpdateCommand(opts) {
1628
1776
  const pm = detectPackageManager(here);
1629
1777
  const { cmd, args } = updateCommand(pm);
1630
1778
  if (!opts.yes) {
1631
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1779
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1632
1780
  const answer = await rl.question(`Run \`${cmd} ${args.join(" ")}\`? [y/N] `);
1633
1781
  rl.close();
1634
1782
  if (!/^y(es)?$/i.test(answer.trim())) {
@@ -1744,7 +1892,7 @@ async function runRestoreCommand(archivePath, skipConfirm) {
1744
1892
  }
1745
1893
  const paths = loadPaths();
1746
1894
  if (!skipConfirm) {
1747
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1895
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
1748
1896
  const answer = await rl.question(
1749
1897
  `This will overwrite files under ${paths.root} with the contents of ${archivePath}. Continue? [y/N] `
1750
1898
  );
@@ -1829,7 +1977,12 @@ async function main() {
1829
1977
  case "mcp":
1830
1978
  case "connector": {
1831
1979
  const cfg = loadConfig();
1832
- process.exit(await runConnectorCommand(parsed.positionals, cfg.port));
1980
+ process.exit(await runMcpCommand(parsed.positionals, cfg.port));
1981
+ return;
1982
+ }
1983
+ case "secret": {
1984
+ const cfg = loadConfig();
1985
+ process.exit(await runSecretCommand(parsed.positionals, cfg.port));
1833
1986
  return;
1834
1987
  }
1835
1988
  case "backup":