@agentconnect.md/daemon 1.0.0-rc.25 → 1.0.0-rc.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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;
@@ -19499,9 +19587,9 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
19499
19587
  const EventEmitter$3 = __require("events");
19500
19588
  const https$3 = __require("https");
19501
19589
  const http$7 = __require("http");
19502
- const net = __require("net");
19590
+ const net$1 = __require("net");
19503
19591
  const tls = __require("tls");
19504
- const { randomBytes, createHash: createHash$1 } = __require("crypto");
19592
+ const { randomBytes: randomBytes$1, createHash: createHash$1 } = __require("crypto");
19505
19593
  const { Duplex: Duplex$2, Readable: Readable$1 } = __require("stream");
19506
19594
  const { URL: URL$1 } = __require("url");
19507
19595
  const PerMessageDeflate = require_permessage_deflate();
@@ -20068,7 +20156,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20068
20156
  }
20069
20157
  }
20070
20158
  const defaultPort = isSecure ? 443 : 80;
20071
- const key = randomBytes(16).toString("base64");
20159
+ const key = randomBytes$1(16).toString("base64");
20072
20160
  const request = isSecure ? https$3.request : http$7.request;
20073
20161
  const protocolSet = /* @__PURE__ */ new Set();
20074
20162
  let perMessageDeflate;
@@ -20245,7 +20333,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20245
20333
  */
20246
20334
  function netConnect(options) {
20247
20335
  options.path = options.socketPath;
20248
- return net.connect(options);
20336
+ return net$1.connect(options);
20249
20337
  }
20250
20338
  /**
20251
20339
  * Create a `tls.TLSSocket` and initiate a connection.
@@ -20256,7 +20344,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20256
20344
  */
20257
20345
  function tlsConnect(options) {
20258
20346
  options.path = void 0;
20259
- if (!options.servername && options.servername !== "") options.servername = net.isIP(options.host) ? "" : options.host;
20347
+ if (!options.servername && options.servername !== "") options.servername = net$1.isIP(options.host) ? "" : options.host;
20260
20348
  return tls.connect(options);
20261
20349
  }
20262
20350
  /**
@@ -23158,7 +23246,12 @@ var SessionManager = class {
23158
23246
  const host = await this.deps.hostFor(agentId);
23159
23247
  if (!rec || !rec.acpSessionId) {
23160
23248
  const cwd = await prepareWorkspace(agent);
23161
- const acpSessionId = await host.newSession(cwd);
23249
+ const mcpServers = this.deps.mcpServersFor?.({
23250
+ agent,
23251
+ channel: msg.channel,
23252
+ thread
23253
+ }) ?? [];
23254
+ const acpSessionId = await host.newSession(cwd, mcpServers);
23162
23255
  rec = {
23163
23256
  key,
23164
23257
  agentId,
@@ -23179,7 +23272,12 @@ var SessionManager = class {
23179
23272
  resumed = true;
23180
23273
  } catch {}
23181
23274
  if (!resumed) {
23182
- const acpSessionId = await host.newSession(cwd);
23275
+ const mcpServers = this.deps.mcpServersFor?.({
23276
+ agent,
23277
+ channel: msg.channel,
23278
+ thread
23279
+ }) ?? [];
23280
+ const acpSessionId = await host.newSession(cwd, mcpServers);
23183
23281
  rec = {
23184
23282
  ...rec,
23185
23283
  acpSessionId,
@@ -23192,7 +23290,7 @@ var SessionManager = class {
23192
23290
  }
23193
23291
  const gap = this.deps.store.transcriptSince(msg.channel, thread, rec.lastDeliveredTs);
23194
23292
  const blocks = [];
23195
- const context = gap.slice(0, -1);
23293
+ const context = gap.slice(0, -1).filter((e) => e.sender !== agentId);
23196
23294
  if (context.length > 0) {
23197
23295
  const ctxText = context.map((e) => `[${e.sender}] ${e.text}`).join("\n");
23198
23296
  blocks.push({
@@ -23219,6 +23317,306 @@ function tsOf(msg) {
23219
23317
  return parts[parts.length - 1] ?? "0";
23220
23318
  }
23221
23319
  //#endregion
23320
+ //#region src/mcp/ipc.ts
23321
+ /** Frame a message for the wire: compact JSON + a single trailing newline. */
23322
+ function encodeFrame(msg) {
23323
+ return JSON.stringify(msg) + "\n";
23324
+ }
23325
+ /**
23326
+ * Split a buffer of newline-delimited frames into parsed objects plus the
23327
+ * trailing partial line. Callers keep `rest` and prepend it to the next chunk.
23328
+ *
23329
+ * Per-line tolerant: a malformed (non-JSON) line is skipped, not thrown — so one
23330
+ * bad frame can't crash a stream reader or discard the good frames batched
23331
+ * alongside it in the same chunk. `onError` lets callers log dropped lines.
23332
+ */
23333
+ function decodeFrames(buf, onError) {
23334
+ const parts = buf.split("\n");
23335
+ const rest = parts.pop() ?? "";
23336
+ const messages = [];
23337
+ for (const line of parts) {
23338
+ const trimmed = line.trim();
23339
+ if (!trimmed) continue;
23340
+ try {
23341
+ messages.push(JSON.parse(trimmed));
23342
+ } catch (err) {
23343
+ onError?.(trimmed, err);
23344
+ }
23345
+ }
23346
+ return {
23347
+ messages,
23348
+ rest
23349
+ };
23350
+ }
23351
+ //#endregion
23352
+ //#region src/mcp/ops.ts
23353
+ /**
23354
+ * Execute one tool call inside the daemon and return a plain result object (the
23355
+ * bridge wraps it into an MCP `CallToolResult`). Throws on bad input or a
23356
+ * missing connection — the caller turns that into an MCP `isError` result.
23357
+ */
23358
+ async function executeTool(ctx, name, args, deps) {
23359
+ const gw = deps.gatewayFor(ctx.integrationId);
23360
+ if (!gw) throw new Error(`no live Slack connection for integration ${ctx.integrationId}`);
23361
+ switch (name) {
23362
+ case "sendSlackMessage": {
23363
+ const text = requireString(args, "text");
23364
+ const channel = optionalString(args, "channel") ?? ctx.channel;
23365
+ const thread = "thread" in args ? optionalString(args, "thread") || void 0 : ctx.thread;
23366
+ const ts = await gw.postMessage(channel, text, thread) ?? `local-${deps.now()}`;
23367
+ deps.recordOutbound(ctx, channel, thread, text, ts);
23368
+ return {
23369
+ ok: true,
23370
+ channel,
23371
+ thread: thread ?? null,
23372
+ ts
23373
+ };
23374
+ }
23375
+ case "getCurrentChannel": {
23376
+ const info = await gw.getChannelInfo(ctx.channel).catch(() => void 0);
23377
+ return {
23378
+ channel: ctx.channel,
23379
+ thread: ctx.thread,
23380
+ name: info?.name ?? null,
23381
+ isIm: info?.isIm ?? null
23382
+ };
23383
+ }
23384
+ case "listChannelMembers": {
23385
+ const channel = optionalString(args, "channel") ?? ctx.channel;
23386
+ return {
23387
+ channel,
23388
+ members: await gw.listMembers(channel)
23389
+ };
23390
+ }
23391
+ case "listChannels": return { channels: await gw.listChannels() };
23392
+ case "getUserProfile": {
23393
+ const user = requireString(args, "user");
23394
+ return await gw.getUserProfile(user);
23395
+ }
23396
+ default: throw new Error(`unknown tool: ${name}`);
23397
+ }
23398
+ }
23399
+ function requireString(args, key) {
23400
+ const v = args[key];
23401
+ if (typeof v !== "string" || v.length === 0) throw new Error(`missing required string argument: ${key}`);
23402
+ return v;
23403
+ }
23404
+ function optionalString(args, key) {
23405
+ const v = args[key];
23406
+ if (v === void 0 || v === null) return void 0;
23407
+ if (typeof v !== "string") throw new Error(`argument ${key} must be a string`);
23408
+ return v;
23409
+ }
23410
+ //#endregion
23411
+ //#region src/mcp/control-server.ts
23412
+ /**
23413
+ * The daemon-hosted half of MCP. It owns all tool logic, the registry of live
23414
+ * sessions, and the Unix-domain socket the `mcp-bridge` subprocesses connect to.
23415
+ * "The MCP server is the daemon itself" — the bridge is only a stdio↔socket pipe.
23416
+ */
23417
+ var McpControlServer = class {
23418
+ deps;
23419
+ server;
23420
+ sessions = /* @__PURE__ */ new Map();
23421
+ conns = /* @__PURE__ */ new Set();
23422
+ constructor(deps) {
23423
+ this.deps = deps;
23424
+ }
23425
+ /**
23426
+ * Register a session's tool set and return an opaque token. The token is
23427
+ * embedded in the bridge's env at `session/new`; the bridge presents it on
23428
+ * every IPC request so we can resolve the channel/thread/agent binding.
23429
+ */
23430
+ register(ctx) {
23431
+ const token = randomBytes(18).toString("base64url");
23432
+ this.sessions.set(token, ctx);
23433
+ return token;
23434
+ }
23435
+ unregister(token) {
23436
+ this.sessions.delete(token);
23437
+ }
23438
+ async start() {
23439
+ if (this.server) return;
23440
+ const path = this.deps.socketPath;
23441
+ const dir = dirname(path);
23442
+ mkdirSync(dir, {
23443
+ recursive: true,
23444
+ mode: 448
23445
+ });
23446
+ try {
23447
+ chmodSync(dir, 448);
23448
+ } catch {}
23449
+ rmSync(path, { force: true });
23450
+ const server = net.createServer((socket) => this.onConnection(socket));
23451
+ this.server = server;
23452
+ await new Promise((resolve, reject) => {
23453
+ const onStartupError = (err) => reject(err);
23454
+ server.once("error", onStartupError);
23455
+ server.listen(path, () => {
23456
+ server.off("error", onStartupError);
23457
+ server.on("error", (err) => this.deps.log?.error(`mcp: control server error: ${err.message}`));
23458
+ resolve();
23459
+ });
23460
+ });
23461
+ try {
23462
+ chmodSync(path, 384);
23463
+ } catch {}
23464
+ this.deps.log?.info(`mcp: control socket listening at ${path}`);
23465
+ }
23466
+ onConnection(socket) {
23467
+ socket.setEncoding("utf8");
23468
+ this.conns.add(socket);
23469
+ let buf = "";
23470
+ socket.on("data", (chunk) => {
23471
+ buf += chunk;
23472
+ const { messages, rest } = decodeFrames(buf, (line) => this.deps.log?.debug(`mcp: dropping malformed frame: ${line.slice(0, 120)}`));
23473
+ buf = rest;
23474
+ for (const req of messages) this.handle(req, socket);
23475
+ });
23476
+ socket.on("error", (err) => this.deps.log?.debug(`mcp: socket error: ${err.message}`));
23477
+ socket.on("close", () => this.conns.delete(socket));
23478
+ }
23479
+ async handle(req, socket) {
23480
+ const reply = (res) => {
23481
+ if (!socket.destroyed) socket.write(encodeFrame(res));
23482
+ };
23483
+ const ctx = this.sessions.get(req.token);
23484
+ if (!ctx) return reply({
23485
+ id: req.id,
23486
+ ok: false,
23487
+ error: "unknown or expired session token"
23488
+ });
23489
+ try {
23490
+ if (req.op === "listTools") {
23491
+ reply({
23492
+ id: req.id,
23493
+ ok: true,
23494
+ result: { tools: ctx.tools }
23495
+ });
23496
+ return;
23497
+ }
23498
+ const result = await executeTool(ctx, req.name, req.args ?? {}, this.deps);
23499
+ reply({
23500
+ id: req.id,
23501
+ ok: true,
23502
+ result
23503
+ });
23504
+ } catch (err) {
23505
+ reply({
23506
+ id: req.id,
23507
+ ok: false,
23508
+ error: err.message
23509
+ });
23510
+ }
23511
+ }
23512
+ async stop() {
23513
+ this.sessions.clear();
23514
+ const server = this.server;
23515
+ if (!server) return;
23516
+ this.server = void 0;
23517
+ for (const s of this.conns) s.destroy();
23518
+ this.conns.clear();
23519
+ await new Promise((resolve) => server.close(() => resolve()));
23520
+ rmSync(this.deps.socketPath, { force: true });
23521
+ }
23522
+ };
23523
+ //#endregion
23524
+ //#region src/mcp/inject.ts
23525
+ /**
23526
+ * Build the `mcpServers` entry that makes the agent harness spawn our stdio
23527
+ * bridge. We reuse the *current* interpreter + CLI entry (so it works under both
23528
+ * `tsx` in dev and plain `node` in prod) and invoke the hidden `mcp-bridge`
23529
+ * subcommand. The socket path and per-session token travel via env — they reach
23530
+ * only the harness-spawned subprocess, never the model.
23531
+ */
23532
+ function buildMcpServers(opts) {
23533
+ return [{
23534
+ name: "agentconnect",
23535
+ command: process.execPath,
23536
+ args: [
23537
+ ...process.execArgv,
23538
+ opts.cliEntry,
23539
+ "mcp-bridge"
23540
+ ],
23541
+ env: [{
23542
+ name: "AC_MCP_ENDPOINT",
23543
+ value: opts.socketPath
23544
+ }, {
23545
+ name: "AC_MCP_TOKEN",
23546
+ value: opts.token
23547
+ }]
23548
+ }];
23549
+ }
23550
+ //#endregion
23551
+ //#region src/mcp/tools.ts
23552
+ const obj = (properties, required = []) => ({
23553
+ type: "object",
23554
+ properties,
23555
+ required,
23556
+ additionalProperties: false
23557
+ });
23558
+ /**
23559
+ * Tools injected when an agent has a Slack integration. `sendSlackMessage` is
23560
+ * the active-messaging tool from the design (token held by the daemon, invisible
23561
+ * to the model); the rest are channel/user read helpers.
23562
+ */
23563
+ const SLACK_TOOLS = [
23564
+ {
23565
+ name: "sendSlackMessage",
23566
+ 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.",
23567
+ inputSchema: obj({
23568
+ text: {
23569
+ type: "string",
23570
+ description: "Message body, in Slack mrkdwn."
23571
+ },
23572
+ channel: {
23573
+ type: "string",
23574
+ description: "Target channel ID (e.g. C0123ABC). Defaults to the current channel."
23575
+ },
23576
+ thread: {
23577
+ type: "string",
23578
+ description: "Thread timestamp to reply under. Defaults to the current thread; \"\" posts to the channel root."
23579
+ }
23580
+ }, ["text"])
23581
+ },
23582
+ {
23583
+ name: "getCurrentChannel",
23584
+ description: "Return the Slack channel (and thread) this conversation is bound to, including the channel name when available.",
23585
+ inputSchema: obj({})
23586
+ },
23587
+ {
23588
+ name: "listChannelMembers",
23589
+ description: "List the users and bots in a channel (id, name, is_bot). Omit `channel` to list members of the current channel.",
23590
+ inputSchema: obj({ channel: {
23591
+ type: "string",
23592
+ description: "Channel ID. Defaults to the current channel."
23593
+ } })
23594
+ },
23595
+ {
23596
+ name: "listChannels",
23597
+ description: "List the public/private channels the bot is a member of and can post to.",
23598
+ inputSchema: obj({})
23599
+ },
23600
+ {
23601
+ name: "getUserProfile",
23602
+ description: "Look up a Slack user or bot by id, returning their display name, real name, and bot flag.",
23603
+ inputSchema: obj({ user: {
23604
+ type: "string",
23605
+ description: "User ID (e.g. U0123ABC)."
23606
+ } }, ["user"])
23607
+ }
23608
+ ];
23609
+ SLACK_TOOLS.map((t) => t.name);
23610
+ /**
23611
+ * The default MCP tool set for an agent, gated by its integrations. Today only
23612
+ * Slack is supported, so an agent with no Slack integration gets no tools.
23613
+ */
23614
+ function toolsForIntegrations(integrations) {
23615
+ const tools = [];
23616
+ if (integrations.some((i) => i.platform === "slack")) tools.push(...SLACK_TOOLS);
23617
+ return tools;
23618
+ }
23619
+ //#endregion
23222
23620
  //#region src/router/routing-table.ts
23223
23621
  const KIND_ORDER = [
23224
23622
  "mention",
@@ -79273,6 +79671,8 @@ function consolidate(agents) {
79273
79671
  }
79274
79672
  return groups;
79275
79673
  }
79674
+ /** Cap on members enriched per `listChannelMembers` call (bounds users.info fan-out). */
79675
+ const MEMBER_ENRICH_CAP = 50;
79276
79676
  var SlackConnection = class {
79277
79677
  deps;
79278
79678
  app;
@@ -79318,11 +79718,51 @@ var SlackConnection = class {
79318
79718
  log?.debug("slack: app.start resolved → socket established");
79319
79719
  }
79320
79720
  async postMessage(channel, text, threadTs) {
79321
- await this.app.client.chat.postMessage({
79721
+ return (await this.app.client.chat.postMessage({
79322
79722
  channel,
79323
79723
  text,
79324
79724
  thread_ts: threadTs
79325
- });
79725
+ }))?.ts;
79726
+ }
79727
+ async getChannelInfo(channel) {
79728
+ const c = (await this.app.client.conversations.info({ channel })).channel ?? {};
79729
+ return {
79730
+ id: c.id ?? channel,
79731
+ name: c.name,
79732
+ isIm: c.is_im,
79733
+ isPrivate: c.is_private
79734
+ };
79735
+ }
79736
+ async listMembers(channel) {
79737
+ const ids = ((await this.app.client.conversations.members({
79738
+ channel,
79739
+ limit: 200
79740
+ })).members ?? []).slice(0, MEMBER_ENRICH_CAP);
79741
+ return Promise.all(ids.map((id) => this.getUserProfile(id).then((p) => ({
79742
+ id: p.id,
79743
+ name: p.name,
79744
+ isBot: p.isBot
79745
+ })).catch(() => ({ id }))));
79746
+ }
79747
+ async listChannels() {
79748
+ return ((await this.app.client.conversations.list({
79749
+ types: "public_channel,private_channel",
79750
+ exclude_archived: true,
79751
+ limit: 200
79752
+ })).channels ?? []).map((c) => ({
79753
+ id: c.id ?? "",
79754
+ name: c.name,
79755
+ isPrivate: c.is_private
79756
+ }));
79757
+ }
79758
+ async getUserProfile(user) {
79759
+ const u = (await this.app.client.users.info({ user })).user ?? {};
79760
+ return {
79761
+ id: u.id ?? user,
79762
+ name: u.name,
79763
+ realName: u.real_name ?? u.profile?.real_name,
79764
+ isBot: u.is_bot
79765
+ };
79326
79766
  }
79327
79767
  /**
79328
79768
  * Best-effort assistant loading status (assistant.threads.setStatus).
@@ -79347,9 +79787,18 @@ var SlackConnection = class {
79347
79787
  };
79348
79788
  //#endregion
79349
79789
  //#region src/slack/render.ts
79790
+ const THINKING = "is thinking…";
79791
+ const MAX_ACTIVITY = 10;
79792
+ const MAX_LABEL = 100;
79793
+ function clampLabel(s) {
79794
+ const t = s.trim();
79795
+ return t.length > MAX_LABEL ? `${t.slice(0, MAX_LABEL - 1)}…` : t;
79796
+ }
79350
79797
  var OutputConverger = class {
79351
79798
  mode;
79352
79799
  buf = "";
79800
+ activity = [];
79801
+ toolTitles = /* @__PURE__ */ new Map();
79353
79802
  constructor(mode) {
79354
79803
  this.mode = mode;
79355
79804
  }
@@ -79365,6 +79814,31 @@ var OutputConverger = class {
79365
79814
  text
79366
79815
  }];
79367
79816
  }
79817
+ /**
79818
+ * Record an activity label and build the loading-status action carrying the rolling
79819
+ * window as `loading_messages`. Consecutive repeats collapse to nothing (returns []),
79820
+ * which throttles streamed thought chunks down to one status update per thinking run.
79821
+ */
79822
+ pushActivity(raw) {
79823
+ const label = clampLabel(raw);
79824
+ if (this.activity[this.activity.length - 1] === label) return [];
79825
+ this.activity.push(label);
79826
+ if (this.activity.length > MAX_ACTIVITY) this.activity.shift();
79827
+ return [{
79828
+ kind: "set-status",
79829
+ text: label,
79830
+ loadingMessages: [...this.activity]
79831
+ }];
79832
+ }
79833
+ /** Resolve a tool call's display label, reusing a known title when an update omits it. */
79834
+ toolLabel(update) {
79835
+ const id = update.toolCallId;
79836
+ if (update.title) {
79837
+ if (id) this.toolTitles.set(id, update.title);
79838
+ return update.title;
79839
+ }
79840
+ return (id && this.toolTitles.get(id)) ?? id ?? "tool";
79841
+ }
79368
79842
  onUpdate(update) {
79369
79843
  switch (update.sessionUpdate) {
79370
79844
  case "agent_message_chunk": {
@@ -79373,42 +79847,53 @@ var OutputConverger = class {
79373
79847
  return [];
79374
79848
  }
79375
79849
  case "agent_thought_chunk": {
79376
- if (this.mode === "low") return [...this.flush(), {
79377
- kind: "set-status",
79378
- text: "is thinking…"
79379
- }];
79380
- if (this.mode !== "high") return [];
79381
- const content = update.content;
79382
- return [...this.flush(), {
79383
- kind: "update-main",
79384
- text: `_thinking: ${content?.text ?? ""}_`
79385
- }];
79850
+ const status = this.pushActivity(THINKING);
79851
+ if (this.mode === "high") {
79852
+ const content = update.content;
79853
+ return [
79854
+ ...this.flush(),
79855
+ ...status,
79856
+ {
79857
+ kind: "update-main",
79858
+ text: `_thinking: ${content?.text ?? ""}_`
79859
+ }
79860
+ ];
79861
+ }
79862
+ if (this.mode === "low") return [...this.flush(), ...status];
79863
+ return status;
79386
79864
  }
79387
79865
  case "tool_call":
79388
79866
  case "tool_call_update": {
79389
- const title = update.title ?? update.toolCallId ?? "tool";
79390
- if (this.mode === "low") return [...this.flush(), {
79391
- kind: "set-status",
79392
- text: title
79393
- }];
79394
- return [...this.flush(), {
79395
- kind: "update-main",
79396
- text: `:hammer_and_wrench: ${title}`
79397
- }];
79867
+ const label = this.toolLabel(update);
79868
+ const status = this.pushActivity(label);
79869
+ if (this.mode === "low") return [...this.flush(), ...status];
79870
+ return [
79871
+ ...this.flush(),
79872
+ ...status,
79873
+ {
79874
+ kind: "update-main",
79875
+ text: `:hammer_and_wrench: ${label}`
79876
+ }
79877
+ ];
79398
79878
  }
79399
79879
  case "usage_update": return [];
79400
79880
  default: return [];
79401
79881
  }
79402
79882
  }
79403
79883
  onFinal(link) {
79404
- if (this.mode === "low") return [...this.flush(), {
79884
+ const clear = {
79405
79885
  kind: "set-status",
79406
79886
  text: ""
79407
- }];
79408
- return [...this.flush(), {
79409
- kind: "post",
79410
- text: `:white_check_mark: done — <${link}|details>`
79411
- }];
79887
+ };
79888
+ if (this.mode === "low") return [...this.flush(), clear];
79889
+ return [
79890
+ ...this.flush(),
79891
+ clear,
79892
+ {
79893
+ kind: "post",
79894
+ text: `:white_check_mark: done — <${link}|details>`
79895
+ }
79896
+ ];
79412
79897
  }
79413
79898
  };
79414
79899
  //#endregion
@@ -80769,6 +81254,7 @@ const MAX_QUEUED_PER_SESSION = 10;
80769
81254
  var Daemon = class {
80770
81255
  opts;
80771
81256
  store;
81257
+ mcp;
80772
81258
  agents = /* @__PURE__ */ new Map();
80773
81259
  fileAgents = /* @__PURE__ */ new Map();
80774
81260
  hosts = /* @__PURE__ */ new Map();
@@ -80844,10 +81330,42 @@ var Daemon = class {
80844
81330
  save: (s) => this.store.setCpAgents(JSON.stringify(s))
80845
81331
  }, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
80846
81332
  for (const a of this.effectiveAgents()) this.agents.set(a.id, a);
81333
+ this.mcp = new McpControlServer({
81334
+ socketPath: mcpSocketPath(root),
81335
+ log: this.log,
81336
+ now: () => Date.now(),
81337
+ gatewayFor: (integrationId) => this.connByIntegration.get(integrationId),
81338
+ recordOutbound: (ctx, channel, thread, text, ts) => this.store.appendTranscript({
81339
+ channel,
81340
+ thread: thread ?? ctx.thread,
81341
+ ts,
81342
+ sender: ctx.agentId,
81343
+ text
81344
+ })
81345
+ });
81346
+ await this.mcp.start();
81347
+ const cliEntry = process.argv[1] ?? "";
80847
81348
  this.sessions = new SessionManager({
80848
81349
  store: this.store,
80849
81350
  hostFor: (agentId) => this.ensureHostAsync(agentId),
80850
- agentById: (id) => this.agents.get(id)
81351
+ agentById: (id) => this.agents.get(id),
81352
+ mcpServersFor: ({ agent, channel, thread }) => {
81353
+ const tools = toolsForIntegrations(agent.integrations);
81354
+ const integrationId = agent.integrations.find((i) => i.platform === "slack")?.id;
81355
+ if (tools.length === 0 || !integrationId) return [];
81356
+ const token = this.mcp.register({
81357
+ agentId: agent.id,
81358
+ integrationId,
81359
+ channel,
81360
+ thread,
81361
+ tools
81362
+ });
81363
+ return buildMcpServers({
81364
+ socketPath: mcpSocketPath(root),
81365
+ token,
81366
+ cliEntry
81367
+ });
81368
+ }
80851
81369
  });
80852
81370
  this.scheduler = new Scheduler({
80853
81371
  onFire: (agentId, msg) => void this.dispatch(agentId, msg).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
@@ -81119,10 +81637,11 @@ var Daemon = class {
81119
81637
  }
81120
81638
  this.flushQueued(agentId, sessionId, integrationId);
81121
81639
  }
81122
- /** Route a converger action: set-status → setStatus (status text only; '' clears); else postMessage. */
81640
+ /** Route a converger action: set-status → setStatus (status + rotating loading_messages;
81641
+ * '' clears); else postMessage. */
81123
81642
  async applyAction(action, conn, channel, thread) {
81124
81643
  if (action.kind === "set-status") {
81125
- if (conn && thread) await conn.setStatus(channel, thread, action.text);
81644
+ if (conn && thread) await conn.setStatus(channel, thread, action.text, action.loadingMessages);
81126
81645
  return;
81127
81646
  }
81128
81647
  await conn?.postMessage(channel, action.text, thread);
@@ -81253,6 +81772,7 @@ var Daemon = class {
81253
81772
  this.cpCrons?.stop();
81254
81773
  for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
81255
81774
  for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
81775
+ await Promise.resolve(this.mcp?.stop()).catch((e) => errors.push(e));
81256
81776
  this.store?.close();
81257
81777
  if (errors.length) throw new AggregateError(errors, "stop: partial failure");
81258
81778
  }
@@ -81423,12 +81943,78 @@ async function runLogin(opts, partial = {}) {
81423
81943
  await deps.runForeground();
81424
81944
  }
81425
81945
  //#endregion
81946
+ //#region src/lock.ts
81947
+ /**
81948
+ * Thrown when a foreground daemon is started while another live daemon already
81949
+ * holds the per-root lock. Two daemons sharing one Slack app token each open a
81950
+ * Socket Mode connection; Slack round-robins events across connections (it does
81951
+ * not broadcast), so each instance receives only a fraction of messages and the
81952
+ * rest appear silently dropped — no error, no log on the instance you're watching.
81953
+ */
81954
+ var DaemonAlreadyRunningError = class extends Error {
81955
+ pid;
81956
+ lockFile;
81957
+ constructor(pid, lockFile) {
81958
+ 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.`);
81959
+ this.pid = pid;
81960
+ this.lockFile = lockFile;
81961
+ this.name = "DaemonAlreadyRunningError";
81962
+ }
81963
+ };
81964
+ /** True if a process with `pid` exists. EPERM ⇒ it exists but is owned by another user. */
81965
+ function pidAlive(pid) {
81966
+ try {
81967
+ process.kill(pid, 0);
81968
+ return true;
81969
+ } catch (err) {
81970
+ return err.code === "EPERM";
81971
+ }
81972
+ }
81973
+ /**
81974
+ * Best-effort single-instance guard for the foreground daemon, keyed by `root`.
81975
+ * A lock file holding a live pid blocks (throws DaemonAlreadyRunningError); a
81976
+ * stale lock (dead pid, garbage, or absent) is reclaimed. `release()` removes the
81977
+ * file only if we still own it. There is a small TOCTOU window — this guards
81978
+ * against accidental double-starts (the launchd service + a manual `run`), not a
81979
+ * determined concurrent race.
81980
+ *
81981
+ * Scoped per-root, not per-app-token: the common collision is two daemons sharing
81982
+ * one `~/.agentconnect`. Distinct roots that happen to carry the same token would
81983
+ * still collide on Slack but not here.
81984
+ */
81985
+ function acquireSingletonLock(root, deps = {}) {
81986
+ const file = lockPath(root);
81987
+ const myPid = deps.pid ?? process.pid;
81988
+ try {
81989
+ const prev = Number.parseInt(readFileSync(file, "utf8").trim(), 10);
81990
+ if (Number.isInteger(prev) && prev !== myPid && pidAlive(prev)) throw new DaemonAlreadyRunningError(prev, file);
81991
+ } catch (err) {
81992
+ if (err instanceof DaemonAlreadyRunningError) throw err;
81993
+ }
81994
+ writeFileSync(file, `${myPid}\n`, "utf8");
81995
+ let released = false;
81996
+ return { release() {
81997
+ if (released) return;
81998
+ released = true;
81999
+ try {
82000
+ if (Number.parseInt(readFileSync(file, "utf8").trim(), 10) === myPid) unlinkSync(file);
82001
+ } catch {}
82002
+ } };
82003
+ }
82004
+ //#endregion
81426
82005
  //#region src/index.ts
81427
82006
  const program = new Command();
81428
82007
  program.name("agentconnect").description("AgentConnect daemon — edge message + agent execution unit").version(DAEMON_VERSION);
81429
82008
  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
82009
  program.command("run").description("Run the daemon in the foreground").action(async () => {
81431
82010
  const opts = program.opts();
82011
+ let lock;
82012
+ try {
82013
+ lock = acquireSingletonLock(resolveRoot(opts.root));
82014
+ } catch (err) {
82015
+ console.error(`agentconnect run: ${err.message}`);
82016
+ process.exit(1);
82017
+ }
81432
82018
  try {
81433
82019
  await runForeground({
81434
82020
  root: opts.root,
@@ -81443,12 +82029,18 @@ program.command("run").description("Run the daemon in the foreground").action(as
81443
82029
  maxAgents: opts.maxAgents ? Number(opts.maxAgents) : void 0
81444
82030
  }
81445
82031
  });
82032
+ lock.release();
81446
82033
  process.exit(0);
81447
82034
  } catch (err) {
82035
+ lock.release();
81448
82036
  console.error(`agentconnect run: ${err.message}`);
81449
82037
  process.exit(1);
81450
82038
  }
81451
82039
  });
82040
+ program.command("mcp-bridge", { hidden: true }).description("internal: stdio MCP bridge to the running daemon").action(async () => {
82041
+ const { runBridge } = await import("./bridge-Dj8U1Jp7.js");
82042
+ await runBridge();
82043
+ });
81452
82044
  const controller = () => resolveController({ root: program.opts().root });
81453
82045
  const requireInstalled = (c) => {
81454
82046
  if (!c.isInstalled()) {
@@ -81580,6 +82172,6 @@ program.command("chat [message]").description("Discover an agent under --agents-
81580
82172
  });
81581
82173
  program.parse();
81582
82174
  //#endregion
81583
- export {};
82175
+ 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
82176
 
81585
82177
  //# sourceMappingURL=index.js.map