@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/CHANGELOG.md +41 -0
- package/README.md +9 -5
- package/dist/{browser-4LFD7NX2.js → browser-NOWM4S6C.js} +32 -6
- package/dist/browser-NOWM4S6C.js.map +7 -0
- package/dist/{chunk-4XML2GPY.js → chunk-RMDMUCVS.js} +50 -4
- package/dist/chunk-RMDMUCVS.js.map +7 -0
- package/dist/index.js +229 -1
- package/dist/index.js.map +3 -3
- package/dist/server.js +427 -11
- package/dist/server.js.map +4 -4
- package/package.json +1 -1
- package/web/dist/assets/index-B90EjIDl.css +2 -0
- package/web/dist/assets/index-nnI9BOXM.js +130 -0
- package/web/dist/index.html +2 -2
- package/dist/browser-4LFD7NX2.js.map +0 -7
- package/dist/chunk-4XML2GPY.js.map +0 -7
- package/web/dist/assets/index-BCKrSQY_.js +0 -130
- package/web/dist/assets/index-e3_GBdXi.css +0 -2
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
|
-
|
|
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
|
-
/**
|
|
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-
|
|
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
|
|
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
|
-
|
|
3391
|
+
log11.info(`serving UI from ${dist}`);
|
|
2976
3392
|
} else {
|
|
2977
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
3424
|
+
log11.info(`ant-bot listening on ${url}`);
|
|
3009
3425
|
return {
|
|
3010
3426
|
fastify,
|
|
3011
3427
|
app,
|