@agentconnect.md/daemon 1.0.0-rc.25 → 1.0.0-rc.27
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/bridge-Dj8U1Jp7.js +9900 -0
- package/dist/bridge-Dj8U1Jp7.js.map +1 -0
- package/dist/index.js +652 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { EventEmitter } from "node:events";
|
|
|
4
4
|
import childProcess, { execFile, spawn } from "node:child_process";
|
|
5
5
|
import * as sp from "node:path";
|
|
6
6
|
import path, { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
7
|
-
import fs, { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, stat, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
|
|
7
|
+
import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, stat, unlinkSync, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
9
|
import { stripVTControlCharacters } from "node:util";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,9 +12,10 @@ import { createInterface } from "node:readline";
|
|
|
12
12
|
import { freemem, homedir, hostname, loadavg, totalmem, type } from "node:os";
|
|
13
13
|
import { spawn as spawn$1 } from "child_process";
|
|
14
14
|
import { Readable, Writable } from "node:stream";
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
16
16
|
import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
|
|
17
17
|
import { DatabaseSync } from "node:sqlite";
|
|
18
|
+
import net from "node:net";
|
|
18
19
|
//#region \0rolldown/runtime.js
|
|
19
20
|
var __create = Object.create;
|
|
20
21
|
var __defProp$1 = Object.defineProperty;
|
|
@@ -3599,6 +3600,7 @@ const string$1 = (params) => {
|
|
|
3599
3600
|
const integer = /^-?\d+$/;
|
|
3600
3601
|
const number$1 = /^-?\d+(?:\.\d+)?$/;
|
|
3601
3602
|
const boolean$1 = /^(?:true|false)$/i;
|
|
3603
|
+
const _null$2 = /^null$/i;
|
|
3602
3604
|
const lowercase = /^[^A-Z]*$/;
|
|
3603
3605
|
const uppercase = /^[^a-z]*$/;
|
|
3604
3606
|
//#endregion
|
|
@@ -4411,6 +4413,22 @@ const $ZodBoolean = /*@__PURE__*/ $constructor("$ZodBoolean", (inst, def) => {
|
|
|
4411
4413
|
return payload;
|
|
4412
4414
|
};
|
|
4413
4415
|
});
|
|
4416
|
+
const $ZodNull = /*@__PURE__*/ $constructor("$ZodNull", (inst, def) => {
|
|
4417
|
+
$ZodType.init(inst, def);
|
|
4418
|
+
inst._zod.pattern = _null$2;
|
|
4419
|
+
inst._zod.values = /* @__PURE__ */ new Set([null]);
|
|
4420
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
4421
|
+
const input = payload.value;
|
|
4422
|
+
if (input === null) return payload;
|
|
4423
|
+
payload.issues.push({
|
|
4424
|
+
expected: "null",
|
|
4425
|
+
code: "invalid_type",
|
|
4426
|
+
input,
|
|
4427
|
+
inst
|
|
4428
|
+
});
|
|
4429
|
+
return payload;
|
|
4430
|
+
};
|
|
4431
|
+
});
|
|
4414
4432
|
const $ZodUnknown = /*@__PURE__*/ $constructor("$ZodUnknown", (inst, def) => {
|
|
4415
4433
|
$ZodType.init(inst, def);
|
|
4416
4434
|
inst._zod.parse = (payload) => payload;
|
|
@@ -5240,6 +5258,9 @@ function handlePipeResult(left, next, ctx) {
|
|
|
5240
5258
|
fallback: left.fallback
|
|
5241
5259
|
}, ctx);
|
|
5242
5260
|
}
|
|
5261
|
+
const $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
|
|
5262
|
+
$ZodPipe.init(inst, def);
|
|
5263
|
+
});
|
|
5243
5264
|
const $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
|
|
5244
5265
|
$ZodType.init(inst, def);
|
|
5245
5266
|
defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
|
|
@@ -5633,6 +5654,13 @@ function _boolean(Class, params) {
|
|
|
5633
5654
|
});
|
|
5634
5655
|
}
|
|
5635
5656
|
// @__NO_SIDE_EFFECTS__
|
|
5657
|
+
function _null$1(Class, params) {
|
|
5658
|
+
return new Class({
|
|
5659
|
+
type: "null",
|
|
5660
|
+
...normalizeParams(params)
|
|
5661
|
+
});
|
|
5662
|
+
}
|
|
5663
|
+
// @__NO_SIDE_EFFECTS__
|
|
5636
5664
|
function _unknown(Class) {
|
|
5637
5665
|
return new Class({ type: "unknown" });
|
|
5638
5666
|
}
|
|
@@ -5799,6 +5827,17 @@ function _array(Class, element, params) {
|
|
|
5799
5827
|
});
|
|
5800
5828
|
}
|
|
5801
5829
|
// @__NO_SIDE_EFFECTS__
|
|
5830
|
+
function _custom(Class, fn, _params) {
|
|
5831
|
+
const norm = normalizeParams(_params);
|
|
5832
|
+
norm.abort ?? (norm.abort = true);
|
|
5833
|
+
return new Class({
|
|
5834
|
+
type: "custom",
|
|
5835
|
+
check: "custom",
|
|
5836
|
+
fn,
|
|
5837
|
+
...norm
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
// @__NO_SIDE_EFFECTS__
|
|
5802
5841
|
function _refine(Class, fn, _params) {
|
|
5803
5842
|
return new Class({
|
|
5804
5843
|
type: "custom",
|
|
@@ -6181,6 +6220,13 @@ const numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
6181
6220
|
const booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
6182
6221
|
json.type = "boolean";
|
|
6183
6222
|
};
|
|
6223
|
+
const nullProcessor = (_schema, ctx, json, _params) => {
|
|
6224
|
+
if (ctx.target === "openapi-3.0") {
|
|
6225
|
+
json.type = "string";
|
|
6226
|
+
json.nullable = true;
|
|
6227
|
+
json.enum = [null];
|
|
6228
|
+
} else json.type = "null";
|
|
6229
|
+
};
|
|
6184
6230
|
const neverProcessor = (_schema, _ctx, json, _params) => {
|
|
6185
6231
|
json.not = {};
|
|
6186
6232
|
};
|
|
@@ -6874,6 +6920,14 @@ const ZodBoolean = /*@__PURE__*/ $constructor("ZodBoolean", (inst, def) => {
|
|
|
6874
6920
|
function boolean(params) {
|
|
6875
6921
|
return /* @__PURE__ */ _boolean(ZodBoolean, params);
|
|
6876
6922
|
}
|
|
6923
|
+
const ZodNull = /*@__PURE__*/ $constructor("ZodNull", (inst, def) => {
|
|
6924
|
+
$ZodNull.init(inst, def);
|
|
6925
|
+
ZodType.init(inst, def);
|
|
6926
|
+
inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
|
|
6927
|
+
});
|
|
6928
|
+
function _null(params) {
|
|
6929
|
+
return /* @__PURE__ */ _null$1(ZodNull, params);
|
|
6930
|
+
}
|
|
6877
6931
|
const ZodUnknown = /*@__PURE__*/ $constructor("ZodUnknown", (inst, def) => {
|
|
6878
6932
|
$ZodUnknown.init(inst, def);
|
|
6879
6933
|
ZodType.init(inst, def);
|
|
@@ -6987,6 +7041,14 @@ function object(shape, params) {
|
|
|
6987
7041
|
...normalizeParams(params)
|
|
6988
7042
|
});
|
|
6989
7043
|
}
|
|
7044
|
+
function looseObject(shape, params) {
|
|
7045
|
+
return new ZodObject({
|
|
7046
|
+
type: "object",
|
|
7047
|
+
shape,
|
|
7048
|
+
catchall: unknown(),
|
|
7049
|
+
...normalizeParams(params)
|
|
7050
|
+
});
|
|
7051
|
+
}
|
|
6990
7052
|
const ZodUnion = /*@__PURE__*/ $constructor("ZodUnion", (inst, def) => {
|
|
6991
7053
|
$ZodUnion.init(inst, def);
|
|
6992
7054
|
ZodType.init(inst, def);
|
|
@@ -7241,6 +7303,10 @@ function pipe(in_, out) {
|
|
|
7241
7303
|
out
|
|
7242
7304
|
});
|
|
7243
7305
|
}
|
|
7306
|
+
const ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
|
|
7307
|
+
ZodPipe.init(inst, def);
|
|
7308
|
+
$ZodPreprocess.init(inst, def);
|
|
7309
|
+
});
|
|
7244
7310
|
const ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
|
|
7245
7311
|
$ZodReadonly.init(inst, def);
|
|
7246
7312
|
ZodType.init(inst, def);
|
|
@@ -7258,12 +7324,22 @@ const ZodCustom = /*@__PURE__*/ $constructor("ZodCustom", (inst, def) => {
|
|
|
7258
7324
|
ZodType.init(inst, def);
|
|
7259
7325
|
inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
|
|
7260
7326
|
});
|
|
7327
|
+
function custom(fn, _params) {
|
|
7328
|
+
return /* @__PURE__ */ _custom(ZodCustom, fn ?? (() => true), _params);
|
|
7329
|
+
}
|
|
7261
7330
|
function refine(fn, _params = {}) {
|
|
7262
7331
|
return /* @__PURE__ */ _refine(ZodCustom, fn, _params);
|
|
7263
7332
|
}
|
|
7264
7333
|
function superRefine(fn, params) {
|
|
7265
7334
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
7266
7335
|
}
|
|
7336
|
+
function preprocess(fn, schema) {
|
|
7337
|
+
return new ZodPreprocess({
|
|
7338
|
+
type: "pipe",
|
|
7339
|
+
in: transform(fn),
|
|
7340
|
+
out: schema
|
|
7341
|
+
});
|
|
7342
|
+
}
|
|
7267
7343
|
//#endregion
|
|
7268
7344
|
//#region src/config/config-schema.ts
|
|
7269
7345
|
const RuntimeDefSchema = object({
|
|
@@ -7328,9 +7404,21 @@ function registryCachePath(root) {
|
|
|
7328
7404
|
function logsDir(root) {
|
|
7329
7405
|
return join(root, "logs");
|
|
7330
7406
|
}
|
|
7407
|
+
/**
|
|
7408
|
+
* Unix-domain socket the daemon's MCP control server listens on. The stdio
|
|
7409
|
+
* `agentconnect mcp-bridge` subprocess (spawned by the agent harness) connects
|
|
7410
|
+
* here to forward tool calls back to the daemon. Kept short (macOS caps UDS
|
|
7411
|
+
* paths at ~104 bytes) and under `run/` so it's separate from durable state.
|
|
7412
|
+
*/
|
|
7413
|
+
function mcpSocketPath(root) {
|
|
7414
|
+
return join(root, "run", "mcp.sock");
|
|
7415
|
+
}
|
|
7331
7416
|
function daemonLogPath(root) {
|
|
7332
7417
|
return join(root, "logs", "daemon.log");
|
|
7333
7418
|
}
|
|
7419
|
+
function lockPath(root) {
|
|
7420
|
+
return join(root, "daemon.lock");
|
|
7421
|
+
}
|
|
7334
7422
|
//#endregion
|
|
7335
7423
|
//#region src/config/load-config.ts
|
|
7336
7424
|
function loadConfig(opts = {}) {
|
|
@@ -16789,10 +16877,10 @@ var AcpHost = class {
|
|
|
16789
16877
|
this.canLoad = init.agentCapabilities?.loadSession ?? false;
|
|
16790
16878
|
this.opts.log?.debug(`acp: agent initialized (loadSession capability=${this.canLoad})`);
|
|
16791
16879
|
}
|
|
16792
|
-
async newSession(cwd) {
|
|
16880
|
+
async newSession(cwd, mcpServers = []) {
|
|
16793
16881
|
const res = await this.conn.agent.request(methods.agent.session.new, {
|
|
16794
16882
|
cwd,
|
|
16795
|
-
mcpServers
|
|
16883
|
+
mcpServers
|
|
16796
16884
|
});
|
|
16797
16885
|
this.live.add(res.sessionId);
|
|
16798
16886
|
return res.sessionId;
|
|
@@ -17104,6 +17192,22 @@ const NormalizedMessageRef = object({
|
|
|
17104
17192
|
seenUpToSeq: number().int()
|
|
17105
17193
|
});
|
|
17106
17194
|
/**
|
|
17195
|
+
* Where the agent runs. Two modes; the **path is always daemon-generated** —
|
|
17196
|
+
* never specified by the caller (UX picks the mode, the machine owns the dir).
|
|
17197
|
+
*
|
|
17198
|
+
* - `scratch`: a fresh empty working dir on the machine, no repo.
|
|
17199
|
+
* - `github`: the daemon clones `gitRepo` @ `branch` and runs the agent in
|
|
17200
|
+
* `agentDir` (a subdir of the repo, repo-root if omitted). **Multiple agents
|
|
17201
|
+
* may share one repo** — they differ by `agentDir`, so the repo is not an
|
|
17202
|
+
* owned entity, just shared config on each agent.
|
|
17203
|
+
*/
|
|
17204
|
+
const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scratch") }), object({
|
|
17205
|
+
mode: literal("github"),
|
|
17206
|
+
gitRepo: string(),
|
|
17207
|
+
branch: string().default("main"),
|
|
17208
|
+
agentDir: string().optional()
|
|
17209
|
+
})]);
|
|
17210
|
+
/**
|
|
17107
17211
|
* The editable agent definition the CP owns and the daemon needs to run it:
|
|
17108
17212
|
* prompt + runtime selection. Raft ships this in the launch config and the
|
|
17109
17213
|
* daemon synthesizes the system prompt locally; `description` IS the prompt.
|
|
@@ -17114,6 +17218,7 @@ const AgentSpec = object({
|
|
|
17114
17218
|
model: string().optional(),
|
|
17115
17219
|
reasoningEffort: string().optional(),
|
|
17116
17220
|
executionMode: string().optional(),
|
|
17221
|
+
workspace: AgentWorkspace.optional(),
|
|
17117
17222
|
env: record(string(), string()).optional()
|
|
17118
17223
|
});
|
|
17119
17224
|
const AgentLaunch = object({
|
|
@@ -19499,9 +19604,9 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
19499
19604
|
const EventEmitter$3 = __require("events");
|
|
19500
19605
|
const https$3 = __require("https");
|
|
19501
19606
|
const http$7 = __require("http");
|
|
19502
|
-
const net = __require("net");
|
|
19607
|
+
const net$1 = __require("net");
|
|
19503
19608
|
const tls = __require("tls");
|
|
19504
|
-
const { randomBytes, createHash: createHash$1 } = __require("crypto");
|
|
19609
|
+
const { randomBytes: randomBytes$1, createHash: createHash$1 } = __require("crypto");
|
|
19505
19610
|
const { Duplex: Duplex$2, Readable: Readable$1 } = __require("stream");
|
|
19506
19611
|
const { URL: URL$1 } = __require("url");
|
|
19507
19612
|
const PerMessageDeflate = require_permessage_deflate();
|
|
@@ -20068,7 +20173,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
20068
20173
|
}
|
|
20069
20174
|
}
|
|
20070
20175
|
const defaultPort = isSecure ? 443 : 80;
|
|
20071
|
-
const key = randomBytes(16).toString("base64");
|
|
20176
|
+
const key = randomBytes$1(16).toString("base64");
|
|
20072
20177
|
const request = isSecure ? https$3.request : http$7.request;
|
|
20073
20178
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
20074
20179
|
let perMessageDeflate;
|
|
@@ -20245,7 +20350,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
20245
20350
|
*/
|
|
20246
20351
|
function netConnect(options) {
|
|
20247
20352
|
options.path = options.socketPath;
|
|
20248
|
-
return net.connect(options);
|
|
20353
|
+
return net$1.connect(options);
|
|
20249
20354
|
}
|
|
20250
20355
|
/**
|
|
20251
20356
|
* Create a `tls.TLSSocket` and initiate a connection.
|
|
@@ -20256,7 +20361,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
20256
20361
|
*/
|
|
20257
20362
|
function tlsConnect(options) {
|
|
20258
20363
|
options.path = void 0;
|
|
20259
|
-
if (!options.servername && options.servername !== "") options.servername = net.isIP(options.host) ? "" : options.host;
|
|
20364
|
+
if (!options.servername && options.servername !== "") options.servername = net$1.isIP(options.host) ? "" : options.host;
|
|
20260
20365
|
return tls.connect(options);
|
|
20261
20366
|
}
|
|
20262
20367
|
/**
|
|
@@ -23158,7 +23263,12 @@ var SessionManager = class {
|
|
|
23158
23263
|
const host = await this.deps.hostFor(agentId);
|
|
23159
23264
|
if (!rec || !rec.acpSessionId) {
|
|
23160
23265
|
const cwd = await prepareWorkspace(agent);
|
|
23161
|
-
const
|
|
23266
|
+
const mcpServers = this.deps.mcpServersFor?.({
|
|
23267
|
+
agent,
|
|
23268
|
+
channel: msg.channel,
|
|
23269
|
+
thread
|
|
23270
|
+
}) ?? [];
|
|
23271
|
+
const acpSessionId = await host.newSession(cwd, mcpServers);
|
|
23162
23272
|
rec = {
|
|
23163
23273
|
key,
|
|
23164
23274
|
agentId,
|
|
@@ -23179,7 +23289,12 @@ var SessionManager = class {
|
|
|
23179
23289
|
resumed = true;
|
|
23180
23290
|
} catch {}
|
|
23181
23291
|
if (!resumed) {
|
|
23182
|
-
const
|
|
23292
|
+
const mcpServers = this.deps.mcpServersFor?.({
|
|
23293
|
+
agent,
|
|
23294
|
+
channel: msg.channel,
|
|
23295
|
+
thread
|
|
23296
|
+
}) ?? [];
|
|
23297
|
+
const acpSessionId = await host.newSession(cwd, mcpServers);
|
|
23183
23298
|
rec = {
|
|
23184
23299
|
...rec,
|
|
23185
23300
|
acpSessionId,
|
|
@@ -23192,7 +23307,7 @@ var SessionManager = class {
|
|
|
23192
23307
|
}
|
|
23193
23308
|
const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
|
|
23194
23309
|
const blocks = [];
|
|
23195
|
-
const context = gap.slice(0, -1);
|
|
23310
|
+
const context = gap.slice(0, -1).filter((e) => e.sender !== agentId);
|
|
23196
23311
|
if (context.length > 0) {
|
|
23197
23312
|
const ctxText = context.map((e) => `[${e.sender}] ${e.text}`).join("\n");
|
|
23198
23313
|
blocks.push({
|
|
@@ -23219,6 +23334,306 @@ function tsOf(msg) {
|
|
|
23219
23334
|
return parts[parts.length - 1] ?? "0";
|
|
23220
23335
|
}
|
|
23221
23336
|
//#endregion
|
|
23337
|
+
//#region src/mcp/ipc.ts
|
|
23338
|
+
/** Frame a message for the wire: compact JSON + a single trailing newline. */
|
|
23339
|
+
function encodeFrame(msg) {
|
|
23340
|
+
return JSON.stringify(msg) + "\n";
|
|
23341
|
+
}
|
|
23342
|
+
/**
|
|
23343
|
+
* Split a buffer of newline-delimited frames into parsed objects plus the
|
|
23344
|
+
* trailing partial line. Callers keep `rest` and prepend it to the next chunk.
|
|
23345
|
+
*
|
|
23346
|
+
* Per-line tolerant: a malformed (non-JSON) line is skipped, not thrown — so one
|
|
23347
|
+
* bad frame can't crash a stream reader or discard the good frames batched
|
|
23348
|
+
* alongside it in the same chunk. `onError` lets callers log dropped lines.
|
|
23349
|
+
*/
|
|
23350
|
+
function decodeFrames(buf, onError) {
|
|
23351
|
+
const parts = buf.split("\n");
|
|
23352
|
+
const rest = parts.pop() ?? "";
|
|
23353
|
+
const messages = [];
|
|
23354
|
+
for (const line of parts) {
|
|
23355
|
+
const trimmed = line.trim();
|
|
23356
|
+
if (!trimmed) continue;
|
|
23357
|
+
try {
|
|
23358
|
+
messages.push(JSON.parse(trimmed));
|
|
23359
|
+
} catch (err) {
|
|
23360
|
+
onError?.(trimmed, err);
|
|
23361
|
+
}
|
|
23362
|
+
}
|
|
23363
|
+
return {
|
|
23364
|
+
messages,
|
|
23365
|
+
rest
|
|
23366
|
+
};
|
|
23367
|
+
}
|
|
23368
|
+
//#endregion
|
|
23369
|
+
//#region src/mcp/ops.ts
|
|
23370
|
+
/**
|
|
23371
|
+
* Execute one tool call inside the daemon and return a plain result object (the
|
|
23372
|
+
* bridge wraps it into an MCP `CallToolResult`). Throws on bad input or a
|
|
23373
|
+
* missing connection — the caller turns that into an MCP `isError` result.
|
|
23374
|
+
*/
|
|
23375
|
+
async function executeTool(ctx, name, args, deps) {
|
|
23376
|
+
const gw = deps.gatewayFor(ctx.integrationId);
|
|
23377
|
+
if (!gw) throw new Error(`no live Slack connection for integration ${ctx.integrationId}`);
|
|
23378
|
+
switch (name) {
|
|
23379
|
+
case "sendSlackMessage": {
|
|
23380
|
+
const text = requireString(args, "text");
|
|
23381
|
+
const channel = optionalString(args, "channel") ?? ctx.channel;
|
|
23382
|
+
const thread = "thread" in args ? optionalString(args, "thread") || void 0 : ctx.thread;
|
|
23383
|
+
const ts = await gw.postMessage(channel, text, thread) ?? `local-${deps.now()}`;
|
|
23384
|
+
deps.recordOutbound(ctx, channel, thread, text, ts);
|
|
23385
|
+
return {
|
|
23386
|
+
ok: true,
|
|
23387
|
+
channel,
|
|
23388
|
+
thread: thread ?? null,
|
|
23389
|
+
ts
|
|
23390
|
+
};
|
|
23391
|
+
}
|
|
23392
|
+
case "getCurrentChannel": {
|
|
23393
|
+
const info = await gw.getChannelInfo(ctx.channel).catch(() => void 0);
|
|
23394
|
+
return {
|
|
23395
|
+
channel: ctx.channel,
|
|
23396
|
+
thread: ctx.thread,
|
|
23397
|
+
name: info?.name ?? null,
|
|
23398
|
+
isIm: info?.isIm ?? null
|
|
23399
|
+
};
|
|
23400
|
+
}
|
|
23401
|
+
case "listChannelMembers": {
|
|
23402
|
+
const channel = optionalString(args, "channel") ?? ctx.channel;
|
|
23403
|
+
return {
|
|
23404
|
+
channel,
|
|
23405
|
+
members: await gw.listMembers(channel)
|
|
23406
|
+
};
|
|
23407
|
+
}
|
|
23408
|
+
case "listChannels": return { channels: await gw.listChannels() };
|
|
23409
|
+
case "getUserProfile": {
|
|
23410
|
+
const user = requireString(args, "user");
|
|
23411
|
+
return await gw.getUserProfile(user);
|
|
23412
|
+
}
|
|
23413
|
+
default: throw new Error(`unknown tool: ${name}`);
|
|
23414
|
+
}
|
|
23415
|
+
}
|
|
23416
|
+
function requireString(args, key) {
|
|
23417
|
+
const v = args[key];
|
|
23418
|
+
if (typeof v !== "string" || v.length === 0) throw new Error(`missing required string argument: ${key}`);
|
|
23419
|
+
return v;
|
|
23420
|
+
}
|
|
23421
|
+
function optionalString(args, key) {
|
|
23422
|
+
const v = args[key];
|
|
23423
|
+
if (v === void 0 || v === null) return void 0;
|
|
23424
|
+
if (typeof v !== "string") throw new Error(`argument ${key} must be a string`);
|
|
23425
|
+
return v;
|
|
23426
|
+
}
|
|
23427
|
+
//#endregion
|
|
23428
|
+
//#region src/mcp/control-server.ts
|
|
23429
|
+
/**
|
|
23430
|
+
* The daemon-hosted half of MCP. It owns all tool logic, the registry of live
|
|
23431
|
+
* sessions, and the Unix-domain socket the `mcp-bridge` subprocesses connect to.
|
|
23432
|
+
* "The MCP server is the daemon itself" — the bridge is only a stdio↔socket pipe.
|
|
23433
|
+
*/
|
|
23434
|
+
var McpControlServer = class {
|
|
23435
|
+
deps;
|
|
23436
|
+
server;
|
|
23437
|
+
sessions = /* @__PURE__ */ new Map();
|
|
23438
|
+
conns = /* @__PURE__ */ new Set();
|
|
23439
|
+
constructor(deps) {
|
|
23440
|
+
this.deps = deps;
|
|
23441
|
+
}
|
|
23442
|
+
/**
|
|
23443
|
+
* Register a session's tool set and return an opaque token. The token is
|
|
23444
|
+
* embedded in the bridge's env at `session/new`; the bridge presents it on
|
|
23445
|
+
* every IPC request so we can resolve the channel/thread/agent binding.
|
|
23446
|
+
*/
|
|
23447
|
+
register(ctx) {
|
|
23448
|
+
const token = randomBytes(18).toString("base64url");
|
|
23449
|
+
this.sessions.set(token, ctx);
|
|
23450
|
+
return token;
|
|
23451
|
+
}
|
|
23452
|
+
unregister(token) {
|
|
23453
|
+
this.sessions.delete(token);
|
|
23454
|
+
}
|
|
23455
|
+
async start() {
|
|
23456
|
+
if (this.server) return;
|
|
23457
|
+
const path = this.deps.socketPath;
|
|
23458
|
+
const dir = dirname(path);
|
|
23459
|
+
mkdirSync(dir, {
|
|
23460
|
+
recursive: true,
|
|
23461
|
+
mode: 448
|
|
23462
|
+
});
|
|
23463
|
+
try {
|
|
23464
|
+
chmodSync(dir, 448);
|
|
23465
|
+
} catch {}
|
|
23466
|
+
rmSync(path, { force: true });
|
|
23467
|
+
const server = net.createServer((socket) => this.onConnection(socket));
|
|
23468
|
+
this.server = server;
|
|
23469
|
+
await new Promise((resolve, reject) => {
|
|
23470
|
+
const onStartupError = (err) => reject(err);
|
|
23471
|
+
server.once("error", onStartupError);
|
|
23472
|
+
server.listen(path, () => {
|
|
23473
|
+
server.off("error", onStartupError);
|
|
23474
|
+
server.on("error", (err) => this.deps.log?.error(`mcp: control server error: ${err.message}`));
|
|
23475
|
+
resolve();
|
|
23476
|
+
});
|
|
23477
|
+
});
|
|
23478
|
+
try {
|
|
23479
|
+
chmodSync(path, 384);
|
|
23480
|
+
} catch {}
|
|
23481
|
+
this.deps.log?.info(`mcp: control socket listening at ${path}`);
|
|
23482
|
+
}
|
|
23483
|
+
onConnection(socket) {
|
|
23484
|
+
socket.setEncoding("utf8");
|
|
23485
|
+
this.conns.add(socket);
|
|
23486
|
+
let buf = "";
|
|
23487
|
+
socket.on("data", (chunk) => {
|
|
23488
|
+
buf += chunk;
|
|
23489
|
+
const { messages, rest } = decodeFrames(buf, (line) => this.deps.log?.debug(`mcp: dropping malformed frame: ${line.slice(0, 120)}`));
|
|
23490
|
+
buf = rest;
|
|
23491
|
+
for (const req of messages) this.handle(req, socket);
|
|
23492
|
+
});
|
|
23493
|
+
socket.on("error", (err) => this.deps.log?.debug(`mcp: socket error: ${err.message}`));
|
|
23494
|
+
socket.on("close", () => this.conns.delete(socket));
|
|
23495
|
+
}
|
|
23496
|
+
async handle(req, socket) {
|
|
23497
|
+
const reply = (res) => {
|
|
23498
|
+
if (!socket.destroyed) socket.write(encodeFrame(res));
|
|
23499
|
+
};
|
|
23500
|
+
const ctx = this.sessions.get(req.token);
|
|
23501
|
+
if (!ctx) return reply({
|
|
23502
|
+
id: req.id,
|
|
23503
|
+
ok: false,
|
|
23504
|
+
error: "unknown or expired session token"
|
|
23505
|
+
});
|
|
23506
|
+
try {
|
|
23507
|
+
if (req.op === "listTools") {
|
|
23508
|
+
reply({
|
|
23509
|
+
id: req.id,
|
|
23510
|
+
ok: true,
|
|
23511
|
+
result: { tools: ctx.tools }
|
|
23512
|
+
});
|
|
23513
|
+
return;
|
|
23514
|
+
}
|
|
23515
|
+
const result = await executeTool(ctx, req.name, req.args ?? {}, this.deps);
|
|
23516
|
+
reply({
|
|
23517
|
+
id: req.id,
|
|
23518
|
+
ok: true,
|
|
23519
|
+
result
|
|
23520
|
+
});
|
|
23521
|
+
} catch (err) {
|
|
23522
|
+
reply({
|
|
23523
|
+
id: req.id,
|
|
23524
|
+
ok: false,
|
|
23525
|
+
error: err.message
|
|
23526
|
+
});
|
|
23527
|
+
}
|
|
23528
|
+
}
|
|
23529
|
+
async stop() {
|
|
23530
|
+
this.sessions.clear();
|
|
23531
|
+
const server = this.server;
|
|
23532
|
+
if (!server) return;
|
|
23533
|
+
this.server = void 0;
|
|
23534
|
+
for (const s of this.conns) s.destroy();
|
|
23535
|
+
this.conns.clear();
|
|
23536
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
23537
|
+
rmSync(this.deps.socketPath, { force: true });
|
|
23538
|
+
}
|
|
23539
|
+
};
|
|
23540
|
+
//#endregion
|
|
23541
|
+
//#region src/mcp/inject.ts
|
|
23542
|
+
/**
|
|
23543
|
+
* Build the `mcpServers` entry that makes the agent harness spawn our stdio
|
|
23544
|
+
* bridge. We reuse the *current* interpreter + CLI entry (so it works under both
|
|
23545
|
+
* `tsx` in dev and plain `node` in prod) and invoke the hidden `mcp-bridge`
|
|
23546
|
+
* subcommand. The socket path and per-session token travel via env — they reach
|
|
23547
|
+
* only the harness-spawned subprocess, never the model.
|
|
23548
|
+
*/
|
|
23549
|
+
function buildMcpServers(opts) {
|
|
23550
|
+
return [{
|
|
23551
|
+
name: "agentconnect",
|
|
23552
|
+
command: process.execPath,
|
|
23553
|
+
args: [
|
|
23554
|
+
...process.execArgv,
|
|
23555
|
+
opts.cliEntry,
|
|
23556
|
+
"mcp-bridge"
|
|
23557
|
+
],
|
|
23558
|
+
env: [{
|
|
23559
|
+
name: "AC_MCP_ENDPOINT",
|
|
23560
|
+
value: opts.socketPath
|
|
23561
|
+
}, {
|
|
23562
|
+
name: "AC_MCP_TOKEN",
|
|
23563
|
+
value: opts.token
|
|
23564
|
+
}]
|
|
23565
|
+
}];
|
|
23566
|
+
}
|
|
23567
|
+
//#endregion
|
|
23568
|
+
//#region src/mcp/tools.ts
|
|
23569
|
+
const obj = (properties, required = []) => ({
|
|
23570
|
+
type: "object",
|
|
23571
|
+
properties,
|
|
23572
|
+
required,
|
|
23573
|
+
additionalProperties: false
|
|
23574
|
+
});
|
|
23575
|
+
/**
|
|
23576
|
+
* Tools injected when an agent has a Slack integration. `sendSlackMessage` is
|
|
23577
|
+
* the active-messaging tool from the design (token held by the daemon, invisible
|
|
23578
|
+
* to the model); the rest are channel/user read helpers.
|
|
23579
|
+
*/
|
|
23580
|
+
const SLACK_TOOLS = [
|
|
23581
|
+
{
|
|
23582
|
+
name: "sendSlackMessage",
|
|
23583
|
+
description: "Send a message to a Slack channel. Omit `channel` to post in the channel this conversation is happening in. Omit `thread` to reply in the current thread; pass an empty string to post to the channel root instead.",
|
|
23584
|
+
inputSchema: obj({
|
|
23585
|
+
text: {
|
|
23586
|
+
type: "string",
|
|
23587
|
+
description: "Message body, in Slack mrkdwn."
|
|
23588
|
+
},
|
|
23589
|
+
channel: {
|
|
23590
|
+
type: "string",
|
|
23591
|
+
description: "Target channel ID (e.g. C0123ABC). Defaults to the current channel."
|
|
23592
|
+
},
|
|
23593
|
+
thread: {
|
|
23594
|
+
type: "string",
|
|
23595
|
+
description: "Thread timestamp to reply under. Defaults to the current thread; \"\" posts to the channel root."
|
|
23596
|
+
}
|
|
23597
|
+
}, ["text"])
|
|
23598
|
+
},
|
|
23599
|
+
{
|
|
23600
|
+
name: "getCurrentChannel",
|
|
23601
|
+
description: "Return the Slack channel (and thread) this conversation is bound to, including the channel name when available.",
|
|
23602
|
+
inputSchema: obj({})
|
|
23603
|
+
},
|
|
23604
|
+
{
|
|
23605
|
+
name: "listChannelMembers",
|
|
23606
|
+
description: "List the users and bots in a channel (id, name, is_bot). Omit `channel` to list members of the current channel.",
|
|
23607
|
+
inputSchema: obj({ channel: {
|
|
23608
|
+
type: "string",
|
|
23609
|
+
description: "Channel ID. Defaults to the current channel."
|
|
23610
|
+
} })
|
|
23611
|
+
},
|
|
23612
|
+
{
|
|
23613
|
+
name: "listChannels",
|
|
23614
|
+
description: "List the public/private channels the bot is a member of and can post to.",
|
|
23615
|
+
inputSchema: obj({})
|
|
23616
|
+
},
|
|
23617
|
+
{
|
|
23618
|
+
name: "getUserProfile",
|
|
23619
|
+
description: "Look up a Slack user or bot by id, returning their display name, real name, and bot flag.",
|
|
23620
|
+
inputSchema: obj({ user: {
|
|
23621
|
+
type: "string",
|
|
23622
|
+
description: "User ID (e.g. U0123ABC)."
|
|
23623
|
+
} }, ["user"])
|
|
23624
|
+
}
|
|
23625
|
+
];
|
|
23626
|
+
SLACK_TOOLS.map((t) => t.name);
|
|
23627
|
+
/**
|
|
23628
|
+
* The default MCP tool set for an agent, gated by its integrations. Today only
|
|
23629
|
+
* Slack is supported, so an agent with no Slack integration gets no tools.
|
|
23630
|
+
*/
|
|
23631
|
+
function toolsForIntegrations(integrations) {
|
|
23632
|
+
const tools = [];
|
|
23633
|
+
if (integrations.some((i) => i.platform === "slack")) tools.push(...SLACK_TOOLS);
|
|
23634
|
+
return tools;
|
|
23635
|
+
}
|
|
23636
|
+
//#endregion
|
|
23222
23637
|
//#region src/router/routing-table.ts
|
|
23223
23638
|
const KIND_ORDER = [
|
|
23224
23639
|
"mention",
|
|
@@ -79273,6 +79688,8 @@ function consolidate(agents) {
|
|
|
79273
79688
|
}
|
|
79274
79689
|
return groups;
|
|
79275
79690
|
}
|
|
79691
|
+
/** Cap on members enriched per `listChannelMembers` call (bounds users.info fan-out). */
|
|
79692
|
+
const MEMBER_ENRICH_CAP = 50;
|
|
79276
79693
|
var SlackConnection = class {
|
|
79277
79694
|
deps;
|
|
79278
79695
|
app;
|
|
@@ -79318,11 +79735,51 @@ var SlackConnection = class {
|
|
|
79318
79735
|
log?.debug("slack: app.start resolved → socket established");
|
|
79319
79736
|
}
|
|
79320
79737
|
async postMessage(channel, text, threadTs) {
|
|
79321
|
-
await this.app.client.chat.postMessage({
|
|
79738
|
+
return (await this.app.client.chat.postMessage({
|
|
79322
79739
|
channel,
|
|
79323
79740
|
text,
|
|
79324
79741
|
thread_ts: threadTs
|
|
79325
|
-
});
|
|
79742
|
+
}))?.ts;
|
|
79743
|
+
}
|
|
79744
|
+
async getChannelInfo(channel) {
|
|
79745
|
+
const c = (await this.app.client.conversations.info({ channel })).channel ?? {};
|
|
79746
|
+
return {
|
|
79747
|
+
id: c.id ?? channel,
|
|
79748
|
+
name: c.name,
|
|
79749
|
+
isIm: c.is_im,
|
|
79750
|
+
isPrivate: c.is_private
|
|
79751
|
+
};
|
|
79752
|
+
}
|
|
79753
|
+
async listMembers(channel) {
|
|
79754
|
+
const ids = ((await this.app.client.conversations.members({
|
|
79755
|
+
channel,
|
|
79756
|
+
limit: 200
|
|
79757
|
+
})).members ?? []).slice(0, MEMBER_ENRICH_CAP);
|
|
79758
|
+
return Promise.all(ids.map((id) => this.getUserProfile(id).then((p) => ({
|
|
79759
|
+
id: p.id,
|
|
79760
|
+
name: p.name,
|
|
79761
|
+
isBot: p.isBot
|
|
79762
|
+
})).catch(() => ({ id }))));
|
|
79763
|
+
}
|
|
79764
|
+
async listChannels() {
|
|
79765
|
+
return ((await this.app.client.conversations.list({
|
|
79766
|
+
types: "public_channel,private_channel",
|
|
79767
|
+
exclude_archived: true,
|
|
79768
|
+
limit: 200
|
|
79769
|
+
})).channels ?? []).map((c) => ({
|
|
79770
|
+
id: c.id ?? "",
|
|
79771
|
+
name: c.name,
|
|
79772
|
+
isPrivate: c.is_private
|
|
79773
|
+
}));
|
|
79774
|
+
}
|
|
79775
|
+
async getUserProfile(user) {
|
|
79776
|
+
const u = (await this.app.client.users.info({ user })).user ?? {};
|
|
79777
|
+
return {
|
|
79778
|
+
id: u.id ?? user,
|
|
79779
|
+
name: u.name,
|
|
79780
|
+
realName: u.real_name ?? u.profile?.real_name,
|
|
79781
|
+
isBot: u.is_bot
|
|
79782
|
+
};
|
|
79326
79783
|
}
|
|
79327
79784
|
/**
|
|
79328
79785
|
* Best-effort assistant loading status (assistant.threads.setStatus).
|
|
@@ -79347,9 +79804,18 @@ var SlackConnection = class {
|
|
|
79347
79804
|
};
|
|
79348
79805
|
//#endregion
|
|
79349
79806
|
//#region src/slack/render.ts
|
|
79807
|
+
const THINKING = "is thinking…";
|
|
79808
|
+
const MAX_ACTIVITY = 10;
|
|
79809
|
+
const MAX_LABEL = 100;
|
|
79810
|
+
function clampLabel(s) {
|
|
79811
|
+
const t = s.trim();
|
|
79812
|
+
return t.length > MAX_LABEL ? `${t.slice(0, MAX_LABEL - 1)}…` : t;
|
|
79813
|
+
}
|
|
79350
79814
|
var OutputConverger = class {
|
|
79351
79815
|
mode;
|
|
79352
79816
|
buf = "";
|
|
79817
|
+
activity = [];
|
|
79818
|
+
toolTitles = /* @__PURE__ */ new Map();
|
|
79353
79819
|
constructor(mode) {
|
|
79354
79820
|
this.mode = mode;
|
|
79355
79821
|
}
|
|
@@ -79365,6 +79831,31 @@ var OutputConverger = class {
|
|
|
79365
79831
|
text
|
|
79366
79832
|
}];
|
|
79367
79833
|
}
|
|
79834
|
+
/**
|
|
79835
|
+
* Record an activity label and build the loading-status action carrying the rolling
|
|
79836
|
+
* window as `loading_messages`. Consecutive repeats collapse to nothing (returns []),
|
|
79837
|
+
* which throttles streamed thought chunks down to one status update per thinking run.
|
|
79838
|
+
*/
|
|
79839
|
+
pushActivity(raw) {
|
|
79840
|
+
const label = clampLabel(raw);
|
|
79841
|
+
if (this.activity[this.activity.length - 1] === label) return [];
|
|
79842
|
+
this.activity.push(label);
|
|
79843
|
+
if (this.activity.length > MAX_ACTIVITY) this.activity.shift();
|
|
79844
|
+
return [{
|
|
79845
|
+
kind: "set-status",
|
|
79846
|
+
text: label,
|
|
79847
|
+
loadingMessages: [...this.activity]
|
|
79848
|
+
}];
|
|
79849
|
+
}
|
|
79850
|
+
/** Resolve a tool call's display label, reusing a known title when an update omits it. */
|
|
79851
|
+
toolLabel(update) {
|
|
79852
|
+
const id = update.toolCallId;
|
|
79853
|
+
if (update.title) {
|
|
79854
|
+
if (id) this.toolTitles.set(id, update.title);
|
|
79855
|
+
return update.title;
|
|
79856
|
+
}
|
|
79857
|
+
return (id && this.toolTitles.get(id)) ?? id ?? "tool";
|
|
79858
|
+
}
|
|
79368
79859
|
onUpdate(update) {
|
|
79369
79860
|
switch (update.sessionUpdate) {
|
|
79370
79861
|
case "agent_message_chunk": {
|
|
@@ -79373,42 +79864,53 @@ var OutputConverger = class {
|
|
|
79373
79864
|
return [];
|
|
79374
79865
|
}
|
|
79375
79866
|
case "agent_thought_chunk": {
|
|
79376
|
-
|
|
79377
|
-
|
|
79378
|
-
|
|
79379
|
-
|
|
79380
|
-
|
|
79381
|
-
|
|
79382
|
-
|
|
79383
|
-
|
|
79384
|
-
|
|
79385
|
-
|
|
79867
|
+
const status = this.pushActivity(THINKING);
|
|
79868
|
+
if (this.mode === "high") {
|
|
79869
|
+
const content = update.content;
|
|
79870
|
+
return [
|
|
79871
|
+
...this.flush(),
|
|
79872
|
+
...status,
|
|
79873
|
+
{
|
|
79874
|
+
kind: "update-main",
|
|
79875
|
+
text: `_thinking: ${content?.text ?? ""}_`
|
|
79876
|
+
}
|
|
79877
|
+
];
|
|
79878
|
+
}
|
|
79879
|
+
if (this.mode === "low") return [...this.flush(), ...status];
|
|
79880
|
+
return status;
|
|
79386
79881
|
}
|
|
79387
79882
|
case "tool_call":
|
|
79388
79883
|
case "tool_call_update": {
|
|
79389
|
-
const
|
|
79390
|
-
|
|
79391
|
-
|
|
79392
|
-
|
|
79393
|
-
|
|
79394
|
-
|
|
79395
|
-
|
|
79396
|
-
|
|
79397
|
-
|
|
79884
|
+
const label = this.toolLabel(update);
|
|
79885
|
+
const status = this.pushActivity(label);
|
|
79886
|
+
if (this.mode === "low") return [...this.flush(), ...status];
|
|
79887
|
+
return [
|
|
79888
|
+
...this.flush(),
|
|
79889
|
+
...status,
|
|
79890
|
+
{
|
|
79891
|
+
kind: "update-main",
|
|
79892
|
+
text: `:hammer_and_wrench: ${label}`
|
|
79893
|
+
}
|
|
79894
|
+
];
|
|
79398
79895
|
}
|
|
79399
79896
|
case "usage_update": return [];
|
|
79400
79897
|
default: return [];
|
|
79401
79898
|
}
|
|
79402
79899
|
}
|
|
79403
79900
|
onFinal(link) {
|
|
79404
|
-
|
|
79901
|
+
const clear = {
|
|
79405
79902
|
kind: "set-status",
|
|
79406
79903
|
text: ""
|
|
79407
|
-
}
|
|
79408
|
-
return [...this.flush(),
|
|
79409
|
-
|
|
79410
|
-
|
|
79411
|
-
|
|
79904
|
+
};
|
|
79905
|
+
if (this.mode === "low") return [...this.flush(), clear];
|
|
79906
|
+
return [
|
|
79907
|
+
...this.flush(),
|
|
79908
|
+
clear,
|
|
79909
|
+
{
|
|
79910
|
+
kind: "post",
|
|
79911
|
+
text: `:white_check_mark: done — <${link}|details>`
|
|
79912
|
+
}
|
|
79913
|
+
];
|
|
79412
79914
|
}
|
|
79413
79915
|
};
|
|
79414
79916
|
//#endregion
|
|
@@ -80769,6 +81271,7 @@ const MAX_QUEUED_PER_SESSION = 10;
|
|
|
80769
81271
|
var Daemon = class {
|
|
80770
81272
|
opts;
|
|
80771
81273
|
store;
|
|
81274
|
+
mcp;
|
|
80772
81275
|
agents = /* @__PURE__ */ new Map();
|
|
80773
81276
|
fileAgents = /* @__PURE__ */ new Map();
|
|
80774
81277
|
hosts = /* @__PURE__ */ new Map();
|
|
@@ -80844,10 +81347,42 @@ var Daemon = class {
|
|
|
80844
81347
|
save: (s) => this.store.setCpAgents(JSON.stringify(s))
|
|
80845
81348
|
}, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
|
|
80846
81349
|
for (const a of this.effectiveAgents()) this.agents.set(a.id, a);
|
|
81350
|
+
this.mcp = new McpControlServer({
|
|
81351
|
+
socketPath: mcpSocketPath(root),
|
|
81352
|
+
log: this.log,
|
|
81353
|
+
now: () => Date.now(),
|
|
81354
|
+
gatewayFor: (integrationId) => this.connByIntegration.get(integrationId),
|
|
81355
|
+
recordOutbound: (ctx, channel, thread, text, ts) => this.store.appendTranscript({
|
|
81356
|
+
channel,
|
|
81357
|
+
thread: thread ?? ctx.thread,
|
|
81358
|
+
ts,
|
|
81359
|
+
sender: ctx.agentId,
|
|
81360
|
+
text
|
|
81361
|
+
})
|
|
81362
|
+
});
|
|
81363
|
+
await this.mcp.start();
|
|
81364
|
+
const cliEntry = process.argv[1] ?? "";
|
|
80847
81365
|
this.sessions = new SessionManager({
|
|
80848
81366
|
store: this.store,
|
|
80849
81367
|
hostFor: (agentId) => this.ensureHostAsync(agentId),
|
|
80850
|
-
agentById: (id) => this.agents.get(id)
|
|
81368
|
+
agentById: (id) => this.agents.get(id),
|
|
81369
|
+
mcpServersFor: ({ agent, channel, thread }) => {
|
|
81370
|
+
const tools = toolsForIntegrations(agent.integrations);
|
|
81371
|
+
const integrationId = agent.integrations.find((i) => i.platform === "slack")?.id;
|
|
81372
|
+
if (tools.length === 0 || !integrationId) return [];
|
|
81373
|
+
const token = this.mcp.register({
|
|
81374
|
+
agentId: agent.id,
|
|
81375
|
+
integrationId,
|
|
81376
|
+
channel,
|
|
81377
|
+
thread,
|
|
81378
|
+
tools
|
|
81379
|
+
});
|
|
81380
|
+
return buildMcpServers({
|
|
81381
|
+
socketPath: mcpSocketPath(root),
|
|
81382
|
+
token,
|
|
81383
|
+
cliEntry
|
|
81384
|
+
});
|
|
81385
|
+
}
|
|
80851
81386
|
});
|
|
80852
81387
|
this.scheduler = new Scheduler({
|
|
80853
81388
|
onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
|
|
@@ -81119,10 +81654,11 @@ var Daemon = class {
|
|
|
81119
81654
|
}
|
|
81120
81655
|
this.flushQueued(agentId, sessionId, integrationId);
|
|
81121
81656
|
}
|
|
81122
|
-
/** Route a converger action: set-status → setStatus (status
|
|
81657
|
+
/** Route a converger action: set-status → setStatus (status + rotating loading_messages;
|
|
81658
|
+
* '' clears); else postMessage. */
|
|
81123
81659
|
async applyAction(action, conn, channel, thread) {
|
|
81124
81660
|
if (action.kind === "set-status") {
|
|
81125
|
-
if (conn && thread) await conn.setStatus(channel, thread, action.text);
|
|
81661
|
+
if (conn && thread) await conn.setStatus(channel, thread, action.text, action.loadingMessages);
|
|
81126
81662
|
return;
|
|
81127
81663
|
}
|
|
81128
81664
|
await conn?.postMessage(channel, action.text, thread);
|
|
@@ -81253,6 +81789,7 @@ var Daemon = class {
|
|
|
81253
81789
|
this.cpCrons?.stop();
|
|
81254
81790
|
for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
|
|
81255
81791
|
for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
|
|
81792
|
+
await Promise.resolve(this.mcp?.stop()).catch((e) => errors.push(e));
|
|
81256
81793
|
this.store?.close();
|
|
81257
81794
|
if (errors.length) throw new AggregateError(errors, "stop: partial failure");
|
|
81258
81795
|
}
|
|
@@ -81423,12 +81960,78 @@ async function runLogin(opts, partial = {}) {
|
|
|
81423
81960
|
await deps.runForeground();
|
|
81424
81961
|
}
|
|
81425
81962
|
//#endregion
|
|
81963
|
+
//#region src/lock.ts
|
|
81964
|
+
/**
|
|
81965
|
+
* Thrown when a foreground daemon is started while another live daemon already
|
|
81966
|
+
* holds the per-root lock. Two daemons sharing one Slack app token each open a
|
|
81967
|
+
* Socket Mode connection; Slack round-robins events across connections (it does
|
|
81968
|
+
* not broadcast), so each instance receives only a fraction of messages and the
|
|
81969
|
+
* rest appear silently dropped — no error, no log on the instance you're watching.
|
|
81970
|
+
*/
|
|
81971
|
+
var DaemonAlreadyRunningError = class extends Error {
|
|
81972
|
+
pid;
|
|
81973
|
+
lockFile;
|
|
81974
|
+
constructor(pid, lockFile) {
|
|
81975
|
+
super(`another agentconnect daemon is already running (pid ${pid}; lock ${lockFile}). Stop it first — two daemons sharing one Slack app token split Socket Mode events between them, so messages appear to be silently dropped.`);
|
|
81976
|
+
this.pid = pid;
|
|
81977
|
+
this.lockFile = lockFile;
|
|
81978
|
+
this.name = "DaemonAlreadyRunningError";
|
|
81979
|
+
}
|
|
81980
|
+
};
|
|
81981
|
+
/** True if a process with `pid` exists. EPERM ⇒ it exists but is owned by another user. */
|
|
81982
|
+
function pidAlive(pid) {
|
|
81983
|
+
try {
|
|
81984
|
+
process.kill(pid, 0);
|
|
81985
|
+
return true;
|
|
81986
|
+
} catch (err) {
|
|
81987
|
+
return err.code === "EPERM";
|
|
81988
|
+
}
|
|
81989
|
+
}
|
|
81990
|
+
/**
|
|
81991
|
+
* Best-effort single-instance guard for the foreground daemon, keyed by `root`.
|
|
81992
|
+
* A lock file holding a live pid blocks (throws DaemonAlreadyRunningError); a
|
|
81993
|
+
* stale lock (dead pid, garbage, or absent) is reclaimed. `release()` removes the
|
|
81994
|
+
* file only if we still own it. There is a small TOCTOU window — this guards
|
|
81995
|
+
* against accidental double-starts (the launchd service + a manual `run`), not a
|
|
81996
|
+
* determined concurrent race.
|
|
81997
|
+
*
|
|
81998
|
+
* Scoped per-root, not per-app-token: the common collision is two daemons sharing
|
|
81999
|
+
* one `~/.agentconnect`. Distinct roots that happen to carry the same token would
|
|
82000
|
+
* still collide on Slack but not here.
|
|
82001
|
+
*/
|
|
82002
|
+
function acquireSingletonLock(root, deps = {}) {
|
|
82003
|
+
const file = lockPath(root);
|
|
82004
|
+
const myPid = deps.pid ?? process.pid;
|
|
82005
|
+
try {
|
|
82006
|
+
const prev = Number.parseInt(readFileSync(file, "utf8").trim(), 10);
|
|
82007
|
+
if (Number.isInteger(prev) && prev !== myPid && pidAlive(prev)) throw new DaemonAlreadyRunningError(prev, file);
|
|
82008
|
+
} catch (err) {
|
|
82009
|
+
if (err instanceof DaemonAlreadyRunningError) throw err;
|
|
82010
|
+
}
|
|
82011
|
+
writeFileSync(file, `${myPid}\n`, "utf8");
|
|
82012
|
+
let released = false;
|
|
82013
|
+
return { release() {
|
|
82014
|
+
if (released) return;
|
|
82015
|
+
released = true;
|
|
82016
|
+
try {
|
|
82017
|
+
if (Number.parseInt(readFileSync(file, "utf8").trim(), 10) === myPid) unlinkSync(file);
|
|
82018
|
+
} catch {}
|
|
82019
|
+
} };
|
|
82020
|
+
}
|
|
82021
|
+
//#endregion
|
|
81426
82022
|
//#region src/index.ts
|
|
81427
82023
|
const program = new Command();
|
|
81428
82024
|
program.name("agentconnect").description("AgentConnect daemon — edge message + agent execution unit").version(DAEMON_VERSION);
|
|
81429
82025
|
program.option("--config <path>", "path to config.json (default ~/.agentconnect/config.json)").option("--root <dir>", "override ~/.agentconnect root directory").option("--cp-url <url>", "override controlPlane.url").option("--cp-key <key>", "override controlPlane.key (the CP API key)").option("--no-cp", "run fully local, do not connect to the Control Plane").option("--daemon-id <id>", "override daemon identity").option("--log-level <level>", "trace|debug|info|warn|error").option("--agents-dir <dir>", "override agents directory").option("--max-agents <n>", "max agents this daemon advertises / enforces").option("--dry-run", "load + validate config and print the reconcile plan, then exit").option("--agent <name>", "select a single agent by id (run/chat)");
|
|
81430
82026
|
program.command("run").description("Run the daemon in the foreground").action(async () => {
|
|
81431
82027
|
const opts = program.opts();
|
|
82028
|
+
let lock;
|
|
82029
|
+
try {
|
|
82030
|
+
lock = acquireSingletonLock(resolveRoot(opts.root));
|
|
82031
|
+
} catch (err) {
|
|
82032
|
+
console.error(`agentconnect run: ${err.message}`);
|
|
82033
|
+
process.exit(1);
|
|
82034
|
+
}
|
|
81432
82035
|
try {
|
|
81433
82036
|
await runForeground({
|
|
81434
82037
|
root: opts.root,
|
|
@@ -81443,12 +82046,18 @@ program.command("run").description("Run the daemon in the foreground").action(as
|
|
|
81443
82046
|
maxAgents: opts.maxAgents ? Number(opts.maxAgents) : void 0
|
|
81444
82047
|
}
|
|
81445
82048
|
});
|
|
82049
|
+
lock.release();
|
|
81446
82050
|
process.exit(0);
|
|
81447
82051
|
} catch (err) {
|
|
82052
|
+
lock.release();
|
|
81448
82053
|
console.error(`agentconnect run: ${err.message}`);
|
|
81449
82054
|
process.exit(1);
|
|
81450
82055
|
}
|
|
81451
82056
|
});
|
|
82057
|
+
program.command("mcp-bridge", { hidden: true }).description("internal: stdio MCP bridge to the running daemon").action(async () => {
|
|
82058
|
+
const { runBridge } = await import("./bridge-Dj8U1Jp7.js");
|
|
82059
|
+
await runBridge();
|
|
82060
|
+
});
|
|
81452
82061
|
const controller = () => resolveController({ root: program.opts().root });
|
|
81453
82062
|
const requireInstalled = (c) => {
|
|
81454
82063
|
if (!c.isInstalled()) {
|
|
@@ -81580,6 +82189,6 @@ program.command("chat [message]").description("Discover an agent under --agents-
|
|
|
81580
82189
|
});
|
|
81581
82190
|
program.parse();
|
|
81582
82191
|
//#endregion
|
|
81583
|
-
export {};
|
|
82192
|
+
export { __commonJSMin as C, safeParse$1 as S, record as _, _null as a, unknown as b, custom as c, literal as d, looseObject as f, preprocess as g, optional as h, _enum as i, discriminatedUnion as l, object as m, encodeFrame as n, array as o, number as p, DAEMON_VERSION as r, boolean as s, decodeFrames as t, intersection as u, string as v, __toESM as w, datetime as x, union as y };
|
|
81584
82193
|
|
|
81585
82194
|
//# sourceMappingURL=index.js.map
|