@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/server.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  SettingsSchema,
19
19
  UpdateBotRequest,
20
20
  UpdateConnectorRequest
21
- } from "./chunk-I3GHIX3G.js";
21
+ } from "./chunk-G4IIENWF.js";
22
22
  import {
23
23
  findWebDist,
24
24
  nodeLocateDeps,
@@ -38,7 +38,7 @@ import {
38
38
 
39
39
  // daemon/src/api/server.ts
40
40
  import path11 from "node:path";
41
- import { fileURLToPath as fileURLToPath2 } from "node:url";
41
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
42
42
  import { createRequire } from "node:module";
43
43
  import Fastify from "fastify";
44
44
  import cors from "@fastify/cors";
@@ -47,6 +47,7 @@ import websocket from "@fastify/websocket";
47
47
  import fastifyStatic from "@fastify/static";
48
48
 
49
49
  // daemon/src/app.ts
50
+ import fs8 from "node:fs";
50
51
  import path8 from "node:path";
51
52
 
52
53
  // daemon/src/db/db.ts
@@ -184,6 +185,19 @@ CREATE TABLE bot_connectors (
184
185
  enabled INTEGER NOT NULL DEFAULT 1,
185
186
  PRIMARY KEY (bot_id, connector_id)
186
187
  );
188
+ `
189
+ },
190
+ {
191
+ version: 3,
192
+ name: "connector-kind-and-health",
193
+ // Built-in connectors (served by the daemon) alongside custom ones, and the last verdict a
194
+ // check or a turn reached — so the screen shows a connector's real state instead of a toast
195
+ // that has already vanished.
196
+ up: `
197
+ ALTER TABLE connectors ADD COLUMN kind TEXT NOT NULL DEFAULT 'custom';
198
+ ALTER TABLE connectors ADD COLUMN last_status TEXT;
199
+ ALTER TABLE connectors ADD COLUMN last_error TEXT;
200
+ ALTER TABLE connectors ADD COLUMN checked_at INTEGER;
187
201
  `
188
202
  }
189
203
  ];
@@ -257,10 +271,10 @@ function migrate(db, opts = {}) {
257
271
  });
258
272
  try {
259
273
  run();
260
- } catch (err) {
274
+ } catch (err2) {
261
275
  throw new MigrationError(
262
276
  "MIGRATION_FAILED",
263
- `migration ${m.version} (${m.name}) failed: ${err.message}` + (backupPath ? `. The pre-migration database was saved to ${backupPath}` : "")
277
+ `migration ${m.version} (${m.name}) failed: ${err2.message}` + (backupPath ? `. The pre-migration database was saved to ${backupPath}` : "")
264
278
  );
265
279
  }
266
280
  applied.push({ version: m.version, name: m.name });
@@ -365,8 +379,12 @@ var toConnector = (r) => ({
365
379
  id: r.id,
366
380
  name: r.name,
367
381
  description: r.description,
382
+ kind: r.kind === "builtin" ? "builtin" : "custom",
368
383
  config: ConnectorConfigSchema.parse(JSON.parse(r.config_json)),
369
384
  enabled: b(r.enabled),
385
+ lastStatus: r.last_status ?? null,
386
+ lastError: r.last_error ?? null,
387
+ checkedAt: r.checked_at ?? null,
370
388
  createdAt: r.created_at
371
389
  });
372
390
  var toRoutine = (r) => ({
@@ -756,10 +774,17 @@ var Store = class {
756
774
  createConnector(c) {
757
775
  const id = newId();
758
776
  this.db.prepare(
759
- `INSERT INTO connectors (id,name,description,config_json,enabled,created_at) VALUES (?,?,?,?,?,?)`
760
- ).run(id, c.name, c.description ?? "", JSON.stringify(c.config), i(c.enabled, true), now());
777
+ `INSERT INTO connectors (id,name,description,config_json,enabled,kind,created_at) VALUES (?,?,?,?,?,?,?)`
778
+ ).run(id, c.name, c.description ?? "", JSON.stringify(c.config), i(c.enabled, true), c.kind ?? "custom", now());
761
779
  return this.getConnector(id);
762
780
  }
781
+ /** Record the latest verdict. Written by both `check` and the turn's own mount report. */
782
+ setConnectorStatus(id, status, error = null) {
783
+ this.db.prepare(`UPDATE connectors SET last_status=?, last_error=?, checked_at=? WHERE id=?`).run(status, error, now(), id);
784
+ }
785
+ setConnectorStatusByName(name, status, error = null) {
786
+ this.db.prepare(`UPDATE connectors SET last_status=?, last_error=?, checked_at=? WHERE name=?`).run(status, error, now(), name);
787
+ }
763
788
  getConnector(id) {
764
789
  const r = this.db.prepare(`SELECT * FROM connectors WHERE id=?`).get(id);
765
790
  return r ? toConnector(r) : null;
@@ -1042,7 +1067,12 @@ var BUILTIN_RULES = [
1042
1067
  { kind: "allow", toolPattern: "Glob", inputPattern: "", scopeNote: "Listing files is safe", builtin: true, enabled: true },
1043
1068
  { kind: "allow", toolPattern: "Grep", inputPattern: "", scopeNote: "Searching files is safe", builtin: true, enabled: true },
1044
1069
  { kind: "allow", toolPattern: "TodoWrite", inputPattern: "", scopeNote: "Planning scratchpad", builtin: true, enabled: true },
1045
- { kind: "allow", toolPattern: "Bash", inputPattern: "^\\s*(git\\s+(status|diff|log|show|branch)|ls|pwd|cat|head|tail|wc|echo|date|which|grep|find|rg)\\b", scopeNote: "Read-only shell inspection", builtin: true, enabled: true }
1070
+ { kind: "allow", toolPattern: "Bash", inputPattern: "^\\s*(git\\s+(status|diff|log|show|branch)|ls|pwd|cat|head|tail|wc|echo|date|which|grep|find|rg)\\b", scopeNote: "Read-only shell inspection", builtin: true, enabled: true },
1071
+ // The one place a seeded `mcp__*` rule is right: these tool names are ant-bot's own (the gmail
1072
+ // connector is served by the daemon), fixed, and fully qualified — so they cannot match a
1073
+ // third-party server's tool by alias, and they are the two Gmail actions that leave the machine.
1074
+ { kind: "require", toolPattern: "mcp__gmail__send_message", inputPattern: "", scopeNote: "Sending email", builtin: true, enabled: true },
1075
+ { kind: "require", toolPattern: "mcp__gmail__create_draft", inputPattern: "", scopeNote: "Creating an email draft", builtin: true, enabled: true }
1046
1076
  ];
1047
1077
  function seedBuiltinRules(store) {
1048
1078
  const key = (r) => `${r.kind}\0${r.toolPattern}\0${r.inputPattern}`;
@@ -1183,8 +1213,8 @@ var PermissionGateway = class {
1183
1213
  if (verdict.verdict === "deny_suggested")
1184
1214
  return this.askHuman({ ...args, reason: `Auto review flagged this: ${verdict.reason}` });
1185
1215
  return this.askHuman({ ...args, reason: verdict.reason });
1186
- } catch (err) {
1187
- log3.warn("auto review failed; falling back to human approval", err);
1216
+ } catch (err2) {
1217
+ log3.warn("auto review failed; falling back to human approval", err2);
1188
1218
  }
1189
1219
  }
1190
1220
  return this.askHuman({
@@ -1293,6 +1323,18 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
1293
1323
 
1294
1324
  // daemon/src/agent/session.ts
1295
1325
  import { query } from "@anthropic-ai/claude-agent-sdk";
1326
+
1327
+ // daemon/src/agent/runtime.ts
1328
+ var ClaudeRuntime = class {
1329
+ name = "claude";
1330
+ mountConnectors(connectors) {
1331
+ const out = {};
1332
+ for (const [name, c] of Object.entries(connectors)) out[name] = { ...c, alwaysLoad: true };
1333
+ return out;
1334
+ }
1335
+ };
1336
+
1337
+ // daemon/src/agent/session.ts
1296
1338
  var log4 = logger("agent");
1297
1339
  function resolveModel(tier) {
1298
1340
  return tier;
@@ -1305,6 +1347,7 @@ function buildEnv(settings, base = process.env) {
1305
1347
  }
1306
1348
  return env;
1307
1349
  }
1350
+ var runtime = new ClaudeRuntime();
1308
1351
  async function* runTurn(req) {
1309
1352
  const abort = req.abortController ?? new AbortController();
1310
1353
  const options = {
@@ -1316,7 +1359,14 @@ async function* runTurn(req) {
1316
1359
  includePartialMessages: true,
1317
1360
  permissionMode: "default",
1318
1361
  canUseTool: req.canUseTool,
1319
- mcpServers: req.mcpServers,
1362
+ mcpServers: {
1363
+ ...req.mcpServers ?? {},
1364
+ ...runtime.mountConnectors(req.connectors ?? {})
1365
+ },
1366
+ // ant-bot is the MCP host. The SDK mounts exactly what is passed here and nothing else — not
1367
+ // ~/.claude.json, not plugins, not claude.ai connectors. A bot's tools cannot change because
1368
+ // of something configured outside ant-bot, and swapping the runtime later swaps only this.
1369
+ strictMcpConfig: true,
1320
1370
  env: buildEnv(req.settings),
1321
1371
  // Do not inherit the user's own Claude Code project settings into bot turns.
1322
1372
  settingSources: [],
@@ -1324,23 +1374,40 @@ async function* runTurn(req) {
1324
1374
  };
1325
1375
  if (req.resumeSessionId) options.resume = req.resumeSessionId;
1326
1376
  if (req.skillPluginPath) {
1327
- options.plugins = [{ type: "local", path: req.skillPluginPath }];
1377
+ options.plugins = [{ type: "local", path: req.skillPluginPath, skipMcpDiscovery: true }];
1328
1378
  options.skills = req.enabledSkills ?? [];
1329
1379
  }
1380
+ const pending = [];
1381
+ options.onElicitation = async (request) => {
1382
+ if (request.mode === "url" && request.url) {
1383
+ pending.push({ kind: "signin", signin: { serverName: request.serverName, url: request.url } });
1384
+ return { action: "accept" };
1385
+ }
1386
+ log4.warn(`declined a ${request.mode ?? "form"} elicitation from "${request.serverName}": ${request.message}`);
1387
+ return { action: "decline" };
1388
+ };
1330
1389
  let q;
1331
1390
  try {
1332
1391
  q = query({ prompt: req.prompt, options });
1333
- } catch (err) {
1334
- yield { kind: "error", message: err instanceof Error ? err.message : String(err) };
1392
+ } catch (err2) {
1393
+ yield { kind: "error", message: err2 instanceof Error ? err2.message : String(err2) };
1335
1394
  return;
1336
1395
  }
1337
1396
  const seenToolIds = /* @__PURE__ */ new Set();
1338
1397
  try {
1339
1398
  for await (const msg of q) {
1399
+ while (pending.length) yield pending.shift();
1340
1400
  const m = msg;
1341
1401
  switch (m.type) {
1342
1402
  case "system":
1343
1403
  if (m.subtype === "init" && m.session_id) yield { kind: "session", sessionId: m.session_id };
1404
+ if (m.subtype === "elicitation_complete" && typeof m.mcp_server_name === "string") {
1405
+ try {
1406
+ await q.reconnectMcpServer(m.mcp_server_name);
1407
+ } catch (err2) {
1408
+ log4.warn(`reconnect of "${m.mcp_server_name}" after sign-in failed: ${err2.message}`);
1409
+ }
1410
+ }
1344
1411
  if (m.subtype === "init" && Array.isArray(m.mcp_servers)) {
1345
1412
  yield { kind: "mcp_status", mcpStatus: m.mcp_servers };
1346
1413
  }
@@ -1395,13 +1462,13 @@ async function* runTurn(req) {
1395
1462
  break;
1396
1463
  }
1397
1464
  }
1398
- } catch (err) {
1465
+ } catch (err2) {
1399
1466
  if (abort.signal.aborted) {
1400
1467
  yield { kind: "error", message: "Interrupted." };
1401
1468
  return;
1402
1469
  }
1403
- log4.error("turn failed", err);
1404
- yield { kind: "error", message: err instanceof Error ? err.message : String(err) };
1470
+ log4.error("turn failed", err2);
1471
+ yield { kind: "error", message: err2 instanceof Error ? err2.message : String(err2) };
1405
1472
  }
1406
1473
  }
1407
1474
 
@@ -1484,8 +1551,8 @@ var NullAutoReviewer = class {
1484
1551
  function makeAutoReviewer(getSettings, cwd) {
1485
1552
  try {
1486
1553
  return new HaikuAutoReviewer(getSettings, cwd);
1487
- } catch (err) {
1488
- log5.warn("falling back to null reviewer", err);
1554
+ } catch (err2) {
1555
+ log5.warn("falling back to null reviewer", err2);
1489
1556
  return new NullAutoReviewer();
1490
1557
  }
1491
1558
  }
@@ -1748,8 +1815,8 @@ ${lines.join("\n")}` }] };
1748
1815
  text: `Installed: ${names}.${note} A skill must still be assigned to you in Bot settings before you can invoke it \u2014 ask the human to enable it, then retry.`
1749
1816
  }]
1750
1817
  };
1751
- } catch (err) {
1752
- const e = err;
1818
+ } catch (err2) {
1819
+ const e = err2;
1753
1820
  if (e.name === "MultipleSkillsError" && Array.isArray(e.names)) {
1754
1821
  const listed = e.names.slice(0, 40).join(", ");
1755
1822
  const more = e.names.length > 40 ? `, and ${e.names.length - 40} more` : "";
@@ -1782,8 +1849,8 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1782
1849
  return { content: [{ type: "text", text: `No skill with slug "${args.slug}". Installed: ${known || "none"}.` }] };
1783
1850
  }
1784
1851
  return { content: [{ type: "text", text: `Removed "${args.slug}"${res.name ? ` (${res.name})` : ""}.` }] };
1785
- } catch (err) {
1786
- return { content: [{ type: "text", text: `Remove failed: ${err.message}` }] };
1852
+ } catch (err2) {
1853
+ return { content: [{ type: "text", text: `Remove failed: ${err2.message}` }] };
1787
1854
  }
1788
1855
  }
1789
1856
  ),
@@ -1834,13 +1901,12 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1834
1901
  isGroup
1835
1902
  });
1836
1903
  let text = "";
1837
- let ok = true;
1904
+ let ok2 = true;
1838
1905
  let errorMessage = "";
1839
1906
  const toolCards = /* @__PURE__ */ new Map();
1840
1907
  const mcpServers = { antbot: this.buildToolServer(bot, job.threadId, job.hops) };
1841
1908
  const browser = this.deps.browserTools?.(bot.id);
1842
1909
  if (browser) mcpServers.browser = browser;
1843
- if (connectors) Object.assign(mcpServers, connectors.servers);
1844
1910
  try {
1845
1911
  for await (const ev of runTurn({
1846
1912
  prompt: job.prompt,
@@ -1851,6 +1917,9 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1851
1917
  settings,
1852
1918
  abortController: abort,
1853
1919
  mcpServers,
1920
+ // Names are validated against RESERVED_CONNECTOR_NAMES, so these cannot clobber the two
1921
+ // in-process servers above. Mounted by the runtime adapter, not here.
1922
+ connectors: connectors?.servers ?? {},
1854
1923
  skillPluginPath: this.deps.skillPluginPath?.(),
1855
1924
  // Skill names come from SKILL.md frontmatter, which is what the SDK matches on.
1856
1925
  enabledSkills: botSkills.map((s) => s.name),
@@ -1888,17 +1957,17 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1888
1957
  if (ev.kind === "text" && ev.text) text += ev.text;
1889
1958
  if (ev.kind === "done") {
1890
1959
  if (ev.text && !text.trim()) text = ev.text;
1891
- ok = !ev.isError;
1960
+ ok2 = !ev.isError;
1892
1961
  }
1893
1962
  if (ev.kind === "error") {
1894
- ok = false;
1963
+ ok2 = false;
1895
1964
  errorMessage = ev.message ?? "Unknown error";
1896
1965
  }
1897
1966
  }
1898
- } catch (err) {
1899
- ok = false;
1900
- errorMessage = err instanceof Error ? err.message : String(err);
1901
- log6.error("turn crashed", err);
1967
+ } catch (err2) {
1968
+ ok2 = false;
1969
+ errorMessage = err2 instanceof Error ? err2.message : String(err2);
1970
+ log6.error("turn crashed", err2);
1902
1971
  } finally {
1903
1972
  this.running.delete(job.botId);
1904
1973
  }
@@ -1912,7 +1981,7 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1912
1981
  const interrupted = abort.signal.aborted;
1913
1982
  this.setState(job.botId, interrupted ? "interrupted" : "idle", "unread");
1914
1983
  if (interrupted) this.setState(job.botId, "idle", "unread");
1915
- job.onDone?.(final || errorMessage, ok && !interrupted);
1984
+ job.onDone?.(final || errorMessage, ok2 && !interrupted);
1916
1985
  void this.drain();
1917
1986
  }
1918
1987
  async applyEvent(ev, ctx) {
@@ -1926,6 +1995,10 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1926
1995
  // simply behaves as though the connector were never assigned. Surfacing the SDK's own
1927
1996
  // verdict is the difference between "my bot ignores my connector" and a stated reason.
1928
1997
  case "mcp_status": {
1998
+ for (const m of ev.mcpStatus ?? []) {
1999
+ if (m.name === "antbot" || m.name === "browser") continue;
2000
+ store.setConnectorStatusByName(m.name, m.status, m.error ?? null);
2001
+ }
1929
2002
  const bad = (ev.mcpStatus ?? []).filter((m) => m.status !== "connected");
1930
2003
  if (!bad.length) break;
1931
2004
  for (const m of bad) {
@@ -1941,6 +2014,16 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1941
2014
  });
1942
2015
  break;
1943
2016
  }
2017
+ // Mid-turn sign-in: the link goes into the thread as a card, where the human is looking,
2018
+ // and it persists — a toast would be gone before they came back from the browser.
2019
+ case "signin": {
2020
+ if (!ev.signin) break;
2021
+ const card = { type: "signin", serverName: ev.signin.serverName, url: ev.signin.url };
2022
+ const idx = store.appendCard(msgId, card);
2023
+ bus.publish({ type: "message.card", threadId: job.threadId, botId: bot.id, messageId: msgId, card, cardIndex: idx });
2024
+ store.setConnectorStatusByName(ev.signin.serverName, "needs-sign-in", null);
2025
+ break;
2026
+ }
1944
2027
  case "text":
1945
2028
  if (ev.text) {
1946
2029
  const cur = store.getMessage(msgId);
@@ -2009,7 +2092,7 @@ function describeMcpStatus(status) {
2009
2092
  case "needs-auth":
2010
2093
  return "needs authentication \u2014 the server rejected the credentials it was given (or was given none)";
2011
2094
  case "failed":
2012
- return "failed to start \u2014 check the command or URL with `antbot connector test`";
2095
+ return "failed to start \u2014 run `antbot mcp check <name>` to see why";
2013
2096
  case "pending":
2014
2097
  return "did not finish connecting in time";
2015
2098
  case "disabled":
@@ -2222,8 +2305,8 @@ async function discoverAuth(mcpUrl, headers = {}) {
2222
2305
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2223
2306
  });
2224
2307
  challenge = res.headers.get("www-authenticate");
2225
- } catch (err) {
2226
- throw new OAuthError(`Could not reach ${mcpUrl}: ${err.message}`);
2308
+ } catch (err2) {
2309
+ throw new OAuthError(`Could not reach ${mcpUrl}: ${err2.message}`);
2227
2310
  }
2228
2311
  const found = await firstResourceMetadata(mcpUrl, challenge);
2229
2312
  if (!found) {
@@ -2310,13 +2393,16 @@ var clientSecretName = (connectorName) => `antbot:oauth-client:${connectorName}`
2310
2393
  var redirectUri = (port) => `http://127.0.0.1:${port}/api/connectors/oauth/callback`;
2311
2394
  var LOGIN_TTL_MS = 10 * 60 * 1e3;
2312
2395
  var ConnectorAuthService = class {
2313
- constructor(secrets, port) {
2396
+ constructor(secrets, portOf) {
2314
2397
  this.secrets = secrets;
2315
- this.port = port;
2398
+ this.portOf = portOf;
2316
2399
  }
2317
2400
  secrets;
2318
- port;
2401
+ portOf;
2319
2402
  pending = /* @__PURE__ */ new Map();
2403
+ get port() {
2404
+ return this.portOf();
2405
+ }
2320
2406
  /** Has this connector been signed in? Names only — never reads a value to answer. */
2321
2407
  isAuthorized(connectorName) {
2322
2408
  return this.secrets.list().includes(tokenSecretName(connectorName));
@@ -2342,8 +2428,8 @@ var ConnectorAuthService = class {
2342
2428
  async forgetClient(connectorName) {
2343
2429
  await this.secrets.remove(clientSecretName(connectorName));
2344
2430
  }
2345
- async readClient(connectorName) {
2346
- const key = clientSecretName(connectorName);
2431
+ async readClient(clientKey) {
2432
+ const key = clientSecretName(clientKey);
2347
2433
  const found = (await this.secrets.resolve([key])).get(key);
2348
2434
  if (!found) return null;
2349
2435
  try {
@@ -2364,49 +2450,76 @@ var ConnectorAuthService = class {
2364
2450
  throw new OAuthError("Sign-in applies to http and sse connectors; a stdio server takes its credentials in env.");
2365
2451
  }
2366
2452
  const discovery = await discoverAuth(connector.config.url);
2453
+ const authorizeUrl = await this.beginLoginWith(
2454
+ {
2455
+ connectorId: connector.id,
2456
+ connectorName: connector.name,
2457
+ clientKey: connector.name,
2458
+ authorizationEndpoint: discovery.authServer.authorizationEndpoint,
2459
+ tokenEndpoint: discovery.authServer.tokenEndpoint,
2460
+ registrationEndpoint: discovery.authServer.registrationEndpoint,
2461
+ resource: discovery.resource.resource,
2462
+ scopes: opts.scopes?.length ? opts.scopes : discovery.resource.scopesSupported,
2463
+ // Without these Google issues no refresh token, and the connector dies in an hour. Harmless
2464
+ // for providers that ignore them.
2465
+ extras: { access_type: "offline", prompt: "consent" }
2466
+ },
2467
+ opts
2468
+ );
2469
+ return { authorizeUrl, discovery };
2470
+ }
2471
+ /**
2472
+ * Begin a sign-in against a known authorization server. Used directly by built-in connectors,
2473
+ * whose provider endpoints are fixed and whose client credentials are shared under one
2474
+ * `clientKey` — one Google client serves Gmail, Calendar and Drive.
2475
+ */
2476
+ async beginLoginWith(target, opts = {}) {
2367
2477
  const redirect = redirectUri(this.port);
2368
- const remembered = await this.readClient(connector.name);
2478
+ const remembered = await this.readClient(target.clientKey);
2369
2479
  let clientId = opts.clientId ?? remembered?.clientId;
2370
2480
  let clientSecret = opts.clientSecret ?? (opts.clientId ? void 0 : remembered?.clientSecret);
2371
- if (!clientId && discovery.authServer.registrationEndpoint) {
2372
- const registered = await registerClient(discovery.authServer.registrationEndpoint, redirect);
2481
+ if (!clientId && target.registrationEndpoint) {
2482
+ const registered = await registerClient(target.registrationEndpoint, redirect);
2373
2483
  clientId = registered?.clientId;
2374
2484
  clientSecret = registered?.clientSecret;
2375
2485
  }
2376
2486
  if (!clientId) {
2487
+ const who = target.providerName ?? new URL(target.authorizationEndpoint).host;
2377
2488
  throw new OAuthError(
2378
- `${new URL(discovery.authServer.authorizationEndpoint).host} does not support automatic app registration, so it needs a client ID you create yourself. Register one with that provider, add "${redirect}" as an authorised redirect URI, and pass the client ID with --client-id.`
2489
+ `${who} does not support automatic app registration, so it needs a client ID you create yourself. Register one with that provider, add "${redirect}" as an authorised redirect URI, and pass the client ID with --client-id.`
2379
2490
  );
2380
2491
  }
2381
2492
  if (opts.clientId || opts.clientSecret || !remembered) {
2382
- await this.secrets.set(clientSecretName(connector.name), JSON.stringify({ clientId, clientSecret }));
2493
+ await this.secrets.set(clientSecretName(target.clientKey), JSON.stringify({ clientId, clientSecret }));
2383
2494
  }
2384
2495
  const pkce = createPkce();
2385
2496
  const state = crypto2.randomBytes(16).toString("base64url");
2386
2497
  this.pending.set(state, {
2387
- connectorId: connector.id,
2388
- connectorName: connector.name,
2498
+ connectorId: target.connectorId,
2499
+ connectorName: target.connectorName,
2389
2500
  verifier: pkce.verifier,
2390
2501
  clientId,
2391
2502
  clientSecret,
2392
- tokenEndpoint: discovery.authServer.tokenEndpoint,
2393
- resource: discovery.resource.resource,
2503
+ tokenEndpoint: target.tokenEndpoint,
2504
+ resource: target.resource,
2394
2505
  redirectUri: redirect,
2395
2506
  startedAt: Date.now()
2396
2507
  });
2397
2508
  this.sweep();
2398
- const authorizeUrl = buildAuthorizeUrl({
2399
- authorizationEndpoint: discovery.authServer.authorizationEndpoint,
2509
+ return buildAuthorizeUrl({
2510
+ authorizationEndpoint: target.authorizationEndpoint,
2400
2511
  clientId,
2401
2512
  redirectUri: redirect,
2402
- scopes: opts.scopes?.length ? opts.scopes : discovery.resource.scopesSupported,
2513
+ scopes: target.scopes,
2403
2514
  state,
2404
2515
  challenge: pkce.challenge,
2405
- resource: discovery.resource.resource,
2406
- // Without these Google issues no refresh token, and the connector dies in an hour.
2407
- extra: { access_type: "offline", prompt: "consent" }
2516
+ resource: target.resource,
2517
+ extra: target.extras
2408
2518
  });
2409
- return { authorizeUrl, discovery };
2519
+ }
2520
+ /** Whether client credentials are on file for a key (a connector name or a provider key). */
2521
+ hasClient(clientKey) {
2522
+ return this.secrets.list().includes(clientSecretName(clientKey));
2410
2523
  }
2411
2524
  /** Finish a sign-in from the redirect. Returns the connector that was authorised. */
2412
2525
  async completeLogin(state, code) {
@@ -2424,14 +2537,14 @@ var ConnectorAuthService = class {
2424
2537
  redirectUri: p.redirectUri,
2425
2538
  resource: p.resource
2426
2539
  });
2427
- } catch (err) {
2428
- const message = err.message;
2540
+ } catch (err2) {
2541
+ const message = err2.message;
2429
2542
  if (/client_secret/i.test(message)) {
2430
2543
  throw new OAuthError(
2431
2544
  `This provider requires a client secret as well as a client ID. Add the secret from the same OAuth client (in Google's console: the client's "Client secret") and sign in again.`
2432
2545
  );
2433
2546
  }
2434
- throw err;
2547
+ throw err2;
2435
2548
  }
2436
2549
  await this.write(p.connectorName, tokens);
2437
2550
  log7.info(`connector "${p.connectorName}" signed in`);
@@ -2449,8 +2562,8 @@ var ConnectorAuthService = class {
2449
2562
  try {
2450
2563
  tokens = await refreshTokens(tokens);
2451
2564
  await this.write(connectorName, tokens);
2452
- } catch (err) {
2453
- log7.warn(`could not refresh tokens for "${connectorName}": ${err.message}`);
2565
+ } catch (err2) {
2566
+ log7.warn(`could not refresh tokens for "${connectorName}": ${err2.message}`);
2454
2567
  return null;
2455
2568
  }
2456
2569
  }
@@ -2462,6 +2575,544 @@ var ConnectorAuthService = class {
2462
2575
  }
2463
2576
  };
2464
2577
 
2578
+ // daemon/src/connectors/builtin/service.ts
2579
+ import crypto3 from "node:crypto";
2580
+
2581
+ // daemon/src/connectors/builtin/gmail.ts
2582
+ import { z as z2 } from "zod";
2583
+
2584
+ // daemon/src/connectors/builtin/mcpServer.ts
2585
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
2586
+ var RPC = {
2587
+ PARSE_ERROR: -32700,
2588
+ INVALID_REQUEST: -32600,
2589
+ METHOD_NOT_FOUND: -32601,
2590
+ INVALID_PARAMS: -32602
2591
+ };
2592
+ var err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
2593
+ var ok = (id, result) => ({ jsonrpc: "2.0", id, result });
2594
+ var toolError = (text) => ({ content: [{ type: "text", text }], isError: true });
2595
+ var toolText = (text) => ({ content: [{ type: "text", text }] });
2596
+ async function handleMcpRequest(body, server, ctx) {
2597
+ const req = body;
2598
+ if (!req || typeof req !== "object" || req.jsonrpc !== "2.0" || typeof req.method !== "string") {
2599
+ return err(null, RPC.INVALID_REQUEST, "Expected a JSON-RPC 2.0 request");
2600
+ }
2601
+ const id = req.id ?? null;
2602
+ if (req.id === void 0) return null;
2603
+ switch (req.method) {
2604
+ case "initialize": {
2605
+ const asked = String(req.params?.protocolVersion ?? MCP_PROTOCOL_VERSION);
2606
+ return ok(id, {
2607
+ // Echo the client's version when it is one we can serve; the surface is small enough that
2608
+ // every revision since 2024-11-05 is compatible for these three methods.
2609
+ protocolVersion: asked,
2610
+ capabilities: { tools: {} },
2611
+ serverInfo: { name: server.name, version: server.version }
2612
+ });
2613
+ }
2614
+ case "ping":
2615
+ return ok(id, {});
2616
+ case "tools/list":
2617
+ return ok(id, {
2618
+ tools: server.tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
2619
+ });
2620
+ case "tools/call": {
2621
+ const name = String(req.params?.name ?? "");
2622
+ const tool2 = server.tools.find((t) => t.name === name);
2623
+ if (!tool2) return err(id, RPC.INVALID_PARAMS, `Unknown tool: ${name}`);
2624
+ const parsed = tool2.parse.safeParse(req.params?.arguments ?? {});
2625
+ if (!parsed.success) {
2626
+ return err(id, RPC.INVALID_PARAMS, `Invalid arguments for ${name}: ${parsed.error.issues[0]?.message ?? "invalid"}`);
2627
+ }
2628
+ let context;
2629
+ try {
2630
+ context = await ctx();
2631
+ } catch (e) {
2632
+ return ok(id, toolError(e.message));
2633
+ }
2634
+ try {
2635
+ return ok(id, await tool2.handler(parsed.data, context));
2636
+ } catch (e) {
2637
+ return ok(id, toolError(`${name} failed: ${e.message}`));
2638
+ }
2639
+ }
2640
+ default:
2641
+ return err(id, RPC.METHOD_NOT_FOUND, `Method not supported: ${req.method}`);
2642
+ }
2643
+ }
2644
+
2645
+ // daemon/src/connectors/builtin/gmail.ts
2646
+ var API = "https://gmail.googleapis.com/gmail/v1/users/me";
2647
+ async function gmail(fetchFn, ctx, path12, init = {}) {
2648
+ const res = await fetchFn(`${API}${path12}`, {
2649
+ ...init,
2650
+ headers: { Authorization: `Bearer ${ctx.accessToken}`, "content-type": "application/json", ...init.headers ?? {} }
2651
+ });
2652
+ const text = await res.text();
2653
+ let body = null;
2654
+ try {
2655
+ body = text ? JSON.parse(text) : null;
2656
+ } catch {
2657
+ }
2658
+ if (!res.ok) {
2659
+ const msg = body?.error?.message ?? `HTTP ${res.status}`;
2660
+ const hint = res.status === 401 || res.status === 403 ? " \u2014 sign in to the gmail connector again" : "";
2661
+ return { ok: false, error: `Gmail: ${msg}${hint}` };
2662
+ }
2663
+ return { ok: true, body };
2664
+ }
2665
+ function summarizeMessage(m) {
2666
+ const headers = {};
2667
+ for (const h of m?.payload?.headers ?? []) {
2668
+ const k = String(h.name).toLowerCase();
2669
+ if (["from", "to", "cc", "subject", "date"].includes(k)) headers[k] = String(h.value);
2670
+ }
2671
+ return {
2672
+ id: m?.id,
2673
+ threadId: m?.threadId,
2674
+ labelIds: m?.labelIds ?? [],
2675
+ snippet: m?.snippet ?? "",
2676
+ ...headers,
2677
+ body: extractText(m?.payload) ?? ""
2678
+ };
2679
+ }
2680
+ function extractText(payload) {
2681
+ if (!payload) return null;
2682
+ const decode = (data) => Buffer.from(data.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
2683
+ if (payload.mimeType === "text/plain" && payload.body?.data) return decode(payload.body.data);
2684
+ if (Array.isArray(payload.parts)) {
2685
+ for (const p of payload.parts) {
2686
+ const t = extractText(p);
2687
+ if (t) return t;
2688
+ }
2689
+ }
2690
+ if (payload.mimeType === "text/html" && payload.body?.data) {
2691
+ return decode(payload.body.data).replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
2692
+ }
2693
+ return null;
2694
+ }
2695
+ function buildRawMessage(m) {
2696
+ const lines = [
2697
+ `To: ${m.to}`,
2698
+ ...m.cc ? [`Cc: ${m.cc}`] : [],
2699
+ `Subject: ${m.subject}`,
2700
+ ...m.inReplyTo ? [`In-Reply-To: ${m.inReplyTo}`, `References: ${m.inReplyTo}`] : [],
2701
+ "Content-Type: text/plain; charset=utf-8",
2702
+ "MIME-Version: 1.0",
2703
+ "",
2704
+ m.body
2705
+ ];
2706
+ return Buffer.from(lines.join("\r\n"), "utf8").toString("base64url");
2707
+ }
2708
+ var json = (v) => toolText(JSON.stringify(v, null, 2));
2709
+ function gmailTools(fetchFn = fetch) {
2710
+ return [
2711
+ {
2712
+ name: "search_threads",
2713
+ description: 'Search the mailbox with Gmail query syntax (e.g. "is:unread", "from:alice newer_than:7d"). Returns thread ids with a snippet of the latest message.',
2714
+ inputSchema: {
2715
+ type: "object",
2716
+ properties: { query: { type: "string" }, maxResults: { type: "integer", minimum: 1, maximum: 50 } },
2717
+ required: ["query"]
2718
+ },
2719
+ parse: z2.object({ query: z2.string().min(1), maxResults: z2.number().int().min(1).max(50).default(10) }),
2720
+ handler: async (a, ctx) => {
2721
+ const r = await gmail(fetchFn, ctx, `/threads?q=${encodeURIComponent(a.query)}&maxResults=${a.maxResults}`);
2722
+ if (!r.ok) return toolError(r.error);
2723
+ return json({ threads: (r.body.threads ?? []).map((t) => ({ id: t.id, snippet: t.snippet })), estimate: r.body.resultSizeEstimate });
2724
+ }
2725
+ },
2726
+ {
2727
+ name: "get_thread",
2728
+ description: "Read every message in a thread: from, to, subject, date and a plain-text body.",
2729
+ inputSchema: { type: "object", properties: { threadId: { type: "string" } }, required: ["threadId"] },
2730
+ parse: z2.object({ threadId: z2.string().min(1) }),
2731
+ handler: async (a, ctx) => {
2732
+ const r = await gmail(fetchFn, ctx, `/threads/${encodeURIComponent(a.threadId)}?format=full`);
2733
+ if (!r.ok) return toolError(r.error);
2734
+ return json({ id: r.body.id, messages: (r.body.messages ?? []).map(summarizeMessage) });
2735
+ }
2736
+ },
2737
+ {
2738
+ name: "get_message",
2739
+ description: "Read one message by id.",
2740
+ inputSchema: { type: "object", properties: { messageId: { type: "string" } }, required: ["messageId"] },
2741
+ parse: z2.object({ messageId: z2.string().min(1) }),
2742
+ handler: async (a, ctx) => {
2743
+ const r = await gmail(fetchFn, ctx, `/messages/${encodeURIComponent(a.messageId)}?format=full`);
2744
+ if (!r.ok) return toolError(r.error);
2745
+ return json(summarizeMessage(r.body));
2746
+ }
2747
+ },
2748
+ {
2749
+ name: "list_labels",
2750
+ description: "List the mailbox labels (INBOX, SENT, user labels\u2026) with their ids.",
2751
+ inputSchema: { type: "object", properties: {} },
2752
+ parse: z2.object({}),
2753
+ handler: async (_a, ctx) => {
2754
+ const r = await gmail(fetchFn, ctx, "/labels");
2755
+ if (!r.ok) return toolError(r.error);
2756
+ return json({ labels: (r.body.labels ?? []).map((l) => ({ id: l.id, name: l.name, type: l.type })) });
2757
+ }
2758
+ },
2759
+ {
2760
+ name: "create_draft",
2761
+ description: "Create a draft email. Nothing is sent. Set inReplyTo to a message id to draft a reply in that thread.",
2762
+ inputSchema: {
2763
+ type: "object",
2764
+ properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, cc: { type: "string" }, inReplyTo: { type: "string" } },
2765
+ required: ["to", "subject", "body"]
2766
+ },
2767
+ parse: z2.object({ to: z2.string().min(1), subject: z2.string(), body: z2.string(), cc: z2.string().optional(), inReplyTo: z2.string().optional() }),
2768
+ handler: async (a, ctx) => {
2769
+ const r = await gmail(fetchFn, ctx, "/drafts", { method: "POST", body: JSON.stringify({ message: { raw: buildRawMessage(a) } }) });
2770
+ if (!r.ok) return toolError(r.error);
2771
+ return json({ draftId: r.body.id, messageId: r.body.message?.id });
2772
+ }
2773
+ },
2774
+ {
2775
+ name: "send_message",
2776
+ description: "Send an email immediately. This is consequential and asks the human for approval.",
2777
+ inputSchema: {
2778
+ type: "object",
2779
+ properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, cc: { type: "string" }, inReplyTo: { type: "string" } },
2780
+ required: ["to", "subject", "body"]
2781
+ },
2782
+ parse: z2.object({ to: z2.string().min(1), subject: z2.string(), body: z2.string(), cc: z2.string().optional(), inReplyTo: z2.string().optional() }),
2783
+ handler: async (a, ctx) => {
2784
+ const r = await gmail(fetchFn, ctx, "/messages/send", { method: "POST", body: JSON.stringify({ raw: buildRawMessage(a) }) });
2785
+ if (!r.ok) return toolError(r.error);
2786
+ return json({ sent: true, messageId: r.body.id, threadId: r.body.threadId });
2787
+ }
2788
+ }
2789
+ ];
2790
+ }
2791
+
2792
+ // daemon/src/connectors/builtin/catalog.ts
2793
+ var GOOGLE = {
2794
+ key: "google",
2795
+ displayName: "Google",
2796
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
2797
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
2798
+ authorizeExtras: { access_type: "offline", prompt: "consent" },
2799
+ dynamicRegistration: false,
2800
+ setupSteps: [
2801
+ "Open console.cloud.google.com \u2192 APIs & Services \u2192 Credentials \u2192 Create credentials \u2192 OAuth client ID.",
2802
+ 'Application type: Web application. Under "Authorised redirect URIs" add exactly: {redirectUri}',
2803
+ "Enable the Gmail API for the project (APIs & Services \u2192 Library).",
2804
+ "Copy the Client ID and Client secret it shows you. One client works for every Google connector."
2805
+ ]
2806
+ };
2807
+ var BUILTIN_CATALOG = {
2808
+ gmail: {
2809
+ name: "gmail",
2810
+ displayName: "Gmail",
2811
+ description: "Read, search and draft email in the signed-in Gmail account.",
2812
+ provider: GOOGLE,
2813
+ // modify covers read + draft + send; asking for less means a second consent screen later.
2814
+ scopes: ["https://www.googleapis.com/auth/gmail.modify"],
2815
+ tools: gmailTools
2816
+ }
2817
+ };
2818
+ function builtinAlternativeFor(authorizationHost) {
2819
+ for (const [name, def] of Object.entries(BUILTIN_CATALOG)) {
2820
+ if (new URL(def.provider.authorizationEndpoint).host === authorizationHost && !def.provider.dynamicRegistration) return name;
2821
+ }
2822
+ return void 0;
2823
+ }
2824
+
2825
+ // daemon/src/connectors/builtin/service.ts
2826
+ var BuiltinService = class {
2827
+ constructor(auth, portOf, version) {
2828
+ this.auth = auth;
2829
+ this.portOf = portOf;
2830
+ this.version = version;
2831
+ }
2832
+ auth;
2833
+ portOf;
2834
+ version;
2835
+ /** Rotates every boot. Checked on every `/mcp/<name>` request. */
2836
+ bearer = crypto3.randomBytes(24).toString("base64url");
2837
+ get port() {
2838
+ return this.portOf();
2839
+ }
2840
+ get(name) {
2841
+ return BUILTIN_CATALOG[name];
2842
+ }
2843
+ /** The config a built-in connector's row stores: the daemon's own endpoint, nothing secret. */
2844
+ rowConfig(name) {
2845
+ return { transport: "http", url: `http://127.0.0.1:${this.port}/mcp/${name}`, headers: {} };
2846
+ }
2847
+ /** What actually gets mounted: the row's config plus this boot's bearer. */
2848
+ mountConfig(connector) {
2849
+ return {
2850
+ type: "http",
2851
+ url: `http://127.0.0.1:${this.port}/mcp/${connector.name}`,
2852
+ headers: { Authorization: `Bearer ${this.bearer}` }
2853
+ };
2854
+ }
2855
+ authorized(name) {
2856
+ return this.auth?.isAuthorized(name) ?? false;
2857
+ }
2858
+ /** Constant-time compare so the bearer cannot be guessed a byte at a time. */
2859
+ checkBearer(header) {
2860
+ const given = (header ?? "").replace(/^Bearer\s+/i, "");
2861
+ const a = Buffer.from(given);
2862
+ const b2 = Buffer.from(this.bearer);
2863
+ return a.length === b2.length && crypto3.timingSafeEqual(a, b2);
2864
+ }
2865
+ /** Serve one MCP request for a built-in connector. */
2866
+ async handle(name, body) {
2867
+ const def = this.get(name);
2868
+ if (!def) return { jsonrpc: "2.0", id: null, error: { code: -32601, message: `No built-in connector named ${name}` } };
2869
+ return handleMcpRequest(body, { name: def.name, version: this.version, tools: def.tools() }, async () => {
2870
+ const hdr = await this.auth?.authHeader(name);
2871
+ if (!hdr) {
2872
+ throw new Error(
2873
+ `${def.displayName} is not signed in. Sign in on the Connectors screen or with \`antbot mcp login ${name}\`.`
2874
+ );
2875
+ }
2876
+ return { accessToken: hdr.Authorization.replace(/^Bearer\s+/, "") };
2877
+ });
2878
+ }
2879
+ /** Start the provider sign-in for a built-in connector. Returns the URL to open. */
2880
+ async beginLogin(connector, opts = {}) {
2881
+ const def = this.get(connector.name);
2882
+ if (!def) throw new Error(`No built-in connector named ${connector.name}`);
2883
+ if (!this.auth) throw new Error("Secrets backend unavailable, so a sign-in cannot be stored.");
2884
+ const p = def.provider;
2885
+ return this.auth.beginLoginWith(
2886
+ {
2887
+ connectorId: connector.id,
2888
+ connectorName: connector.name,
2889
+ clientKey: p.key,
2890
+ authorizationEndpoint: p.authorizationEndpoint,
2891
+ tokenEndpoint: p.tokenEndpoint,
2892
+ scopes: def.scopes,
2893
+ extras: p.authorizeExtras,
2894
+ providerName: p.displayName
2895
+ },
2896
+ opts
2897
+ );
2898
+ }
2899
+ };
2900
+
2901
+ // daemon/src/bots/mcpProbe.ts
2902
+ import { spawn } from "node:child_process";
2903
+ var log8 = logger("mcp-probe");
2904
+ var PROTOCOL_VERSION = "2025-06-18";
2905
+ var DEFAULT_TIMEOUT_MS = 1e4;
2906
+ var MAX_DESCRIPTION = 200;
2907
+ function parseToolsResult(result) {
2908
+ const tools = result?.tools;
2909
+ if (!Array.isArray(tools)) return [];
2910
+ return tools.filter((t) => typeof t === "object" && t !== null).map((t) => ({
2911
+ name: String(t.name ?? ""),
2912
+ description: String(t.description ?? "").slice(0, MAX_DESCRIPTION)
2913
+ })).filter((t) => t.name.length > 0);
2914
+ }
2915
+ var rpc = (id, method, params) => `${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
2916
+ `;
2917
+ var notify = (method) => `${JSON.stringify({ jsonrpc: "2.0", method })}
2918
+ `;
2919
+ var failed = (error) => ({ ok: false, tools: [], error });
2920
+ async function probeStdio(cfg, timeoutMs) {
2921
+ return new Promise((resolve) => {
2922
+ let child;
2923
+ try {
2924
+ child = spawn(cfg.command, cfg.args ?? [], {
2925
+ // The server's own env plus the connector's — a connector that needs PATH still gets it.
2926
+ env: { ...process.env, ...cfg.env ?? {} },
2927
+ stdio: ["pipe", "pipe", "pipe"]
2928
+ });
2929
+ } catch (err2) {
2930
+ return resolve(failed(err2.message));
2931
+ }
2932
+ let settled = false;
2933
+ let stderr = "";
2934
+ let buffer = "";
2935
+ const finish = (r) => {
2936
+ if (settled) return;
2937
+ settled = true;
2938
+ clearTimeout(timer);
2939
+ try {
2940
+ child.kill("SIGKILL");
2941
+ } catch {
2942
+ }
2943
+ resolve(r);
2944
+ };
2945
+ const timer = setTimeout(
2946
+ () => finish(failed(`timed out after ${timeoutMs}ms${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`)),
2947
+ timeoutMs
2948
+ );
2949
+ child.on("error", (err2) => finish(failed(err2.message)));
2950
+ child.on(
2951
+ "exit",
2952
+ (code) => finish(failed(`server exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`))
2953
+ );
2954
+ child.stderr?.on("data", (d) => {
2955
+ stderr += d.toString();
2956
+ });
2957
+ child.stdout?.on("data", (d) => {
2958
+ buffer += d.toString();
2959
+ const lines = buffer.split("\n");
2960
+ buffer = lines.pop() ?? "";
2961
+ for (const line of lines) {
2962
+ if (!line.trim()) continue;
2963
+ let msg;
2964
+ try {
2965
+ msg = JSON.parse(line);
2966
+ } catch {
2967
+ continue;
2968
+ }
2969
+ if (msg.id === 1) {
2970
+ try {
2971
+ child.stdin?.write(notify("notifications/initialized"));
2972
+ child.stdin?.write(rpc(2, "tools/list"));
2973
+ } catch (err2) {
2974
+ finish(failed(err2.message));
2975
+ }
2976
+ } else if (msg.id === 2) {
2977
+ if (msg.error) return finish(failed(JSON.stringify(msg.error).slice(0, 200)));
2978
+ finish({ ok: true, tools: parseToolsResult(msg.result) });
2979
+ }
2980
+ }
2981
+ });
2982
+ try {
2983
+ child.stdin?.write(
2984
+ rpc(1, "initialize", {
2985
+ protocolVersion: PROTOCOL_VERSION,
2986
+ capabilities: {},
2987
+ clientInfo: { name: "ant-bot", version: "1.0.0" }
2988
+ })
2989
+ );
2990
+ } catch (err2) {
2991
+ finish(failed(err2.message));
2992
+ }
2993
+ });
2994
+ }
2995
+ async function probeHttp(cfg, timeoutMs) {
2996
+ const ac = new AbortController();
2997
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
2998
+ const base = {
2999
+ "content-type": "application/json",
3000
+ accept: "application/json, text/event-stream",
3001
+ ...cfg.headers ?? {}
3002
+ };
3003
+ const readBody = async (res) => {
3004
+ const text = await res.text();
3005
+ const line = text.split("\n").find((l) => l.startsWith("data:"));
3006
+ try {
3007
+ return JSON.parse(line ? line.slice(5).trim() : text);
3008
+ } catch {
3009
+ return null;
3010
+ }
3011
+ };
3012
+ try {
3013
+ const initRes = await fetch(cfg.url, {
3014
+ method: "POST",
3015
+ signal: ac.signal,
3016
+ headers: base,
3017
+ body: rpc(1, "initialize", {
3018
+ protocolVersion: PROTOCOL_VERSION,
3019
+ capabilities: {},
3020
+ clientInfo: { name: "ant-bot", version: "1.0.0" }
3021
+ })
3022
+ });
3023
+ if (!initRes.ok) return failed(`initialize returned HTTP ${initRes.status}`);
3024
+ const session = initRes.headers.get("mcp-session-id");
3025
+ const withSession = session ? { ...base, "mcp-session-id": session } : base;
3026
+ await readBody(initRes);
3027
+ await fetch(cfg.url, { method: "POST", signal: ac.signal, headers: withSession, body: notify("notifications/initialized") }).catch(() => void 0);
3028
+ const listRes = await fetch(cfg.url, {
3029
+ method: "POST",
3030
+ signal: ac.signal,
3031
+ headers: withSession,
3032
+ body: rpc(2, "tools/list")
3033
+ });
3034
+ if (!listRes.ok) return failed(`tools/list returned HTTP ${listRes.status}`);
3035
+ const body = await readBody(listRes);
3036
+ if (body?.error) return failed(JSON.stringify(body.error).slice(0, 200));
3037
+ return { ok: true, tools: parseToolsResult(body?.result) };
3038
+ } catch (err2) {
3039
+ const e = err2;
3040
+ return failed(e.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message);
3041
+ } finally {
3042
+ clearTimeout(timer);
3043
+ }
3044
+ }
3045
+ async function probeConnector(config, opts = {}) {
3046
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3047
+ const type = config.type;
3048
+ try {
3049
+ if (type === "stdio") {
3050
+ return await probeStdio(config, timeoutMs);
3051
+ }
3052
+ if (type === "http") {
3053
+ return await probeHttp(config, timeoutMs);
3054
+ }
3055
+ if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
3056
+ return failed(`unknown transport: ${String(type)}`);
3057
+ } catch (err2) {
3058
+ log8.warn("probe threw", err2);
3059
+ return failed(err2.message);
3060
+ }
3061
+ }
3062
+
3063
+ // daemon/src/connectors/check.ts
3064
+ function decideCheck(signals) {
3065
+ if (signals.builtinProvider) {
3066
+ return signals.builtinSignedIn ? { status: "ready", tools: signals.probe?.tools ?? [], provider: signals.builtinProvider.name } : {
3067
+ status: "needs-sign-in",
3068
+ selfRegistration: signals.builtinProvider.dynamicRegistration,
3069
+ provider: signals.builtinProvider.name,
3070
+ tools: signals.probe?.tools ?? []
3071
+ };
3072
+ }
3073
+ if (signals.missingSecrets.length) {
3074
+ return { status: "needs-credential", tools: [], detail: `missing secret(s): ${signals.missingSecrets.join(", ")}` };
3075
+ }
3076
+ if (signals.challenge === "auth") {
3077
+ const as = signals.discovery?.authServer;
3078
+ const host = as ? new URL(as.authorizationEndpoint).host : void 0;
3079
+ const alternative = host ? builtinAlternativeFor(host) : void 0;
3080
+ return {
3081
+ status: "needs-sign-in",
3082
+ selfRegistration: Boolean(as?.registrationEndpoint),
3083
+ provider: host,
3084
+ tools: signals.probe?.tools ?? [],
3085
+ ...alternative ? {
3086
+ alternative,
3087
+ detail: `${host} does not accept third-party MCP clients here, so a sign-in would not help. Use the built-in instead: antbot mcp add ${alternative}`
3088
+ } : {}
3089
+ };
3090
+ }
3091
+ if (signals.challenge === "unreachable" || signals.probe && !signals.probe.ok) {
3092
+ return { status: "unreachable", tools: [], detail: signals.probe?.error };
3093
+ }
3094
+ return { status: "ready", tools: signals.probe?.tools ?? [] };
3095
+ }
3096
+ async function gatherCustomSignals(connector, mounted, missingSecrets) {
3097
+ if (missingSecrets.length || !mounted) return { probe: null, challenge: "none", missingSecrets };
3098
+ const probe = await probeConnector(mounted, { timeoutMs: 8e3 });
3099
+ if (mounted.type === "stdio") return { probe, challenge: probe.ok ? "none" : "unreachable", missingSecrets };
3100
+ if (!probe.ok) return { probe, challenge: "unreachable", missingSecrets };
3101
+ try {
3102
+ const discovery = await discoverAuth(mounted.url, mounted.headers);
3103
+ return { probe, challenge: "auth", discovery, missingSecrets };
3104
+ } catch (err2) {
3105
+ const msg = err2.message;
3106
+ if (err2 instanceof OAuthError && /did not advertise an authorization server/.test(msg)) {
3107
+ return { probe, challenge: "auth", discovery: null, missingSecrets };
3108
+ }
3109
+ return { probe, challenge: "none", missingSecrets };
3110
+ }
3111
+ }
3112
+
3113
+ // daemon/src/app.ts
3114
+ import { fileURLToPath } from "node:url";
3115
+
2465
3116
  // daemon/src/config/config.ts
2466
3117
  import fs6 from "node:fs";
2467
3118
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -2538,11 +3189,11 @@ function writeConfig(cfg) {
2538
3189
  // daemon/src/permissions/secrets.ts
2539
3190
  import fs7 from "node:fs";
2540
3191
  import path7 from "node:path";
2541
- import crypto3 from "node:crypto";
3192
+ import crypto4 from "node:crypto";
2542
3193
  import { execFile } from "node:child_process";
2543
3194
  import { promisify } from "node:util";
2544
3195
  var exec = promisify(execFile);
2545
- var log8 = logger("secrets");
3196
+ var log9 = logger("secrets");
2546
3197
  var SERVICE = "ant-bot";
2547
3198
  var SecretToolBackend = class {
2548
3199
  name = "libsecret (secret-tool)";
@@ -2551,7 +3202,7 @@ var SecretToolBackend = class {
2551
3202
  const p = execFile(
2552
3203
  "secret-tool",
2553
3204
  ["store", "--label", `${SERVICE}: ${key}`, "service", SERVICE, "account", key],
2554
- (err) => err ? reject(err) : resolve()
3205
+ (err2) => err2 ? reject(err2) : resolve()
2555
3206
  );
2556
3207
  p.stdin?.end(value);
2557
3208
  });
@@ -2609,7 +3260,7 @@ var EncryptedFileBackend = class {
2609
3260
  key() {
2610
3261
  if (!fs7.existsSync(this.keyFile)) {
2611
3262
  fs7.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
2612
- fs7.writeFileSync(this.keyFile, crypto3.randomBytes(32), { mode: 384 });
3263
+ fs7.writeFileSync(this.keyFile, crypto4.randomBytes(32), { mode: 384 });
2613
3264
  }
2614
3265
  return fs7.readFileSync(this.keyFile);
2615
3266
  }
@@ -2620,7 +3271,7 @@ var EncryptedFileBackend = class {
2620
3271
  const key = this.key();
2621
3272
  const out = {};
2622
3273
  for (const [k, v] of Object.entries(raw)) {
2623
- const d = crypto3.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
3274
+ const d = crypto4.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
2624
3275
  d.setAuthTag(Buffer.from(v.tag, "base64"));
2625
3276
  out[k] = Buffer.concat([d.update(Buffer.from(v.data, "base64")), d.final()]).toString("utf8");
2626
3277
  }
@@ -2633,8 +3284,8 @@ var EncryptedFileBackend = class {
2633
3284
  const key = this.key();
2634
3285
  const out = {};
2635
3286
  for (const [k, v] of Object.entries(values)) {
2636
- const iv = crypto3.randomBytes(12);
2637
- const c = crypto3.createCipheriv("aes-256-gcm", key, iv);
3287
+ const iv = crypto4.randomBytes(12);
3288
+ const c = crypto4.createCipheriv("aes-256-gcm", key, iv);
2638
3289
  const data = Buffer.concat([c.update(v, "utf8"), c.final()]);
2639
3290
  out[k] = { iv: iv.toString("base64"), tag: c.getAuthTag().toString("base64"), data: data.toString("base64") };
2640
3291
  }
@@ -2663,13 +3314,13 @@ async function pickBackend(fallbackFile) {
2663
3314
  try {
2664
3315
  await exec(bin, args);
2665
3316
  return true;
2666
- } catch (err) {
2667
- return err.code !== "ENOENT";
3317
+ } catch (err2) {
3318
+ return err2.code !== "ENOENT";
2668
3319
  }
2669
3320
  };
2670
3321
  if (process.platform === "darwin" && await has("security", ["-h"])) return new MacKeychainBackend();
2671
3322
  if (process.platform === "linux" && await has("secret-tool", ["--version"])) return new SecretToolBackend();
2672
- log8.warn("no system keychain available; using the encrypted-file fallback");
3323
+ log9.warn("no system keychain available; using the encrypted-file fallback");
2673
3324
  return new EncryptedFileBackend(fallbackFile);
2674
3325
  }
2675
3326
  var SecretsService = class {
@@ -2741,12 +3392,12 @@ var SecretsService = class {
2741
3392
  };
2742
3393
 
2743
3394
  // daemon/src/app.ts
2744
- var log9 = logger("app");
3395
+ var log10 = logger("app");
2745
3396
  async function optionalImport(name, load) {
2746
3397
  try {
2747
3398
  return await load();
2748
- } catch (err) {
2749
- log9.warn(`${name} module could not be loaded`, err.message);
3399
+ } catch (err2) {
3400
+ log10.warn(`${name} module could not be loaded`, err2.message);
2750
3401
  return null;
2751
3402
  }
2752
3403
  }
@@ -2775,7 +3426,9 @@ async function createApp(opts = {}) {
2775
3426
  manager: void 0,
2776
3427
  lastUserActivity: { at: Date.now() },
2777
3428
  shutdown: async () => {
2778
- }
3429
+ },
3430
+ mountConnector: async () => null,
3431
+ checkConnector: async () => ({ status: "unreachable", tools: [] })
2779
3432
  };
2780
3433
  app.manager = new BotManager({
2781
3434
  store,
@@ -2820,41 +3473,84 @@ async function createApp(opts = {}) {
2820
3473
  */
2821
3474
  connectorServers: async (botId) => {
2822
3475
  const assigned = store.listBotConnectors(botId);
2823
- if (!assigned.length) return { servers: {}, mounted: [] };
2824
- const available = new Set(app.secrets?.list() ?? []);
2825
- const { mount, skipped } = planConnectorMount(assigned, available);
2826
- for (const s of skipped) {
2827
- log9.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
2828
- }
2829
3476
  const servers = {};
2830
3477
  const mounted = [];
2831
- for (const connector of mount) {
2832
- const refs = extractSecretRefs(connector.config);
2833
- try {
2834
- const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
2835
- const built = buildMcpServerConfig(connector, secrets);
2836
- const auth = await app.connectorAuth?.authHeader(connector.name);
2837
- if (auth && built.headers && !("Authorization" in built.headers)) {
2838
- built.headers = { ...built.headers, ...auth };
2839
- }
2840
- servers[connector.name] = built;
2841
- mounted.push({ name: connector.name, description: connector.description });
2842
- } catch (err) {
2843
- log9.warn(`connector "${connector.name}" not mounted`, err.message);
2844
- }
3478
+ for (const connector of assigned) {
3479
+ const built = await app.mountConnector(connector);
3480
+ if (!built) continue;
3481
+ servers[connector.name] = built;
3482
+ mounted.push({ name: connector.name, description: connector.description });
2845
3483
  }
2846
3484
  return { servers, mounted };
2847
3485
  }
2848
3486
  });
3487
+ app.mountConnector = async (connector) => {
3488
+ if (connector.kind === "builtin") {
3489
+ if (!app.builtin?.get(connector.name)) return null;
3490
+ return app.builtin.mountConfig(connector);
3491
+ }
3492
+ const available = new Set(app.secrets?.list() ?? []);
3493
+ const { skipped } = planConnectorMount([connector], available);
3494
+ if (skipped.length) {
3495
+ const missing = skipped[0].missing;
3496
+ log10.warn(`connector "${connector.name}" not mounted \u2014 missing secret(s): ${missing.join(", ")}`);
3497
+ store.setConnectorStatus(connector.id, "needs-credential", `missing secret(s): ${missing.join(", ")}`);
3498
+ return null;
3499
+ }
3500
+ try {
3501
+ const refs = extractSecretRefs(connector.config);
3502
+ const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
3503
+ const built = buildMcpServerConfig(connector, secrets);
3504
+ const auth = await app.connectorAuth?.authHeader(connector.name);
3505
+ if (auth && built.type !== "stdio" && !("Authorization" in built.headers)) {
3506
+ built.headers = { ...built.headers, ...auth };
3507
+ }
3508
+ return built;
3509
+ } catch (err2) {
3510
+ log10.warn(`connector "${connector.name}" not mounted`, err2.message);
3511
+ store.setConnectorStatus(connector.id, "needs-credential", err2.message);
3512
+ return null;
3513
+ }
3514
+ };
3515
+ app.checkConnector = async (connector) => {
3516
+ let verdict;
3517
+ if (connector.kind === "builtin") {
3518
+ const def = app.builtin?.get(connector.name);
3519
+ const tools = def ? def.tools().map((t) => ({ name: t.name, description: t.description })) : [];
3520
+ verdict = decideCheck({
3521
+ probe: def ? { ok: true, tools } : null,
3522
+ challenge: "none",
3523
+ missingSecrets: [],
3524
+ builtinSignedIn: app.builtin?.authorized(connector.name) ?? false,
3525
+ builtinProvider: def ? { name: def.provider.displayName, dynamicRegistration: def.provider.dynamicRegistration } : void 0
3526
+ });
3527
+ } else {
3528
+ const available = new Set(app.secrets?.list() ?? []);
3529
+ const missing = computeMissingSecrets(connector, available);
3530
+ const mounted = missing.length ? null : await app.mountConnector(connector);
3531
+ verdict = decideCheck(await gatherCustomSignals(connector, mounted, missing));
3532
+ }
3533
+ store.setConnectorStatus(connector.id, verdict.status, verdict.detail ?? null);
3534
+ return verdict;
3535
+ };
2849
3536
  try {
2850
3537
  app.secrets = new SecretsService(
2851
3538
  await pickBackend(cfg.paths.secrets),
2852
3539
  `${cfg.paths.secrets}.index`
2853
3540
  );
2854
- log9.info(`secrets backend: ${app.secrets.backendName}`);
2855
- app.connectorAuth = new ConnectorAuthService(app.secrets, cfg.port);
2856
- } catch (err) {
2857
- log9.warn("secrets backend unavailable", err.message);
3541
+ log10.info(`secrets backend: ${app.secrets.backendName}`);
3542
+ app.connectorAuth = new ConnectorAuthService(app.secrets, () => app.cfg.port);
3543
+ } catch (err2) {
3544
+ log10.warn("secrets backend unavailable", err2.message);
3545
+ }
3546
+ app.builtin = new BuiltinService(
3547
+ app.connectorAuth,
3548
+ () => app.cfg.port,
3549
+ readPackageVersion(path8.dirname(fileURLToPath(import.meta.url)), (p) => fs8.existsSync(p), (p) => fs8.readFileSync(p, "utf8"))
3550
+ );
3551
+ try {
3552
+ } catch (err2) {
3553
+ log10.warn("secrets backend unavailable", err2.message);
2858
3554
  }
2859
3555
  await wireSkills(app);
2860
3556
  await wireBrowser(app);
@@ -2880,12 +3576,12 @@ async function wireSkills(app) {
2880
3576
  const mod = await optionalImport("skills", () => import("./skills-JUHWFBD6.js"));
2881
3577
  const pluginMod = await optionalImport("skill plugin", () => import("./plugin-WYUCG6F7.js"));
2882
3578
  const Ctor = mod?.SkillStore ?? mod?.default;
2883
- if (!Ctor) return void log9.warn("skills subsystem unavailable: no SkillStore export");
3579
+ if (!Ctor) return void log10.warn("skills subsystem unavailable: no SkillStore export");
2884
3580
  const pluginRoot = app.cfg.paths.skills;
2885
3581
  if (pluginMod?.ensureSkillPlugin) {
2886
3582
  pluginMod.ensureSkillPlugin(pluginRoot);
2887
3583
  const moved = pluginMod.migrateLegacyLayout?.(pluginRoot) ?? [];
2888
- if (moved.length) log9.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
3584
+ if (moved.length) log10.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
2889
3585
  app.skillPluginPath = pluginRoot;
2890
3586
  }
2891
3587
  const filesDir = pluginMod?.skillFilesDir?.(pluginRoot) ?? pluginRoot;
@@ -2898,30 +3594,30 @@ async function wireSkills(app) {
2898
3594
  const installed = took("install");
2899
3595
  const updated = took("update");
2900
3596
  const kept = [...took("skip-modified"), ...took("skip-foreign")];
2901
- if (installed.length) log9.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
2902
- if (updated.length) log9.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
2903
- if (kept.length) log9.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
3597
+ if (installed.length) log10.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
3598
+ if (updated.length) log10.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
3599
+ if (kept.length) log10.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
2904
3600
  const written = [...installed, ...updated, ...took("adopt")];
2905
3601
  const renamed = app.skills?.refreshFromDisk?.(written) ?? [];
2906
- if (renamed.length) log9.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
3602
+ if (renamed.length) log10.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
2907
3603
  } catch (e) {
2908
- log9.warn("bundled skills not synced", e.message);
3604
+ log10.warn("bundled skills not synced", e.message);
2909
3605
  }
2910
3606
  }
2911
3607
  app.skills.syncFromDisk?.();
2912
3608
  const fixed = app.skills.reconcile?.();
2913
- if (fixed?.repaired.length) log9.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
2914
- if (fixed?.removed.length) log9.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
2915
- log9.info(`skills ready (${app.store.listSkills().length} registered)`);
2916
- } catch (err) {
2917
- log9.warn("skills subsystem unavailable", err.message);
3609
+ if (fixed?.repaired.length) log10.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
3610
+ if (fixed?.removed.length) log10.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
3611
+ log10.info(`skills ready (${app.store.listSkills().length} registered)`);
3612
+ } catch (err2) {
3613
+ log10.warn("skills subsystem unavailable", err2.message);
2918
3614
  }
2919
3615
  }
2920
3616
  async function wireBrowser(app) {
2921
3617
  try {
2922
3618
  const mod = await optionalImport("browser", () => import("./browser-NOWM4S6C.js"));
2923
3619
  const Ctor = mod?.BrowserService ?? mod?.default;
2924
- if (!Ctor) return void log9.warn("browser subsystem unavailable: no BrowserService export");
3620
+ if (!Ctor) return void log10.warn("browser subsystem unavailable: no BrowserService export");
2925
3621
  const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
2926
3622
  let toolsMod = null;
2927
3623
  toolsMod = await optionalImport("browser tools", () => import("./tools-YNE7ZRPR.js"));
@@ -2936,16 +3632,16 @@ async function wireBrowser(app) {
2936
3632
  return s;
2937
3633
  };
2938
3634
  app.browser = svc;
2939
- log9.info("browser computer service ready");
2940
- } catch (err) {
2941
- log9.warn("browser subsystem unavailable", err.message);
3635
+ log10.info("browser computer service ready");
3636
+ } catch (err2) {
3637
+ log10.warn("browser subsystem unavailable", err2.message);
2942
3638
  }
2943
3639
  }
2944
3640
  async function wireScheduler(app) {
2945
3641
  try {
2946
3642
  const mod = await optionalImport("scheduler", () => import("./scheduler-VZVHQWGD.js"));
2947
3643
  const Ctor = mod?.Scheduler ?? mod?.default;
2948
- if (!Ctor) return void log9.warn("scheduler subsystem unavailable: no Scheduler export");
3644
+ if (!Ctor) return void log10.warn("scheduler subsystem unavailable: no Scheduler export");
2949
3645
  app.scheduler = new Ctor({
2950
3646
  store: app.store,
2951
3647
  bus: app.bus,
@@ -2953,9 +3649,9 @@ async function wireScheduler(app) {
2953
3649
  getSettings: app.getSettings
2954
3650
  });
2955
3651
  app.scheduler.start?.();
2956
- log9.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
2957
- } catch (err) {
2958
- log9.warn("scheduler subsystem unavailable", err.message);
3652
+ log10.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
3653
+ } catch (err2) {
3654
+ log10.warn("scheduler subsystem unavailable", err2.message);
2959
3655
  }
2960
3656
  }
2961
3657
  function drainMailbox(app) {
@@ -2987,7 +3683,7 @@ function workspaceRelative(root, p) {
2987
3683
 
2988
3684
  // daemon/src/bots/groups.ts
2989
3685
  import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2990
- var log10 = logger("groups");
3686
+ var log11 = logger("groups");
2991
3687
  async function routeGroupMessage(args) {
2992
3688
  const { text, members, mentionBotIds, mentionEveryone, settings, cwd } = args;
2993
3689
  if (mentionEveryone) return members;
@@ -3025,8 +3721,8 @@ Which single teammate should own this? Reply with only the slug.`,
3025
3721
  const slug = out.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
3026
3722
  const found = members.find((m) => m.slug === slug);
3027
3723
  if (found) return [found];
3028
- } catch (err) {
3029
- log10.warn("group router failed; defaulting to first member", err);
3724
+ } catch (err2) {
3725
+ log11.warn("group router failed; defaulting to first member", err2);
3030
3726
  }
3031
3727
  return members.slice(0, 1);
3032
3728
  }
@@ -3037,13 +3733,13 @@ function parseMentions(text, members) {
3037
3733
  }
3038
3734
 
3039
3735
  // daemon/src/api/routes-core.ts
3040
- import fs8 from "node:fs";
3736
+ import fs9 from "node:fs";
3041
3737
  import path9 from "node:path";
3042
- import { fileURLToPath } from "node:url";
3738
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3043
3739
  var SERVER_VERSION = readPackageVersion(
3044
- path9.dirname(fileURLToPath(import.meta.url)),
3045
- (p) => fs8.existsSync(p),
3046
- (p) => fs8.readFileSync(p, "utf8")
3740
+ path9.dirname(fileURLToPath2(import.meta.url)),
3741
+ (p) => fs9.existsSync(p),
3742
+ (p) => fs9.readFileSync(p, "utf8")
3047
3743
  );
3048
3744
  function registerCoreRoutes(f, app) {
3049
3745
  const { store, bus, manager } = app;
@@ -3066,9 +3762,9 @@ function registerCoreRoutes(f, app) {
3066
3762
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
3067
3763
  try {
3068
3764
  return store.createBot(parsed.data);
3069
- } catch (err) {
3070
- if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
3071
- throw err;
3765
+ } catch (err2) {
3766
+ if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
3767
+ throw err2;
3072
3768
  }
3073
3769
  });
3074
3770
  f.get("/api/bots/:id", async (req, reply) => {
@@ -3104,9 +3800,9 @@ function registerCoreRoutes(f, app) {
3104
3800
  try {
3105
3801
  const copy = store.duplicateBot(req.params.id);
3106
3802
  return copy ?? reply.code(404).send({ error: "No such bot" });
3107
- } catch (err) {
3108
- if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
3109
- throw err;
3803
+ } catch (err2) {
3804
+ if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
3805
+ throw err2;
3110
3806
  }
3111
3807
  });
3112
3808
  f.post("/api/bots/:id/stop", async (req) => ({
@@ -3159,9 +3855,9 @@ function registerCoreRoutes(f, app) {
3159
3855
  return reply.code(400).send({ error: "One or more bots do not exist" });
3160
3856
  const title = parsed.data.title || members.map((m) => m.name).join(", ");
3161
3857
  return store.createThread({ ...parsed.data, title });
3162
- } catch (err) {
3163
- if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
3164
- throw err;
3858
+ } catch (err2) {
3859
+ if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
3860
+ throw err2;
3165
3861
  }
3166
3862
  });
3167
3863
  f.get("/api/threads/:id", async (req, reply) => {
@@ -3202,9 +3898,9 @@ function registerCoreRoutes(f, app) {
3202
3898
  if (parsed.data.attachmentIds?.length) {
3203
3899
  try {
3204
3900
  store.attachToMessage(parsed.data.attachmentIds, msg.id);
3205
- } catch (err) {
3206
- if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
3207
- throw err;
3901
+ } catch (err2) {
3902
+ if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
3903
+ throw err2;
3208
3904
  }
3209
3905
  }
3210
3906
  bus.publish({ type: "message.created", threadId: thread.id, botId: null, message: store.getMessage(msg.id) });
@@ -3236,178 +3932,8 @@ ${attachments.map((a) => `- ${a.name} \u2192 ${a.path}`).join("\n")}` : "";
3236
3932
  }
3237
3933
 
3238
3934
  // daemon/src/api/routes-ops.ts
3239
- import fs9 from "node:fs";
3935
+ import fs10 from "node:fs";
3240
3936
  import path10 from "node:path";
3241
-
3242
- // daemon/src/bots/mcpProbe.ts
3243
- import { spawn } from "node:child_process";
3244
- var log11 = logger("mcp-probe");
3245
- var PROTOCOL_VERSION = "2025-06-18";
3246
- var DEFAULT_TIMEOUT_MS = 1e4;
3247
- var MAX_DESCRIPTION = 200;
3248
- function parseToolsResult(result) {
3249
- const tools = result?.tools;
3250
- if (!Array.isArray(tools)) return [];
3251
- return tools.filter((t) => typeof t === "object" && t !== null).map((t) => ({
3252
- name: String(t.name ?? ""),
3253
- description: String(t.description ?? "").slice(0, MAX_DESCRIPTION)
3254
- })).filter((t) => t.name.length > 0);
3255
- }
3256
- var rpc = (id, method, params) => `${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
3257
- `;
3258
- var notify = (method) => `${JSON.stringify({ jsonrpc: "2.0", method })}
3259
- `;
3260
- var failed = (error) => ({ ok: false, tools: [], error });
3261
- async function probeStdio(cfg, timeoutMs) {
3262
- return new Promise((resolve) => {
3263
- let child;
3264
- try {
3265
- child = spawn(cfg.command, cfg.args ?? [], {
3266
- // The server's own env plus the connector's — a connector that needs PATH still gets it.
3267
- env: { ...process.env, ...cfg.env ?? {} },
3268
- stdio: ["pipe", "pipe", "pipe"]
3269
- });
3270
- } catch (err) {
3271
- return resolve(failed(err.message));
3272
- }
3273
- let settled = false;
3274
- let stderr = "";
3275
- let buffer = "";
3276
- const finish = (r) => {
3277
- if (settled) return;
3278
- settled = true;
3279
- clearTimeout(timer);
3280
- try {
3281
- child.kill("SIGKILL");
3282
- } catch {
3283
- }
3284
- resolve(r);
3285
- };
3286
- const timer = setTimeout(
3287
- () => finish(failed(`timed out after ${timeoutMs}ms${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`)),
3288
- timeoutMs
3289
- );
3290
- child.on("error", (err) => finish(failed(err.message)));
3291
- child.on(
3292
- "exit",
3293
- (code) => finish(failed(`server exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`))
3294
- );
3295
- child.stderr?.on("data", (d) => {
3296
- stderr += d.toString();
3297
- });
3298
- child.stdout?.on("data", (d) => {
3299
- buffer += d.toString();
3300
- const lines = buffer.split("\n");
3301
- buffer = lines.pop() ?? "";
3302
- for (const line of lines) {
3303
- if (!line.trim()) continue;
3304
- let msg;
3305
- try {
3306
- msg = JSON.parse(line);
3307
- } catch {
3308
- continue;
3309
- }
3310
- if (msg.id === 1) {
3311
- try {
3312
- child.stdin?.write(notify("notifications/initialized"));
3313
- child.stdin?.write(rpc(2, "tools/list"));
3314
- } catch (err) {
3315
- finish(failed(err.message));
3316
- }
3317
- } else if (msg.id === 2) {
3318
- if (msg.error) return finish(failed(JSON.stringify(msg.error).slice(0, 200)));
3319
- finish({ ok: true, tools: parseToolsResult(msg.result) });
3320
- }
3321
- }
3322
- });
3323
- try {
3324
- child.stdin?.write(
3325
- rpc(1, "initialize", {
3326
- protocolVersion: PROTOCOL_VERSION,
3327
- capabilities: {},
3328
- clientInfo: { name: "ant-bot", version: "1.0.0" }
3329
- })
3330
- );
3331
- } catch (err) {
3332
- finish(failed(err.message));
3333
- }
3334
- });
3335
- }
3336
- async function probeHttp(cfg, timeoutMs) {
3337
- const ac = new AbortController();
3338
- const timer = setTimeout(() => ac.abort(), timeoutMs);
3339
- const base = {
3340
- "content-type": "application/json",
3341
- accept: "application/json, text/event-stream",
3342
- ...cfg.headers ?? {}
3343
- };
3344
- const readBody = async (res) => {
3345
- const text = await res.text();
3346
- const line = text.split("\n").find((l) => l.startsWith("data:"));
3347
- try {
3348
- return JSON.parse(line ? line.slice(5).trim() : text);
3349
- } catch {
3350
- return null;
3351
- }
3352
- };
3353
- try {
3354
- const initRes = await fetch(cfg.url, {
3355
- method: "POST",
3356
- signal: ac.signal,
3357
- headers: base,
3358
- body: rpc(1, "initialize", {
3359
- protocolVersion: PROTOCOL_VERSION,
3360
- capabilities: {},
3361
- clientInfo: { name: "ant-bot", version: "1.0.0" }
3362
- })
3363
- });
3364
- if (!initRes.ok) return failed(`initialize returned HTTP ${initRes.status}`);
3365
- const session = initRes.headers.get("mcp-session-id");
3366
- const withSession = session ? { ...base, "mcp-session-id": session } : base;
3367
- await readBody(initRes);
3368
- await fetch(cfg.url, { method: "POST", signal: ac.signal, headers: withSession, body: notify("notifications/initialized") }).catch(() => void 0);
3369
- const listRes = await fetch(cfg.url, {
3370
- method: "POST",
3371
- signal: ac.signal,
3372
- headers: withSession,
3373
- body: rpc(2, "tools/list")
3374
- });
3375
- if (!listRes.ok) return failed(`tools/list returned HTTP ${listRes.status}`);
3376
- const body = await readBody(listRes);
3377
- if (body?.error) return failed(JSON.stringify(body.error).slice(0, 200));
3378
- return { ok: true, tools: parseToolsResult(body?.result) };
3379
- } catch (err) {
3380
- const e = err;
3381
- return failed(e.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message);
3382
- } finally {
3383
- clearTimeout(timer);
3384
- }
3385
- }
3386
- async function probeConnector(config, opts = {}) {
3387
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3388
- const type = config.type;
3389
- try {
3390
- if (type === "stdio") {
3391
- return await probeStdio(config, timeoutMs);
3392
- }
3393
- if (type === "http") {
3394
- const result = await probeHttp(config, timeoutMs);
3395
- const headers = config.headers ?? {};
3396
- const authed = Object.keys(headers).some((h) => /^(authorization|x-api-key|api-key)$/i.test(h));
3397
- if (result.ok && !authed) {
3398
- result.authHint = "This server was reached without any credential. If it needs one, the tools will list here but a bot will still see none \u2014 add an Authorization header, e.g. {{secret:NAME}}.";
3399
- }
3400
- return result;
3401
- }
3402
- if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
3403
- return failed(`unknown transport: ${String(type)}`);
3404
- } catch (err) {
3405
- log11.warn("probe threw", err);
3406
- return failed(err.message);
3407
- }
3408
- }
3409
-
3410
- // daemon/src/api/routes-ops.ts
3411
3937
  function registerOpsRoutes(f, app) {
3412
3938
  const { store, gateway, bus } = app;
3413
3939
  f.get("/api/approvals", async () => store.listPendingApprovals());
@@ -3441,35 +3967,74 @@ function registerOpsRoutes(f, app) {
3441
3967
  store.deleteRule(rule.id);
3442
3968
  return { ok: true };
3443
3969
  });
3444
- f.get("/api/connectors", async () => {
3445
- const available = new Set(app.secrets?.list() ?? []);
3446
- return store.listConnectors().map((c) => ({
3447
- ...c,
3448
- missingSecrets: computeMissingSecrets(c, available),
3449
- // Names only — knowing a connector is signed in never requires reading its token.
3450
- signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
3451
- }));
3970
+ const describe = (c) => ({
3971
+ ...c,
3972
+ missingSecrets: computeMissingSecrets(c, new Set(app.secrets?.list() ?? [])),
3973
+ // Names only — knowing a connector is signed in never requires reading its token.
3974
+ signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
3452
3975
  });
3976
+ f.get("/api/connectors", async () => store.listConnectors().map(describe));
3977
+ f.get(
3978
+ "/api/connectors/catalog",
3979
+ async () => Object.values(BUILTIN_CATALOG).map((b2) => ({
3980
+ name: b2.name,
3981
+ displayName: b2.displayName,
3982
+ description: b2.description,
3983
+ provider: b2.provider.displayName,
3984
+ needsClientCredentials: !b2.provider.dynamicRegistration,
3985
+ setupSteps: b2.provider.setupSteps.map((step) => step.replace("{redirectUri}", redirectUri(app.cfg.port)))
3986
+ }))
3987
+ );
3453
3988
  f.post("/api/connectors", async (req, reply) => {
3454
3989
  const parsed = CreateConnectorRequest.safeParse(req.body);
3455
3990
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
3456
- if (store.getConnectorByName(parsed.data.name)) {
3457
- return reply.code(409).send({ error: `A connector named "${parsed.data.name}" already exists` });
3991
+ const body = parsed.data;
3992
+ if (store.getConnectorByName(body.name)) {
3993
+ return reply.code(409).send({ error: `A connector named "${body.name}" already exists` });
3458
3994
  }
3459
- return store.createConnector(parsed.data);
3995
+ let created;
3996
+ if (body.builtin) {
3997
+ const def = app.builtin?.get(body.builtin);
3998
+ if (!def) return reply.code(400).send({ error: `No built-in connector named "${body.builtin}"` });
3999
+ if (body.name !== def.name) return reply.code(400).send({ error: `The built-in ${def.name} connector must be named "${def.name}"` });
4000
+ created = store.createConnector({
4001
+ name: def.name,
4002
+ description: body.description || def.description,
4003
+ config: app.builtin.rowConfig(def.name),
4004
+ kind: "builtin",
4005
+ enabled: body.enabled
4006
+ });
4007
+ } else {
4008
+ created = store.createConnector({ name: body.name, description: body.description, config: body.config, enabled: body.enabled });
4009
+ }
4010
+ for (const botId of body.botIds ?? []) {
4011
+ if (!store.getBot(botId)) continue;
4012
+ const current = store.listBotConnectors(botId).map((c) => c.id);
4013
+ store.setBotConnectors(botId, [.../* @__PURE__ */ new Set([...current, created.id])]);
4014
+ }
4015
+ const check = await app.checkConnector(created);
4016
+ return { ...describe(store.getConnector(created.id)), check };
3460
4017
  });
3461
4018
  f.patch("/api/connectors/:id", async (req, reply) => {
3462
4019
  const parsed = UpdateConnectorRequest.safeParse(req.body);
3463
4020
  if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
3464
- const updated = store.updateConnector(req.params.id, parsed.data);
3465
- if (!updated) return reply.code(404).send({ error: "No such connector" });
3466
- return updated;
4021
+ const existing = store.getConnector(req.params.id);
4022
+ if (!existing) return reply.code(404).send({ error: "No such connector" });
4023
+ if (existing.kind === "builtin" && parsed.data.config) return reply.code(400).send({ error: "A built-in connector has no editable config" });
4024
+ return describe(store.updateConnector(req.params.id, parsed.data));
3467
4025
  });
3468
4026
  f.delete("/api/connectors/:id", async (req, reply) => {
3469
- if (!store.getConnector(req.params.id)) return reply.code(404).send({ error: "No such connector" });
4027
+ const c = store.getConnector(req.params.id);
4028
+ if (!c) return reply.code(404).send({ error: "No such connector" });
4029
+ await app.connectorAuth?.signOut(c.name);
3470
4030
  store.deleteConnector(req.params.id);
3471
4031
  return { ok: true };
3472
4032
  });
4033
+ f.post("/api/connectors/:id/check", async (req, reply) => {
4034
+ const connector = store.getConnector(req.params.id);
4035
+ if (!connector) return reply.code(404).send({ error: "No such connector" });
4036
+ return app.checkConnector(connector);
4037
+ });
3473
4038
  f.post(
3474
4039
  "/api/connectors/:id/login",
3475
4040
  async (req, reply) => {
@@ -3477,10 +4042,10 @@ function registerOpsRoutes(f, app) {
3477
4042
  if (!connector) return reply.code(404).send({ error: "No such connector" });
3478
4043
  if (!app.connectorAuth) return reply.code(503).send({ error: "Secrets backend unavailable, so sign-in cannot be stored" });
3479
4044
  try {
3480
- const { authorizeUrl } = await app.connectorAuth.beginLogin(connector, req.body ?? {});
4045
+ const authorizeUrl = connector.kind === "builtin" ? await app.builtin.beginLogin(connector, req.body ?? {}) : (await app.connectorAuth.beginLogin(connector, req.body ?? {})).authorizeUrl;
3481
4046
  return { authorizeUrl };
3482
- } catch (err) {
3483
- return reply.code(400).send({ error: err.message });
4047
+ } catch (err2) {
4048
+ return reply.code(400).send({ error: err2.message });
3484
4049
  }
3485
4050
  }
3486
4051
  );
@@ -3488,56 +4053,39 @@ function registerOpsRoutes(f, app) {
3488
4053
  const connector = store.getConnector(req.params.id);
3489
4054
  if (!connector) return reply.code(404).send({ error: "No such connector" });
3490
4055
  await app.connectorAuth?.signOut(connector.name);
4056
+ store.setConnectorStatus(connector.id, "needs-sign-in", null);
3491
4057
  return { ok: true };
3492
4058
  });
3493
4059
  f.get(
3494
4060
  "/api/connectors/oauth/callback",
3495
4061
  async (req, reply) => {
3496
- const page = (title, detail, ok) => `<!doctype html><meta charset=utf-8><title>${title}</title>
4062
+ const page = (title, detail, ok2) => `<!doctype html><meta charset=utf-8><title>${title}</title>
3497
4063
  <body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:3rem;max-width:40rem">
3498
- <h1 style="color:${ok ? "#4ade80" : "#f87171"}">${title}</h1><p>${detail}</p>
4064
+ <h1 style="color:${ok2 ? "#4ade80" : "#f87171"}">${title}</h1><p>${detail}</p>
3499
4065
  <p style="color:#9aa4b2">You can close this tab and return to ant-bot.</p>`;
3500
4066
  const { code, state, error, error_description: desc } = req.query;
3501
- if (error) {
3502
- return reply.type("text/html").send(page("Sign-in failed", `${error}: ${desc ?? ""}`, false));
3503
- }
3504
- if (!code || !state) {
3505
- return reply.type("text/html").send(page("Sign-in failed", "The provider did not return a code.", false));
3506
- }
3507
- if (!app.connectorAuth) {
3508
- return reply.type("text/html").send(page("Sign-in failed", "The secrets backend is unavailable.", false));
3509
- }
4067
+ if (error) return reply.type("text/html").send(page("Sign-in failed", `${error}: ${desc ?? ""}`, false));
4068
+ if (!code || !state) return reply.type("text/html").send(page("Sign-in failed", "The provider did not return a code.", false));
4069
+ if (!app.connectorAuth) return reply.type("text/html").send(page("Sign-in failed", "The secrets backend is unavailable.", false));
3510
4070
  try {
3511
- const { connectorName } = await app.connectorAuth.completeLogin(state, code);
3512
- bus.publish({
3513
- type: "notify",
3514
- botId: null,
3515
- threadId: null,
3516
- title: "Connector signed in",
3517
- body: `${connectorName} is now authorised.`,
3518
- level: "info"
3519
- });
4071
+ const { connectorId, connectorName } = await app.connectorAuth.completeLogin(state, code);
4072
+ const row = store.getConnector(connectorId);
4073
+ if (row) await app.checkConnector(row);
4074
+ bus.publish({ type: "notify", botId: null, threadId: null, title: "Connector signed in", body: `${connectorName} is now authorised.`, level: "info" });
3520
4075
  return reply.type("text/html").send(page("Signed in", `<b>${connectorName}</b> is now authorised.`, true));
3521
- } catch (err) {
3522
- return reply.type("text/html").send(page("Sign-in failed", err.message, false));
4076
+ } catch (err2) {
4077
+ return reply.type("text/html").send(page("Sign-in failed", err2.message, false));
3523
4078
  }
3524
4079
  }
3525
4080
  );
3526
- f.post("/api/connectors/:id/test", async (req, reply) => {
3527
- const connector = store.getConnector(req.params.id);
3528
- if (!connector) return reply.code(404).send({ error: "No such connector" });
3529
- const refs = extractSecretRefs(connector.config);
3530
- const missing = computeMissingSecrets(connector, new Set(app.secrets?.list() ?? []));
3531
- if (missing.length) {
3532
- return { ok: false, tools: [], error: `missing secret(s): ${missing.join(", ")}` };
3533
- }
3534
- try {
3535
- const secrets = refs.length && app.secrets ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
3536
- return await probeConnector(buildMcpServerConfig(connector, secrets));
3537
- } catch (err) {
3538
- return { ok: false, tools: [], error: err.message };
3539
- }
4081
+ f.post("/mcp/:name", async (req, reply) => {
4082
+ if (!app.builtin) return reply.code(503).send({ error: "Built-in connectors unavailable" });
4083
+ if (!app.builtin.checkBearer(req.headers.authorization)) return reply.code(401).send({ error: "Unauthorized" });
4084
+ const res = await app.builtin.handle(req.params.name, req.body);
4085
+ if (res === null) return reply.code(202).send();
4086
+ return res;
3540
4087
  });
4088
+ f.delete("/mcp/:name", async () => ({ ok: true }));
3541
4089
  f.get("/api/skills", async () => store.listSkills());
3542
4090
  f.post("/api/skills", async (req, reply) => {
3543
4091
  const parsed = CreateSkillRequest.safeParse(req.body);
@@ -3559,8 +4107,8 @@ function registerOpsRoutes(f, app) {
3559
4107
  replaced: i2.replaced
3560
4108
  }))
3561
4109
  };
3562
- } catch (err) {
3563
- return reply.code(400).send({ error: err.message });
4110
+ } catch (err2) {
4111
+ return reply.code(400).send({ error: err2.message });
3564
4112
  }
3565
4113
  });
3566
4114
  f.get("/api/skills/:id", async (req, reply) => {
@@ -3604,9 +4152,9 @@ function registerOpsRoutes(f, app) {
3604
4152
  const routine = store.createRoutine({ ...parsed.data, timezone: parsed.data.timezone ?? app.getSettings().timezone });
3605
4153
  app.scheduler?.reload?.(routine.id);
3606
4154
  return routine;
3607
- } catch (err) {
3608
- if (err instanceof LimitError) return reply.code(409).send({ error: err.message, code: err.code });
3609
- throw err;
4155
+ } catch (err2) {
4156
+ if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
4157
+ throw err2;
3610
4158
  }
3611
4159
  });
3612
4160
  f.patch("/api/routines/:id", async (req, reply) => {
@@ -3637,8 +4185,8 @@ function registerOpsRoutes(f, app) {
3637
4185
  const buf = await part.toBuffer();
3638
4186
  const safe = String(part.filename ?? "file").replace(/[^a-zA-Z0-9._-]/g, "_");
3639
4187
  const dest = path10.join(app.cfg.paths.attachments, `${Date.now()}-${safe}`);
3640
- fs9.mkdirSync(path10.dirname(dest), { recursive: true });
3641
- fs9.writeFileSync(dest, buf);
4188
+ fs10.mkdirSync(path10.dirname(dest), { recursive: true });
4189
+ fs10.writeFileSync(dest, buf);
3642
4190
  try {
3643
4191
  return store.createAttachment({
3644
4192
  messageId: null,
@@ -3647,16 +4195,16 @@ function registerOpsRoutes(f, app) {
3647
4195
  mime: part.mimetype ?? "application/octet-stream",
3648
4196
  bytes: buf.byteLength
3649
4197
  });
3650
- } catch (err) {
3651
- fs9.unlinkSync(dest);
3652
- if (err instanceof LimitError) return reply.code(413).send({ error: err.message, code: err.code });
3653
- throw err;
4198
+ } catch (err2) {
4199
+ fs10.unlinkSync(dest);
4200
+ if (err2 instanceof LimitError) return reply.code(413).send({ error: err2.message, code: err2.code });
4201
+ throw err2;
3654
4202
  }
3655
4203
  });
3656
4204
  f.get("/api/attachments/:id", async (req, reply) => {
3657
4205
  const a = store.getAttachment(req.params.id);
3658
- if (!a || !fs9.existsSync(a.path)) return reply.code(404).send({ error: "No such attachment" });
3659
- return reply.type(a.mime).send(fs9.createReadStream(a.path));
4206
+ if (!a || !fs10.existsSync(a.path)) return reply.code(404).send({ error: "No such attachment" });
4207
+ return reply.type(a.mime).send(fs10.createReadStream(a.path));
3660
4208
  });
3661
4209
  f.get("/api/usage", async () => {
3662
4210
  const rows = store.listUsage(0);
@@ -3723,12 +4271,12 @@ function registerOpsRoutes(f, app) {
3723
4271
  const root = app.cfg.paths.workspace;
3724
4272
  const target = workspaceRelative(root, req.query.path ?? ".");
3725
4273
  if (!target) return reply.code(400).send({ error: "Path is outside the workspace" });
3726
- if (!fs9.existsSync(target)) return [];
3727
- return fs9.readdirSync(target, { withFileTypes: true }).map((d) => {
4274
+ if (!fs10.existsSync(target)) return [];
4275
+ return fs10.readdirSync(target, { withFileTypes: true }).map((d) => {
3728
4276
  const full = path10.join(target, d.name);
3729
4277
  let bytes = 0;
3730
4278
  try {
3731
- bytes = d.isFile() ? fs9.statSync(full).size : 0;
4279
+ bytes = d.isFile() ? fs10.statSync(full).size : 0;
3732
4280
  } catch {
3733
4281
  }
3734
4282
  return { name: d.name, path: path10.relative(root, full), dir: d.isDirectory(), bytes };
@@ -3737,7 +4285,7 @@ function registerOpsRoutes(f, app) {
3737
4285
  f.get("/api/workspace/file", async (req, reply) => {
3738
4286
  const root = app.cfg.paths.workspace;
3739
4287
  const target = workspaceRelative(root, req.query.path ?? "");
3740
- if (!target || !fs9.existsSync(target) || !fs9.statSync(target).isFile())
4288
+ if (!target || !fs10.existsSync(target) || !fs10.statSync(target).isFile())
3741
4289
  return reply.code(404).send({ error: "No such file" });
3742
4290
  const ext = path10.extname(target).toLowerCase();
3743
4291
  const mime = {
@@ -3753,14 +4301,14 @@ function registerOpsRoutes(f, app) {
3753
4301
  ".pdf": "application/pdf",
3754
4302
  ".html": "text/html"
3755
4303
  };
3756
- return reply.type(mime[ext] ?? "application/octet-stream").send(fs9.createReadStream(target));
4304
+ return reply.type(mime[ext] ?? "application/octet-stream").send(fs10.createReadStream(target));
3757
4305
  });
3758
4306
  f.get("/api/computer/status", async () => {
3759
4307
  if (!app.browser?.status) return { available: false, reason: "Browser service not built", mode: "host", headless: true, pages: [] };
3760
4308
  try {
3761
4309
  return await app.browser.status();
3762
- } catch (err) {
3763
- return { available: false, reason: err.message, mode: "host", headless: true, pages: [] };
4310
+ } catch (err2) {
4311
+ return { available: false, reason: err2.message, mode: "host", headless: true, pages: [] };
3764
4312
  }
3765
4313
  });
3766
4314
  f.post("/api/computer/takeover", async (req, reply) => {
@@ -3779,7 +4327,7 @@ var log12 = logger("server");
3779
4327
  var require_ = createRequire(import.meta.url);
3780
4328
  function resolveWebDist() {
3781
4329
  return findWebDist(
3782
- nodeLocateDeps(path11.dirname(fileURLToPath2(import.meta.url)), (spec) => {
4330
+ nodeLocateDeps(path11.dirname(fileURLToPath3(import.meta.url)), (spec) => {
3783
4331
  try {
3784
4332
  return require_.resolve(spec);
3785
4333
  } catch {
@@ -3845,9 +4393,9 @@ async function startServer(opts = {}) {
3845
4393
  } catch {
3846
4394
  }
3847
4395
  });
3848
- } catch (err) {
4396
+ } catch (err2) {
3849
4397
  try {
3850
- socket.send(JSON.stringify({ type: "error", message: err.message }));
4398
+ socket.send(JSON.stringify({ type: "error", message: err2.message }));
3851
4399
  } catch {
3852
4400
  }
3853
4401
  socket.close();
@@ -3861,10 +4409,10 @@ async function startServer(opts = {}) {
3861
4409
  } catch {
3862
4410
  return;
3863
4411
  }
3864
- const fail = (err) => {
4412
+ const fail = (err2) => {
3865
4413
  try {
3866
4414
  if (socket.readyState === 1)
3867
- socket.send(JSON.stringify({ type: "input-error", message: err.message }));
4415
+ socket.send(JSON.stringify({ type: "input-error", message: err2.message }));
3868
4416
  } catch {
3869
4417
  }
3870
4418
  };
@@ -3908,14 +4456,15 @@ async function startServer(opts = {}) {
3908
4456
  });
3909
4457
  }
3910
4458
  }
3911
- fastify.setErrorHandler((err, _req, reply) => {
3912
- log12.error("request failed", err);
3913
- const e = err;
4459
+ fastify.setErrorHandler((err2, _req, reply) => {
4460
+ log12.error("request failed", err2);
4461
+ const e = err2;
3914
4462
  const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
3915
4463
  reply.code(code).send({ error: e.message ?? "Internal error" });
3916
4464
  });
3917
4465
  const port = opts.port ?? app.cfg.port;
3918
4466
  const host = opts.host ?? app.cfg.host;
4467
+ app.cfg.port = port;
3919
4468
  await fastify.listen({ port, host });
3920
4469
  const url = `http://${host}:${port}`;
3921
4470
  const delivered = drainMailbox(app);