@michael-joseph-miller/ant-bot 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -2,7 +2,9 @@ import { createRequire as __antbotCreateRequire } from 'node:module';
2
2
  const require = __antbotCreateRequire(import.meta.url);
3
3
  import {
4
4
  ApprovalDecisionRequest,
5
+ ConnectorConfigSchema,
5
6
  CreateBotRequest,
7
+ CreateConnectorRequest,
6
8
  CreateRoutineRequest,
7
9
  CreateRuleRequest,
8
10
  CreateSkillRequest,
@@ -14,8 +16,9 @@ import {
14
16
  ScreencastClientFrameSchema,
15
17
  SettingsPatchSchema,
16
18
  SettingsSchema,
17
- UpdateBotRequest
18
- } from "./chunk-4XML2GPY.js";
19
+ UpdateBotRequest,
20
+ UpdateConnectorRequest
21
+ } from "./chunk-RMDMUCVS.js";
19
22
  import {
20
23
  findWebDist,
21
24
  nodeLocateDeps,
@@ -162,7 +165,27 @@ var MigrationError = class extends Error {
162
165
  var BASELINE_VERSION = 1;
163
166
  var BASELINE_SENTINEL_TABLE = "bots";
164
167
  var MIGRATIONS = [
165
- { version: BASELINE_VERSION, name: "baseline", up: SCHEMA_SQL }
168
+ { version: BASELINE_VERSION, name: "baseline", up: SCHEMA_SQL },
169
+ {
170
+ version: 2,
171
+ name: "connectors",
172
+ // Plain CREATE TABLE, not IF NOT EXISTS: the ledger already guarantees this runs once, and
173
+ // this module exists precisely because IF NOT EXISTS turns a real conflict into silence.
174
+ up: `
175
+ CREATE TABLE connectors (
176
+ id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL,
177
+ description TEXT NOT NULL DEFAULT '',
178
+ config_json TEXT NOT NULL,
179
+ enabled INTEGER NOT NULL DEFAULT 1,
180
+ created_at INTEGER NOT NULL
181
+ );
182
+ CREATE TABLE bot_connectors (
183
+ bot_id TEXT NOT NULL, connector_id TEXT NOT NULL,
184
+ enabled INTEGER NOT NULL DEFAULT 1,
185
+ PRIMARY KEY (bot_id, connector_id)
186
+ );
187
+ `
188
+ }
166
189
  ];
167
190
  var SCHEMA_VERSION_SQL = `
168
191
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -338,6 +361,14 @@ var toSkill = (r) => ({
338
361
  source: r.source,
339
362
  createdAt: r.created_at
340
363
  });
364
+ var toConnector = (r) => ({
365
+ id: r.id,
366
+ name: r.name,
367
+ description: r.description,
368
+ config: ConnectorConfigSchema.parse(JSON.parse(r.config_json)),
369
+ enabled: b(r.enabled),
370
+ createdAt: r.created_at
371
+ });
341
372
  var toRoutine = (r) => ({
342
373
  id: r.id,
343
374
  botId: r.bot_id,
@@ -479,6 +510,8 @@ var Store = class {
479
510
  });
480
511
  for (const bs of this.db.prepare(`SELECT * FROM bot_skills WHERE bot_id=?`).all(id))
481
512
  this.db.prepare(`INSERT OR REPLACE INTO bot_skills (bot_id,skill_id,enabled) VALUES (?,?,?)`).run(copy.id, bs.skill_id, bs.enabled);
513
+ for (const bc of this.db.prepare(`SELECT * FROM bot_connectors WHERE bot_id=?`).all(id))
514
+ this.db.prepare(`INSERT OR REPLACE INTO bot_connectors (bot_id,connector_id,enabled) VALUES (?,?,?)`).run(copy.id, bc.connector_id, bc.enabled);
482
515
  for (const r of this.listRoutines(id))
483
516
  this.createRoutine({ botId: copy.id, name: r.name, cronExpr: r.cronExpr, timezone: r.timezone, instructionMd: r.instructionMd, enabled: false });
484
517
  return copy;
@@ -697,6 +730,53 @@ var Store = class {
697
730
  `SELECT s.* FROM skills s JOIN bot_skills bs ON bs.skill_id=s.id WHERE bs.bot_id=? AND bs.enabled=1`
698
731
  ).all(botId).map(toSkill);
699
732
  }
733
+ /* ---- connectors ---- */
734
+ createConnector(c) {
735
+ const id = newId();
736
+ this.db.prepare(
737
+ `INSERT INTO connectors (id,name,description,config_json,enabled,created_at) VALUES (?,?,?,?,?,?)`
738
+ ).run(id, c.name, c.description ?? "", JSON.stringify(c.config), i(c.enabled, true), now());
739
+ return this.getConnector(id);
740
+ }
741
+ getConnector(id) {
742
+ const r = this.db.prepare(`SELECT * FROM connectors WHERE id=?`).get(id);
743
+ return r ? toConnector(r) : null;
744
+ }
745
+ getConnectorByName(name) {
746
+ const r = this.db.prepare(`SELECT * FROM connectors WHERE name=?`).get(name);
747
+ return r ? toConnector(r) : null;
748
+ }
749
+ listConnectors() {
750
+ return this.db.prepare(`SELECT * FROM connectors ORDER BY name ASC`).all().map(toConnector);
751
+ }
752
+ /** Patch in place. No rename: the name is baked into every `mcp__<name>__<tool>` a rule may match. */
753
+ updateConnector(id, patch) {
754
+ const existing = this.getConnector(id);
755
+ if (!existing) return null;
756
+ this.db.prepare(`UPDATE connectors SET description=?, config_json=?, enabled=? WHERE id=?`).run(
757
+ patch.description ?? existing.description,
758
+ JSON.stringify(patch.config ?? existing.config),
759
+ i(patch.enabled ?? existing.enabled),
760
+ id
761
+ );
762
+ return this.getConnector(id);
763
+ }
764
+ deleteConnector(id) {
765
+ this.db.prepare(`DELETE FROM connectors WHERE id=?`).run(id);
766
+ this.db.prepare(`DELETE FROM bot_connectors WHERE connector_id=?`).run(id);
767
+ }
768
+ setBotConnectors(botId, connectorIds) {
769
+ this.db.prepare(`DELETE FROM bot_connectors WHERE bot_id=?`).run(botId);
770
+ const stmt = this.db.prepare(`INSERT OR REPLACE INTO bot_connectors (bot_id,connector_id,enabled) VALUES (?,?,1)`);
771
+ for (const c of connectorIds) stmt.run(botId, c);
772
+ }
773
+ /** Assigned AND account-wide enabled — disabling a connector takes it away from every bot at once. */
774
+ listBotConnectors(botId) {
775
+ return this.db.prepare(
776
+ `SELECT c.* FROM connectors c JOIN bot_connectors bc ON bc.connector_id=c.id
777
+ WHERE bc.bot_id=? AND bc.enabled=1 AND c.enabled=1 ORDER BY c.name ASC`
778
+ ).all(botId).map(toConnector);
779
+ }
700
780
  /* ---- routines ---- */
701
781
  createRoutine(r) {
702
782
  const count = this.db.prepare(`SELECT COUNT(*) c FROM routines WHERE bot_id=?`).get(r.botId).c;
@@ -1457,6 +1537,12 @@ You share one computer with every other bot on this account.
1457
1537
  parts.push(`## Your skills
1458
1538
  ${ctx.skills.map((s) => `- **${s.name}** (${s.slug}): ${s.description}`).join("\n")}
1459
1539
  Read the skill file before following it.`);
1540
+ }
1541
+ if (ctx.connectors?.length) {
1542
+ parts.push(`## Your connectors
1543
+ ${ctx.connectors.map((c) => `- **${c.name}**${c.description ? `: ${c.description}` : ""}`).join("\n")}
1544
+ Their tools appear as \`mcp__<connector>__<tool>\`. Prefer a connector's tools over driving the
1545
+ browser for the same service \u2014 it is faster, and it does not depend on a page's layout.`);
1460
1546
  }
1461
1547
  const others = ctx.roster.filter((r) => r.slug !== bot.slug);
1462
1548
  if (others.length) {
@@ -1713,10 +1799,12 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1713
1799
  const botDir = path5.join(workspace, "bots", bot.slug);
1714
1800
  fs4.mkdirSync(botDir, { recursive: true });
1715
1801
  const botSkills = store.listBotSkills(bot.id);
1802
+ const connectors = await this.deps.connectorServers?.(bot.id);
1716
1803
  const systemPrompt = buildSystemPrompt({
1717
1804
  bot,
1718
1805
  workspace,
1719
1806
  skills: botSkills,
1807
+ connectors: connectors?.mounted ?? [],
1720
1808
  roster: store.listBots().map((x) => ({ slug: x.slug, name: x.name, title: x.title })),
1721
1809
  isGroup
1722
1810
  });
@@ -1727,6 +1815,7 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
1727
1815
  const mcpServers = { antbot: this.buildToolServer(bot, job.threadId, job.hops) };
1728
1816
  const browser = this.deps.browserTools?.(bot.id);
1729
1817
  if (browser) mcpServers.browser = browser;
1818
+ if (connectors) Object.assign(mcpServers, connectors.servers);
1730
1819
  try {
1731
1820
  for await (const ev of runTurn({
1732
1821
  prompt: job.prompt,
@@ -1883,10 +1972,73 @@ function summarizeTool(name, input) {
1883
1972
  if (name.includes("list_skills")) return "list installed skills";
1884
1973
  if (name.includes("remember")) return `memory: ${s("title")}`;
1885
1974
  if (name.startsWith("browser_") || name.includes("browser")) return [s("url"), s("selector"), s("text")].filter(Boolean).join(" ").slice(0, 160);
1975
+ const mcp = /^mcp__([^_]+(?:_[^_]+)*)__(.+)$/.exec(name);
1976
+ if (mcp) return `${mcp[1]}: ${mcp[2]} ${JSON.stringify(input ?? {})}`.slice(0, 160);
1886
1977
  const j = JSON.stringify(input ?? {});
1887
1978
  return j.length > 160 ? `${j.slice(0, 160)}\u2026` : j;
1888
1979
  }
1889
1980
 
1981
+ // daemon/src/bots/connectors.ts
1982
+ var SECRET_REF_RE = /\{\{secret:([A-Za-z0-9_.-]+)\}\}/g;
1983
+ function extractSecretRefs(config) {
1984
+ const values = config.transport === "stdio" ? Object.values(config.env) : Object.values(config.headers);
1985
+ const names = [];
1986
+ for (const v of values) {
1987
+ for (const m of v.matchAll(SECRET_REF_RE)) {
1988
+ const name = m[1];
1989
+ if (!names.includes(name)) names.push(name);
1990
+ }
1991
+ }
1992
+ return names;
1993
+ }
1994
+ function computeMissingSecrets(connector, available) {
1995
+ return extractSecretRefs(connector.config).filter((n) => !available.has(n));
1996
+ }
1997
+ function planConnectorMount(assigned, available) {
1998
+ const plan = { mount: [], skipped: [] };
1999
+ for (const connector of assigned) {
2000
+ const missing = computeMissingSecrets(connector, available);
2001
+ if (missing.length) plan.skipped.push({ connector, missing });
2002
+ else plan.mount.push(connector);
2003
+ }
2004
+ return plan;
2005
+ }
2006
+ var MissingSecretError = class extends Error {
2007
+ constructor(connectorName, secretName) {
2008
+ super(`Connector "${connectorName}" references secret "${secretName}", which could not be read.`);
2009
+ this.connectorName = connectorName;
2010
+ this.secretName = secretName;
2011
+ this.name = "MissingSecretError";
2012
+ }
2013
+ connectorName;
2014
+ secretName;
2015
+ };
2016
+ function substitute(value, connectorName, secrets) {
2017
+ return value.replace(SECRET_REF_RE, (_full, name) => {
2018
+ const resolved = secrets.get(name);
2019
+ if (resolved == null) throw new MissingSecretError(connectorName, name);
2020
+ return resolved;
2021
+ });
2022
+ }
2023
+ var substituteAll = (record, connectorName, secrets) => Object.fromEntries(Object.entries(record).map(([k, v]) => [k, substitute(v, connectorName, secrets)]));
2024
+ function buildMcpServerConfig(connector, secrets) {
2025
+ const c = connector.config;
2026
+ if (c.transport === "stdio") {
2027
+ return {
2028
+ type: "stdio",
2029
+ command: c.command,
2030
+ args: c.args,
2031
+ env: substituteAll(c.env, connector.name, secrets)
2032
+ };
2033
+ }
2034
+ return {
2035
+ type: c.transport,
2036
+ url: c.url,
2037
+ headers: substituteAll(c.headers, connector.name, secrets),
2038
+ ...c.tools ? { tools: c.tools } : {}
2039
+ };
2040
+ }
2041
+
1890
2042
  // daemon/src/config/config.ts
1891
2043
  import fs6 from "node:fs";
1892
2044
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -2132,7 +2284,29 @@ var SecretsService = class {
2132
2284
  list() {
2133
2285
  return [...this.names];
2134
2286
  }
2135
- /** Build an env overlay for a tool subprocess. Values never touch the transcript. */
2287
+ /**
2288
+ * Look up exactly the named secrets, for mounting a connector.
2289
+ *
2290
+ * Scoped on purpose. The unscoped `envOverlay()` below hands every stored secret to whatever
2291
+ * asks; a connector should only ever see the ones its own config references, so that adding a
2292
+ * third-party MCP server does not widen the blast radius of every other credential.
2293
+ *
2294
+ * A name with nothing behind it maps to null rather than being dropped, so the caller can tell
2295
+ * "missing" from "empty string" and skip the connector instead of mounting it half-configured.
2296
+ */
2297
+ async resolve(names) {
2298
+ const out = /* @__PURE__ */ new Map();
2299
+ for (const n of new Set(names)) {
2300
+ out.set(n, this.names.has(n) ? await this.backend.get(n) : null);
2301
+ }
2302
+ return out;
2303
+ }
2304
+ /**
2305
+ * Build an env overlay for a tool subprocess. Values never touch the transcript.
2306
+ *
2307
+ * Unused, and unscoped — it returns every secret at once. `resolve()` above is what connectors
2308
+ * use. Kept only because removing it is a separate decision; do not reach for it.
2309
+ */
2136
2310
  async envOverlay() {
2137
2311
  const out = {};
2138
2312
  for (const n of this.names) {
@@ -2212,6 +2386,36 @@ async function createApp(opts = {}) {
2212
2386
  } catch {
2213
2387
  return void 0;
2214
2388
  }
2389
+ },
2390
+ /**
2391
+ * Resolve this bot's connectors into mountable MCP servers.
2392
+ *
2393
+ * This is the only place a secret value is read for a turn, and the values live nowhere but
2394
+ * the returned config — not in the row, not in a log line, not in anything a route returns.
2395
+ * A connector missing a credential is dropped rather than mounted broken or allowed to fail
2396
+ * the turn; the human was already warned on the connectors screen.
2397
+ */
2398
+ connectorServers: async (botId) => {
2399
+ const assigned = store.listBotConnectors(botId);
2400
+ if (!assigned.length) return { servers: {}, mounted: [] };
2401
+ const available = new Set(app.secrets?.list() ?? []);
2402
+ const { mount, skipped } = planConnectorMount(assigned, available);
2403
+ for (const s of skipped) {
2404
+ log8.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
2405
+ }
2406
+ const servers = {};
2407
+ const mounted = [];
2408
+ for (const connector of mount) {
2409
+ const refs = extractSecretRefs(connector.config);
2410
+ try {
2411
+ const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
2412
+ servers[connector.name] = buildMcpServerConfig(connector, secrets);
2413
+ mounted.push({ name: connector.name, description: connector.description });
2414
+ } catch (err) {
2415
+ log8.warn(`connector "${connector.name}" not mounted`, err.message);
2416
+ }
2417
+ }
2418
+ return { servers, mounted };
2215
2419
  }
2216
2420
  });
2217
2421
  try {
@@ -2286,7 +2490,7 @@ async function wireSkills(app) {
2286
2490
  }
2287
2491
  async function wireBrowser(app) {
2288
2492
  try {
2289
- const mod = await optionalImport("browser", () => import("./browser-4LFD7NX2.js"));
2493
+ const mod = await optionalImport("browser", () => import("./browser-NOWM4S6C.js"));
2290
2494
  const Ctor = mod?.BrowserService ?? mod?.default;
2291
2495
  if (!Ctor) return void log8.warn("browser subsystem unavailable: no BrowserService export");
2292
2496
  const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
@@ -2496,6 +2700,15 @@ function registerCoreRoutes(f, app) {
2496
2700
  store.setBotSkills(req.params.id, req.body?.skillIds ?? []);
2497
2701
  return { ok: true };
2498
2702
  });
2703
+ f.get("/api/bots/:id/connectors", async (req, reply) => {
2704
+ if (!store.getBot(req.params.id)) return reply.code(404).send({ error: "No such bot" });
2705
+ return store.listBotConnectors(req.params.id);
2706
+ });
2707
+ f.put("/api/bots/:id/connectors", async (req, reply) => {
2708
+ if (!store.getBot(req.params.id)) return reply.code(404).send({ error: "No such bot" });
2709
+ store.setBotConnectors(req.params.id, req.body?.connectorIds ?? []);
2710
+ return { ok: true };
2711
+ });
2499
2712
  f.get("/api/threads", async () => store.listThreads());
2500
2713
  f.post("/api/threads", async (req, reply) => {
2501
2714
  const parsed = CreateThreadRequest.safeParse(req.body);
@@ -2585,6 +2798,170 @@ ${attachments.map((a) => `- ${a.name} \u2192 ${a.path}`).join("\n")}` : "";
2585
2798
  // daemon/src/api/routes-ops.ts
2586
2799
  import fs9 from "node:fs";
2587
2800
  import path10 from "node:path";
2801
+
2802
+ // daemon/src/bots/mcpProbe.ts
2803
+ import { spawn } from "node:child_process";
2804
+ var log10 = logger("mcp-probe");
2805
+ var PROTOCOL_VERSION = "2025-06-18";
2806
+ var DEFAULT_TIMEOUT_MS = 1e4;
2807
+ var MAX_DESCRIPTION = 200;
2808
+ function parseToolsResult(result) {
2809
+ const tools = result?.tools;
2810
+ if (!Array.isArray(tools)) return [];
2811
+ return tools.filter((t) => typeof t === "object" && t !== null).map((t) => ({
2812
+ name: String(t.name ?? ""),
2813
+ description: String(t.description ?? "").slice(0, MAX_DESCRIPTION)
2814
+ })).filter((t) => t.name.length > 0);
2815
+ }
2816
+ var rpc = (id, method, params) => `${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
2817
+ `;
2818
+ var notify = (method) => `${JSON.stringify({ jsonrpc: "2.0", method })}
2819
+ `;
2820
+ var failed = (error) => ({ ok: false, tools: [], error });
2821
+ async function probeStdio(cfg, timeoutMs) {
2822
+ return new Promise((resolve) => {
2823
+ let child;
2824
+ try {
2825
+ child = spawn(cfg.command, cfg.args ?? [], {
2826
+ // The server's own env plus the connector's — a connector that needs PATH still gets it.
2827
+ env: { ...process.env, ...cfg.env ?? {} },
2828
+ stdio: ["pipe", "pipe", "pipe"]
2829
+ });
2830
+ } catch (err) {
2831
+ return resolve(failed(err.message));
2832
+ }
2833
+ let settled = false;
2834
+ let stderr = "";
2835
+ let buffer = "";
2836
+ const finish = (r) => {
2837
+ if (settled) return;
2838
+ settled = true;
2839
+ clearTimeout(timer);
2840
+ try {
2841
+ child.kill("SIGKILL");
2842
+ } catch {
2843
+ }
2844
+ resolve(r);
2845
+ };
2846
+ const timer = setTimeout(
2847
+ () => finish(failed(`timed out after ${timeoutMs}ms${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`)),
2848
+ timeoutMs
2849
+ );
2850
+ child.on("error", (err) => finish(failed(err.message)));
2851
+ child.on(
2852
+ "exit",
2853
+ (code) => finish(failed(`server exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`))
2854
+ );
2855
+ child.stderr?.on("data", (d) => {
2856
+ stderr += d.toString();
2857
+ });
2858
+ child.stdout?.on("data", (d) => {
2859
+ buffer += d.toString();
2860
+ const lines = buffer.split("\n");
2861
+ buffer = lines.pop() ?? "";
2862
+ for (const line of lines) {
2863
+ if (!line.trim()) continue;
2864
+ let msg;
2865
+ try {
2866
+ msg = JSON.parse(line);
2867
+ } catch {
2868
+ continue;
2869
+ }
2870
+ if (msg.id === 1) {
2871
+ try {
2872
+ child.stdin?.write(notify("notifications/initialized"));
2873
+ child.stdin?.write(rpc(2, "tools/list"));
2874
+ } catch (err) {
2875
+ finish(failed(err.message));
2876
+ }
2877
+ } else if (msg.id === 2) {
2878
+ if (msg.error) return finish(failed(JSON.stringify(msg.error).slice(0, 200)));
2879
+ finish({ ok: true, tools: parseToolsResult(msg.result) });
2880
+ }
2881
+ }
2882
+ });
2883
+ try {
2884
+ child.stdin?.write(
2885
+ rpc(1, "initialize", {
2886
+ protocolVersion: PROTOCOL_VERSION,
2887
+ capabilities: {},
2888
+ clientInfo: { name: "ant-bot", version: "1.0.0" }
2889
+ })
2890
+ );
2891
+ } catch (err) {
2892
+ finish(failed(err.message));
2893
+ }
2894
+ });
2895
+ }
2896
+ async function probeHttp(cfg, timeoutMs) {
2897
+ const ac = new AbortController();
2898
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
2899
+ const base = {
2900
+ "content-type": "application/json",
2901
+ accept: "application/json, text/event-stream",
2902
+ ...cfg.headers ?? {}
2903
+ };
2904
+ const readBody = async (res) => {
2905
+ const text = await res.text();
2906
+ const line = text.split("\n").find((l) => l.startsWith("data:"));
2907
+ try {
2908
+ return JSON.parse(line ? line.slice(5).trim() : text);
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ };
2913
+ try {
2914
+ const initRes = await fetch(cfg.url, {
2915
+ method: "POST",
2916
+ signal: ac.signal,
2917
+ headers: base,
2918
+ body: rpc(1, "initialize", {
2919
+ protocolVersion: PROTOCOL_VERSION,
2920
+ capabilities: {},
2921
+ clientInfo: { name: "ant-bot", version: "1.0.0" }
2922
+ })
2923
+ });
2924
+ if (!initRes.ok) return failed(`initialize returned HTTP ${initRes.status}`);
2925
+ const session = initRes.headers.get("mcp-session-id");
2926
+ const withSession = session ? { ...base, "mcp-session-id": session } : base;
2927
+ await readBody(initRes);
2928
+ await fetch(cfg.url, { method: "POST", signal: ac.signal, headers: withSession, body: notify("notifications/initialized") }).catch(() => void 0);
2929
+ const listRes = await fetch(cfg.url, {
2930
+ method: "POST",
2931
+ signal: ac.signal,
2932
+ headers: withSession,
2933
+ body: rpc(2, "tools/list")
2934
+ });
2935
+ if (!listRes.ok) return failed(`tools/list returned HTTP ${listRes.status}`);
2936
+ const body = await readBody(listRes);
2937
+ if (body?.error) return failed(JSON.stringify(body.error).slice(0, 200));
2938
+ return { ok: true, tools: parseToolsResult(body?.result) };
2939
+ } catch (err) {
2940
+ const e = err;
2941
+ return failed(e.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message);
2942
+ } finally {
2943
+ clearTimeout(timer);
2944
+ }
2945
+ }
2946
+ async function probeConnector(config, opts = {}) {
2947
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2948
+ const type = config.type;
2949
+ try {
2950
+ if (type === "stdio") {
2951
+ return await probeStdio(config, timeoutMs);
2952
+ }
2953
+ if (type === "http") {
2954
+ return await probeHttp(config, timeoutMs);
2955
+ }
2956
+ if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
2957
+ return failed(`unknown transport: ${String(type)}`);
2958
+ } catch (err) {
2959
+ log10.warn("probe threw", err);
2960
+ return failed(err.message);
2961
+ }
2962
+ }
2963
+
2964
+ // daemon/src/api/routes-ops.ts
2588
2965
  function registerOpsRoutes(f, app) {
2589
2966
  const { store, gateway, bus } = app;
2590
2967
  f.get("/api/approvals", async () => store.listPendingApprovals());
@@ -2618,6 +2995,45 @@ function registerOpsRoutes(f, app) {
2618
2995
  store.deleteRule(rule.id);
2619
2996
  return { ok: true };
2620
2997
  });
2998
+ f.get("/api/connectors", async () => {
2999
+ const available = new Set(app.secrets?.list() ?? []);
3000
+ return store.listConnectors().map((c) => ({ ...c, missingSecrets: computeMissingSecrets(c, available) }));
3001
+ });
3002
+ f.post("/api/connectors", async (req, reply) => {
3003
+ const parsed = CreateConnectorRequest.safeParse(req.body);
3004
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
3005
+ if (store.getConnectorByName(parsed.data.name)) {
3006
+ return reply.code(409).send({ error: `A connector named "${parsed.data.name}" already exists` });
3007
+ }
3008
+ return store.createConnector(parsed.data);
3009
+ });
3010
+ f.patch("/api/connectors/:id", async (req, reply) => {
3011
+ const parsed = UpdateConnectorRequest.safeParse(req.body);
3012
+ if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
3013
+ const updated = store.updateConnector(req.params.id, parsed.data);
3014
+ if (!updated) return reply.code(404).send({ error: "No such connector" });
3015
+ return updated;
3016
+ });
3017
+ f.delete("/api/connectors/:id", async (req, reply) => {
3018
+ if (!store.getConnector(req.params.id)) return reply.code(404).send({ error: "No such connector" });
3019
+ store.deleteConnector(req.params.id);
3020
+ return { ok: true };
3021
+ });
3022
+ f.post("/api/connectors/:id/test", async (req, reply) => {
3023
+ const connector = store.getConnector(req.params.id);
3024
+ if (!connector) return reply.code(404).send({ error: "No such connector" });
3025
+ const refs = extractSecretRefs(connector.config);
3026
+ const missing = computeMissingSecrets(connector, new Set(app.secrets?.list() ?? []));
3027
+ if (missing.length) {
3028
+ return { ok: false, tools: [], error: `missing secret(s): ${missing.join(", ")}` };
3029
+ }
3030
+ try {
3031
+ const secrets = refs.length && app.secrets ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
3032
+ return await probeConnector(buildMcpServerConfig(connector, secrets));
3033
+ } catch (err) {
3034
+ return { ok: false, tools: [], error: err.message };
3035
+ }
3036
+ });
2621
3037
  f.get("/api/skills", async () => store.listSkills());
2622
3038
  f.post("/api/skills", async (req, reply) => {
2623
3039
  const parsed = CreateSkillRequest.safeParse(req.body);
@@ -2855,7 +3271,7 @@ function registerOpsRoutes(f, app) {
2855
3271
  }
2856
3272
 
2857
3273
  // daemon/src/api/server.ts
2858
- var log10 = logger("server");
3274
+ var log11 = logger("server");
2859
3275
  var require_ = createRequire(import.meta.url);
2860
3276
  function resolveWebDist() {
2861
3277
  return findWebDist(
@@ -2972,9 +3388,9 @@ async function startServer(opts = {}) {
2972
3388
  if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
2973
3389
  return reply.sendFile("index.html");
2974
3390
  });
2975
- log10.info(`serving UI from ${dist}`);
3391
+ log11.info(`serving UI from ${dist}`);
2976
3392
  } else {
2977
- log10.warn("web UI not built \u2014 run `pnpm --filter @antbot/ui build`");
3393
+ log11.warn("web UI not built \u2014 run `pnpm --filter @antbot/ui build`");
2978
3394
  fastify.setNotFoundHandler((req, reply) => {
2979
3395
  if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
2980
3396
  return reply.type("text/html").send(
@@ -2989,7 +3405,7 @@ async function startServer(opts = {}) {
2989
3405
  }
2990
3406
  }
2991
3407
  fastify.setErrorHandler((err, _req, reply) => {
2992
- log10.error("request failed", err);
3408
+ log11.error("request failed", err);
2993
3409
  const e = err;
2994
3410
  const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
2995
3411
  reply.code(code).send({ error: e.message ?? "Internal error" });
@@ -2999,13 +3415,13 @@ async function startServer(opts = {}) {
2999
3415
  await fastify.listen({ port, host });
3000
3416
  const url = `http://${host}:${port}`;
3001
3417
  const delivered = drainMailbox(app);
3002
- if (delivered) log10.info(`redelivered ${delivered} queued handoff message(s)`);
3418
+ if (delivered) log11.info(`redelivered ${delivered} queued handoff message(s)`);
3003
3419
  const stale = app.store.listBots().filter((b2) => b2.state === "running" || b2.state === "queued");
3004
3420
  for (const b2 of stale) app.store.updateBot(b2.id, { state: "idle" });
3005
3421
  app.db.prepare(`UPDATE messages SET streaming=0 WHERE streaming=1`).run();
3006
3422
  app.db.prepare(`UPDATE approvals SET status='expired', reason='Daemon restarted' WHERE status='pending'`).run();
3007
3423
  app.db.prepare(`UPDATE routine_runs SET status='interrupted', finished_at=? WHERE status='running'`).run(Date.now());
3008
- log10.info(`ant-bot listening on ${url}`);
3424
+ log11.info(`ant-bot listening on ${url}`);
3009
3425
  return {
3010
3426
  fastify,
3011
3427
  app,