@agentconnect.md/daemon 1.0.0-rc.48 → 1.0.0-rc.49
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 +312 -120
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16889,6 +16889,128 @@ AGENT_METHODS.session_cancel, AGENT_METHODS.nes_accept, AGENT_METHODS.nes_reject
|
|
|
16889
16889
|
CLIENT_METHODS.session_request_permission, CLIENT_METHODS.fs_write_text_file, CLIENT_METHODS.fs_read_text_file, CLIENT_METHODS.terminal_create, CLIENT_METHODS.terminal_output, CLIENT_METHODS.terminal_release, CLIENT_METHODS.terminal_wait_for_exit, CLIENT_METHODS.terminal_kill, CLIENT_METHODS.elicitation_create;
|
|
16890
16890
|
CLIENT_METHODS.session_update, CLIENT_METHODS.elicitation_complete;
|
|
16891
16891
|
//#endregion
|
|
16892
|
+
//#region src/runtimes/probe.ts
|
|
16893
|
+
/**
|
|
16894
|
+
* Host-availability probing for runtimes.
|
|
16895
|
+
*
|
|
16896
|
+
* `resolveRuntimes()` produces a runtime for every ACP-registry entry that has a
|
|
16897
|
+
* distribution for this platform — but that says nothing about whether the tool
|
|
16898
|
+
* is actually runnable *here*. This module answers that question so the daemon
|
|
16899
|
+
* only advertises genuinely-installed runtimes in `RegisterReq.capabilities`.
|
|
16900
|
+
*
|
|
16901
|
+
* Two layers:
|
|
16902
|
+
* 1. A generic launcher check — is the runtime's `command` resolvable? For a
|
|
16903
|
+
* runtime distributed as a real binary (`command` is the tool's own
|
|
16904
|
+
* executable) this is a meaningful signal: the binary on `$PATH` means it's
|
|
16905
|
+
* installed.
|
|
16906
|
+
* 2. For runtimes distributed via a package launcher (`npx`/`uvx`), (1) is
|
|
16907
|
+
* meaningless — the launcher fetches on demand, so "npx exists" is true on
|
|
16908
|
+
* virtually every dev host and tells us nothing about the wrapped agent. For
|
|
16909
|
+
* those we REQUIRE a per-runtime custom probe (`CUSTOM_PROBES`) that looks for
|
|
16910
|
+
* the wrapped CLI's own config/state dir, which appears once the user has
|
|
16911
|
+
* installed and run/initialized it. A launcher-distributed runtime with no
|
|
16912
|
+
* probe is treated as NOT installed — otherwise every npx/uvx agent in the
|
|
16913
|
+
* ACP registry would be falsely advertised just because `npx` is on `$PATH`.
|
|
16914
|
+
*
|
|
16915
|
+
* The custom probes test for the config *directory* (the "installed & initialized
|
|
16916
|
+
* on this host" signal). A stricter "is logged in" check would look for the auth
|
|
16917
|
+
* file instead (e.g. `~/.codex/auth.json`), but that yields false negatives for
|
|
16918
|
+
* users who authenticate via an API-key env var, so directory presence is the
|
|
16919
|
+
* pragmatic signal. Paths verified against each tool's docs (June 2026).
|
|
16920
|
+
*/
|
|
16921
|
+
const isWin = process.platform === "win32";
|
|
16922
|
+
function isExecutableFile(p) {
|
|
16923
|
+
try {
|
|
16924
|
+
accessSync(p, isWin ? constants.F_OK : constants.X_OK);
|
|
16925
|
+
return true;
|
|
16926
|
+
} catch {
|
|
16927
|
+
return false;
|
|
16928
|
+
}
|
|
16929
|
+
}
|
|
16930
|
+
/**
|
|
16931
|
+
* Resolve a launcher command the way a shell would. A command containing a path
|
|
16932
|
+
* separator is treated as a literal path; a bare name is searched across `$PATH`
|
|
16933
|
+
* (trying `$PATHEXT` extensions on Windows).
|
|
16934
|
+
*/
|
|
16935
|
+
function isCommandAvailable(command, env = process.env) {
|
|
16936
|
+
return resolveCommandPath(command, env) !== void 0;
|
|
16937
|
+
}
|
|
16938
|
+
/**
|
|
16939
|
+
* Resolve a launcher/command to its absolute path the way a shell would — a path
|
|
16940
|
+
* containing a separator is checked literally; a bare name is searched across
|
|
16941
|
+
* `$PATH` (trying `$PATHEXT` extensions on Windows). Returns undefined if not found.
|
|
16942
|
+
*/
|
|
16943
|
+
function resolveCommandPath(command, env = process.env) {
|
|
16944
|
+
const exts = isWin ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
|
|
16945
|
+
if (command.includes("/") || isWin && command.includes("\\")) return exts.map((ext) => command + ext).find(isExecutableFile);
|
|
16946
|
+
const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
16947
|
+
for (const dir of dirs) for (const ext of exts) {
|
|
16948
|
+
const p = join(dir, command) + ext;
|
|
16949
|
+
if (isExecutableFile(p)) return p;
|
|
16950
|
+
}
|
|
16951
|
+
}
|
|
16952
|
+
function home(env) {
|
|
16953
|
+
return (isWin ? env.USERPROFILE : env.HOME) || homedir();
|
|
16954
|
+
}
|
|
16955
|
+
function xdgConfigHome(env) {
|
|
16956
|
+
return env.XDG_CONFIG_HOME || join(home(env), ".config");
|
|
16957
|
+
}
|
|
16958
|
+
function xdgDataHome(env) {
|
|
16959
|
+
return env.XDG_DATA_HOME || join(home(env), ".local", "share");
|
|
16960
|
+
}
|
|
16961
|
+
function pathExists(p) {
|
|
16962
|
+
return !!p && existsSync(p);
|
|
16963
|
+
}
|
|
16964
|
+
/** True if any candidate path exists (undefined candidates are skipped). */
|
|
16965
|
+
function anyExists(...candidates) {
|
|
16966
|
+
return candidates.some(pathExists);
|
|
16967
|
+
}
|
|
16968
|
+
/**
|
|
16969
|
+
* Custom probes keyed by ACP-registry runtime id. Add an entry here whenever the
|
|
16970
|
+
* launcher-on-PATH check is too weak to mean "actually installed & set up".
|
|
16971
|
+
* Each tests for the tool's home/XDG config dir (or, where the dir is ambiguous,
|
|
16972
|
+
* a tool-specific file), honoring the documented env overrides.
|
|
16973
|
+
*/
|
|
16974
|
+
const CUSTOM_PROBES = {
|
|
16975
|
+
"claude-acp": (env) => anyExists(join(home(env), ".claude"), join(home(env), ".claude.json")),
|
|
16976
|
+
"codex-acp": (env) => anyExists(env.CODEX_HOME, join(home(env), ".codex")),
|
|
16977
|
+
gemini: (env) => pathExists(join(home(env), ".gemini")),
|
|
16978
|
+
"qwen-code": (env) => pathExists(join(home(env), ".qwen")),
|
|
16979
|
+
"github-copilot-cli": (env) => anyExists(env.COPILOT_HOME, join(home(env), ".copilot")),
|
|
16980
|
+
cursor: (env) => anyExists(env.CURSOR_CONFIG_DIR ? join(env.CURSOR_CONFIG_DIR, "cli-config.json") : void 0, join(home(env), ".cursor", "cli-config.json")),
|
|
16981
|
+
opencode: (env) => anyExists(join(xdgConfigHome(env), "opencode"), join(xdgDataHome(env), "opencode", "auth.json")),
|
|
16982
|
+
goose: (env) => pathExists(join(xdgConfigHome(env), "goose")),
|
|
16983
|
+
"amp-acp": (env) => anyExists(env.AMP_SETTINGS_FILE, join(xdgConfigHome(env), "amp")),
|
|
16984
|
+
auggie: (env) => pathExists(join(home(env), ".augment")),
|
|
16985
|
+
cline: (env) => anyExists(env.CLINE_DATA_DIR, join(home(env), ".cline")),
|
|
16986
|
+
"grok-build": (env) => pathExists(join(home(env), ".grok")),
|
|
16987
|
+
kimi: (env) => anyExists(env.KIMI_CODE_HOME, join(home(env), ".kimi"), join(home(env), ".kimi-code")),
|
|
16988
|
+
"factory-droid": (env) => pathExists(join(home(env), ".factory")),
|
|
16989
|
+
devin: (env) => anyExists(join(xdgConfigHome(env), "devin"), join(xdgDataHome(env), "devin"))
|
|
16990
|
+
};
|
|
16991
|
+
/**
|
|
16992
|
+
* Package launchers that fetch-and-run on demand. When a runtime is launched via
|
|
16993
|
+
* one of these, the launcher being on `$PATH` says nothing about whether the
|
|
16994
|
+
* wrapped agent is installed, so such runtimes require a custom probe to count.
|
|
16995
|
+
*/
|
|
16996
|
+
const PACKAGE_LAUNCHERS = /* @__PURE__ */ new Set(["npx", "uvx"]);
|
|
16997
|
+
/** Is this runtime actually usable on this host? */
|
|
16998
|
+
function isRuntimeAvailable(id, rt, env = process.env) {
|
|
16999
|
+
if (!isCommandAvailable(rt.command, env)) return false;
|
|
17000
|
+
const custom = CUSTOM_PROBES[id];
|
|
17001
|
+
if (custom) return custom(env);
|
|
17002
|
+
return !PACKAGE_LAUNCHERS.has(rt.command);
|
|
17003
|
+
}
|
|
17004
|
+
/**
|
|
17005
|
+
* Filter a runtime map down to those installed on this host — what the daemon
|
|
17006
|
+
* should report to the Control Plane and is able to launch.
|
|
17007
|
+
*/
|
|
17008
|
+
function installedRuntimes(runtimes, env = process.env) {
|
|
17009
|
+
const out = {};
|
|
17010
|
+
for (const [id, rt] of Object.entries(runtimes)) if (isRuntimeAvailable(id, rt, env)) out[id] = rt;
|
|
17011
|
+
return out;
|
|
17012
|
+
}
|
|
17013
|
+
//#endregion
|
|
16892
17014
|
//#region src/acp/acp-host.ts
|
|
16893
17015
|
const PROTOCOL_VERSION = 1;
|
|
16894
17016
|
/**
|
|
@@ -16920,19 +17042,32 @@ var AcpHost = class {
|
|
|
16920
17042
|
this.runtime = runtime;
|
|
16921
17043
|
this.opts = opts;
|
|
16922
17044
|
}
|
|
17045
|
+
/** A Claude Code runtime (its command/args reference `claude`) — these embed the
|
|
17046
|
+
* @anthropic-ai/claude-agent-sdk, which needs a Claude Code executable. */
|
|
17047
|
+
isClaudeRuntime() {
|
|
17048
|
+
return [this.runtime.command, ...this.runtime.args].join(" ").toLowerCase().includes("claude");
|
|
17049
|
+
}
|
|
16923
17050
|
async start() {
|
|
16924
17051
|
if (this.child) throw new Error("AcpHost: already started");
|
|
17052
|
+
const env = {
|
|
17053
|
+
...process.env,
|
|
17054
|
+
...Object.fromEntries(this.runtime.env.map((e) => [e.name, e.value])),
|
|
17055
|
+
...this.opts.env ?? {}
|
|
17056
|
+
};
|
|
17057
|
+
if (!env.CLAUDE_CODE_EXECUTABLE && this.isClaudeRuntime()) {
|
|
17058
|
+
const claude = resolveCommandPath("claude", env);
|
|
17059
|
+
if (claude) {
|
|
17060
|
+
env.CLAUDE_CODE_EXECUTABLE = claude;
|
|
17061
|
+
this.opts.log?.info(`acp: CLAUDE_CODE_EXECUTABLE not set — using claude on PATH (${claude})`);
|
|
17062
|
+
}
|
|
17063
|
+
}
|
|
16925
17064
|
const child = spawn(this.runtime.command, this.runtime.args, {
|
|
16926
17065
|
stdio: [
|
|
16927
17066
|
"pipe",
|
|
16928
17067
|
"pipe",
|
|
16929
17068
|
"inherit"
|
|
16930
17069
|
],
|
|
16931
|
-
env
|
|
16932
|
-
...process.env,
|
|
16933
|
-
...Object.fromEntries(this.runtime.env.map((e) => [e.name, e.value])),
|
|
16934
|
-
...this.opts.env ?? {}
|
|
16935
|
-
}
|
|
17070
|
+
env
|
|
16936
17071
|
});
|
|
16937
17072
|
this.child = child;
|
|
16938
17073
|
if (!child.stdin || !child.stdout) throw new Error("AcpHost: subprocess stdin/stdout are not piped");
|
|
@@ -17398,6 +17533,61 @@ const AgentScopeDenied = object({
|
|
|
17398
17533
|
capability: string()
|
|
17399
17534
|
});
|
|
17400
17535
|
//#endregion
|
|
17536
|
+
//#region ../protocol/dist/frames/integration.js
|
|
17537
|
+
/**
|
|
17538
|
+
* Platform integration distribution (C→D) — the Slack "install" flow.
|
|
17539
|
+
*
|
|
17540
|
+
* The Control Plane is the source of truth for platform integrations and pushes
|
|
17541
|
+
* them to the daemon that owns the integration's agent (`integration/upsert`, and
|
|
17542
|
+
* the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
|
|
17543
|
+
* Mode connection from the delivered config (see slack/connection.ts).
|
|
17544
|
+
*
|
|
17545
|
+
* SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
|
|
17546
|
+
* platform tokens (botToken/appToken). These payloads MUST NEVER be logged — no
|
|
17547
|
+
* body dump on decode error, no register/ok snapshot debug dump.
|
|
17548
|
+
*
|
|
17549
|
+
* `signingSecret` is intentionally absent: Socket Mode authenticates with the
|
|
17550
|
+
* app-level token, so the daemon never needs a signing secret.
|
|
17551
|
+
*/
|
|
17552
|
+
/** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
|
|
17553
|
+
const BindMatch = discriminatedUnion("kind", [
|
|
17554
|
+
object({ kind: literal("mention") }),
|
|
17555
|
+
object({ kind: literal("dm") }),
|
|
17556
|
+
object({
|
|
17557
|
+
kind: literal("keyword"),
|
|
17558
|
+
value: string()
|
|
17559
|
+
}),
|
|
17560
|
+
object({ kind: literal("auto") })
|
|
17561
|
+
]);
|
|
17562
|
+
/** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
|
|
17563
|
+
const IntegrationBindRule = object({
|
|
17564
|
+
channel: string().optional(),
|
|
17565
|
+
thread: string().optional(),
|
|
17566
|
+
match: BindMatch
|
|
17567
|
+
});
|
|
17568
|
+
/** The Slack config the daemon needs to open + route a socket (no signingSecret). */
|
|
17569
|
+
const IntegrationSlackConfig = object({
|
|
17570
|
+
botToken: string(),
|
|
17571
|
+
appToken: string(),
|
|
17572
|
+
allowedUserIds: array(string()).default([]),
|
|
17573
|
+
notificationChannelId: string().optional(),
|
|
17574
|
+
bindRules: array(IntegrationBindRule).default([])
|
|
17575
|
+
});
|
|
17576
|
+
/**
|
|
17577
|
+
* One platform integration, owned by exactly one agent. Also the element type of
|
|
17578
|
+
* `RegisterOk.integrations[]` (the per-daemon reconcile set).
|
|
17579
|
+
*/
|
|
17580
|
+
const IntegrationSpec = object({
|
|
17581
|
+
integrationId: string().uuid(),
|
|
17582
|
+
agentId: string().uuid(),
|
|
17583
|
+
platform: literal("slack"),
|
|
17584
|
+
slack: IntegrationSlackConfig
|
|
17585
|
+
});
|
|
17586
|
+
/** C→D EVT — install/update an integration on the owning agent's daemon. */
|
|
17587
|
+
const IntegrationUpsert = IntegrationSpec;
|
|
17588
|
+
/** C→D EVT — remove an integration from the daemon. */
|
|
17589
|
+
const IntegrationRemove = object({ integrationId: string().uuid() });
|
|
17590
|
+
//#endregion
|
|
17401
17591
|
//#region ../protocol/dist/frames/register.js
|
|
17402
17592
|
/**
|
|
17403
17593
|
* Capability upload + the reconcile snapshot — protocol §3.3.
|
|
@@ -17426,6 +17616,7 @@ const RegisterOk = object({
|
|
|
17426
17616
|
assignments: array(RouteAssign),
|
|
17427
17617
|
agents: array(AgentSpec.extend({ agentId: string().uuid() })).default([]),
|
|
17428
17618
|
crons: array(CronUpsert),
|
|
17619
|
+
integrations: array(IntegrationSpec).default([]),
|
|
17429
17620
|
leases: array(SecretsGrant),
|
|
17430
17621
|
drop: object({
|
|
17431
17622
|
assignments: array(string()),
|
|
@@ -17527,6 +17718,8 @@ const FRAME_SCHEMAS = {
|
|
|
17527
17718
|
"drain/done": DrainDone,
|
|
17528
17719
|
"cron/upsert": CronUpsert,
|
|
17529
17720
|
"cron/remove": CronRemove,
|
|
17721
|
+
"integration/upsert": IntegrationUpsert,
|
|
17722
|
+
"integration/remove": IntegrationRemove,
|
|
17530
17723
|
"secrets/request": SecretsRequest,
|
|
17531
17724
|
"secrets/grant": SecretsGrant,
|
|
17532
17725
|
"secrets/renew": SecretsRenew,
|
|
@@ -17641,6 +17834,8 @@ discriminatedUnion("type", [
|
|
|
17641
17834
|
frame("drain/done", FRAME_SCHEMAS["drain/done"]),
|
|
17642
17835
|
frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
|
|
17643
17836
|
frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
|
|
17837
|
+
frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
|
|
17838
|
+
frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
|
|
17644
17839
|
frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
|
|
17645
17840
|
frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
|
|
17646
17841
|
frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
|
|
@@ -81451,116 +81646,6 @@ var Scheduler = class {
|
|
|
81451
81646
|
}
|
|
81452
81647
|
};
|
|
81453
81648
|
//#endregion
|
|
81454
|
-
//#region src/runtimes/probe.ts
|
|
81455
|
-
/**
|
|
81456
|
-
* Host-availability probing for runtimes.
|
|
81457
|
-
*
|
|
81458
|
-
* `resolveRuntimes()` produces a runtime for every ACP-registry entry that has a
|
|
81459
|
-
* distribution for this platform — but that says nothing about whether the tool
|
|
81460
|
-
* is actually runnable *here*. This module answers that question so the daemon
|
|
81461
|
-
* only advertises genuinely-installed runtimes in `RegisterReq.capabilities`.
|
|
81462
|
-
*
|
|
81463
|
-
* Two layers:
|
|
81464
|
-
* 1. A generic launcher check — is the runtime's `command` resolvable? For a
|
|
81465
|
-
* runtime distributed as a real binary (`command` is the tool's own
|
|
81466
|
-
* executable) this is a meaningful signal: the binary on `$PATH` means it's
|
|
81467
|
-
* installed.
|
|
81468
|
-
* 2. For runtimes distributed via a package launcher (`npx`/`uvx`), (1) is
|
|
81469
|
-
* meaningless — the launcher fetches on demand, so "npx exists" is true on
|
|
81470
|
-
* virtually every dev host and tells us nothing about the wrapped agent. For
|
|
81471
|
-
* those we REQUIRE a per-runtime custom probe (`CUSTOM_PROBES`) that looks for
|
|
81472
|
-
* the wrapped CLI's own config/state dir, which appears once the user has
|
|
81473
|
-
* installed and run/initialized it. A launcher-distributed runtime with no
|
|
81474
|
-
* probe is treated as NOT installed — otherwise every npx/uvx agent in the
|
|
81475
|
-
* ACP registry would be falsely advertised just because `npx` is on `$PATH`.
|
|
81476
|
-
*
|
|
81477
|
-
* The custom probes test for the config *directory* (the "installed & initialized
|
|
81478
|
-
* on this host" signal). A stricter "is logged in" check would look for the auth
|
|
81479
|
-
* file instead (e.g. `~/.codex/auth.json`), but that yields false negatives for
|
|
81480
|
-
* users who authenticate via an API-key env var, so directory presence is the
|
|
81481
|
-
* pragmatic signal. Paths verified against each tool's docs (June 2026).
|
|
81482
|
-
*/
|
|
81483
|
-
const isWin = process.platform === "win32";
|
|
81484
|
-
function isExecutableFile(p) {
|
|
81485
|
-
try {
|
|
81486
|
-
accessSync(p, isWin ? constants.F_OK : constants.X_OK);
|
|
81487
|
-
return true;
|
|
81488
|
-
} catch {
|
|
81489
|
-
return false;
|
|
81490
|
-
}
|
|
81491
|
-
}
|
|
81492
|
-
/**
|
|
81493
|
-
* Resolve a launcher command the way a shell would. A command containing a path
|
|
81494
|
-
* separator is treated as a literal path; a bare name is searched across `$PATH`
|
|
81495
|
-
* (trying `$PATHEXT` extensions on Windows).
|
|
81496
|
-
*/
|
|
81497
|
-
function isCommandAvailable(command, env = process.env) {
|
|
81498
|
-
const exts = isWin ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
|
|
81499
|
-
if (command.includes("/") || isWin && command.includes("\\")) return exts.some((ext) => isExecutableFile(command + ext));
|
|
81500
|
-
return (env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => exts.some((ext) => isExecutableFile(join(dir, command) + ext)));
|
|
81501
|
-
}
|
|
81502
|
-
function home(env) {
|
|
81503
|
-
return (isWin ? env.USERPROFILE : env.HOME) || homedir();
|
|
81504
|
-
}
|
|
81505
|
-
function xdgConfigHome(env) {
|
|
81506
|
-
return env.XDG_CONFIG_HOME || join(home(env), ".config");
|
|
81507
|
-
}
|
|
81508
|
-
function xdgDataHome(env) {
|
|
81509
|
-
return env.XDG_DATA_HOME || join(home(env), ".local", "share");
|
|
81510
|
-
}
|
|
81511
|
-
function pathExists(p) {
|
|
81512
|
-
return !!p && existsSync(p);
|
|
81513
|
-
}
|
|
81514
|
-
/** True if any candidate path exists (undefined candidates are skipped). */
|
|
81515
|
-
function anyExists(...candidates) {
|
|
81516
|
-
return candidates.some(pathExists);
|
|
81517
|
-
}
|
|
81518
|
-
/**
|
|
81519
|
-
* Custom probes keyed by ACP-registry runtime id. Add an entry here whenever the
|
|
81520
|
-
* launcher-on-PATH check is too weak to mean "actually installed & set up".
|
|
81521
|
-
* Each tests for the tool's home/XDG config dir (or, where the dir is ambiguous,
|
|
81522
|
-
* a tool-specific file), honoring the documented env overrides.
|
|
81523
|
-
*/
|
|
81524
|
-
const CUSTOM_PROBES = {
|
|
81525
|
-
"claude-acp": (env) => anyExists(join(home(env), ".claude"), join(home(env), ".claude.json")),
|
|
81526
|
-
"codex-acp": (env) => anyExists(env.CODEX_HOME, join(home(env), ".codex")),
|
|
81527
|
-
gemini: (env) => pathExists(join(home(env), ".gemini")),
|
|
81528
|
-
"qwen-code": (env) => pathExists(join(home(env), ".qwen")),
|
|
81529
|
-
"github-copilot-cli": (env) => anyExists(env.COPILOT_HOME, join(home(env), ".copilot")),
|
|
81530
|
-
cursor: (env) => anyExists(env.CURSOR_CONFIG_DIR ? join(env.CURSOR_CONFIG_DIR, "cli-config.json") : void 0, join(home(env), ".cursor", "cli-config.json")),
|
|
81531
|
-
opencode: (env) => anyExists(join(xdgConfigHome(env), "opencode"), join(xdgDataHome(env), "opencode", "auth.json")),
|
|
81532
|
-
goose: (env) => pathExists(join(xdgConfigHome(env), "goose")),
|
|
81533
|
-
"amp-acp": (env) => anyExists(env.AMP_SETTINGS_FILE, join(xdgConfigHome(env), "amp")),
|
|
81534
|
-
auggie: (env) => pathExists(join(home(env), ".augment")),
|
|
81535
|
-
cline: (env) => anyExists(env.CLINE_DATA_DIR, join(home(env), ".cline")),
|
|
81536
|
-
"grok-build": (env) => pathExists(join(home(env), ".grok")),
|
|
81537
|
-
kimi: (env) => anyExists(env.KIMI_CODE_HOME, join(home(env), ".kimi"), join(home(env), ".kimi-code")),
|
|
81538
|
-
"factory-droid": (env) => pathExists(join(home(env), ".factory")),
|
|
81539
|
-
devin: (env) => anyExists(join(xdgConfigHome(env), "devin"), join(xdgDataHome(env), "devin"))
|
|
81540
|
-
};
|
|
81541
|
-
/**
|
|
81542
|
-
* Package launchers that fetch-and-run on demand. When a runtime is launched via
|
|
81543
|
-
* one of these, the launcher being on `$PATH` says nothing about whether the
|
|
81544
|
-
* wrapped agent is installed, so such runtimes require a custom probe to count.
|
|
81545
|
-
*/
|
|
81546
|
-
const PACKAGE_LAUNCHERS = /* @__PURE__ */ new Set(["npx", "uvx"]);
|
|
81547
|
-
/** Is this runtime actually usable on this host? */
|
|
81548
|
-
function isRuntimeAvailable(id, rt, env = process.env) {
|
|
81549
|
-
if (!isCommandAvailable(rt.command, env)) return false;
|
|
81550
|
-
const custom = CUSTOM_PROBES[id];
|
|
81551
|
-
if (custom) return custom(env);
|
|
81552
|
-
return !PACKAGE_LAUNCHERS.has(rt.command);
|
|
81553
|
-
}
|
|
81554
|
-
/**
|
|
81555
|
-
* Filter a runtime map down to those installed on this host — what the daemon
|
|
81556
|
-
* should report to the Control Plane and is able to launch.
|
|
81557
|
-
*/
|
|
81558
|
-
function installedRuntimes(runtimes, env = process.env) {
|
|
81559
|
-
const out = {};
|
|
81560
|
-
for (const [id, rt] of Object.entries(runtimes)) if (isRuntimeAvailable(id, rt, env)) out[id] = rt;
|
|
81561
|
-
return out;
|
|
81562
|
-
}
|
|
81563
|
-
//#endregion
|
|
81564
81649
|
//#region src/runtimes/runtime-prober.ts
|
|
81565
81650
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
81566
81651
|
const DEFAULT_CONCURRENCY = 3;
|
|
@@ -81963,6 +82048,12 @@ var CpClient = class {
|
|
|
81963
82048
|
case "agent/remove":
|
|
81964
82049
|
this.deps.configApply.applyAgentRemove(frame.payload.agentId);
|
|
81965
82050
|
return;
|
|
82051
|
+
case "integration/upsert":
|
|
82052
|
+
this.deps.configApply.applyIntegrationUpsert(frame.payload);
|
|
82053
|
+
return;
|
|
82054
|
+
case "integration/remove":
|
|
82055
|
+
this.deps.configApply.applyIntegrationRemove(frame.payload.integrationId);
|
|
82056
|
+
return;
|
|
81966
82057
|
case "agent/launch": {
|
|
81967
82058
|
const launch = frame.payload;
|
|
81968
82059
|
this.deps.configApply.applyAgentLaunch(launch).then((launched) => this.reply(frame, "agent/launched", launched)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/launch failed: ${err.message}`, false));
|
|
@@ -82259,6 +82350,59 @@ var CpAgentRegistry = class {
|
|
|
82259
82350
|
}
|
|
82260
82351
|
};
|
|
82261
82352
|
//#endregion
|
|
82353
|
+
//#region src/cp/cp-integration-registry.ts
|
|
82354
|
+
/** Map the wire spec to the daemon's `Integration` shape (agent-schema). */
|
|
82355
|
+
function toIntegration(spec) {
|
|
82356
|
+
return {
|
|
82357
|
+
id: spec.integrationId,
|
|
82358
|
+
platform: "slack",
|
|
82359
|
+
slack: {
|
|
82360
|
+
botToken: spec.slack.botToken,
|
|
82361
|
+
appToken: spec.slack.appToken,
|
|
82362
|
+
allowedUserIds: spec.slack.allowedUserIds,
|
|
82363
|
+
...spec.slack.notificationChannelId ? { notificationChannelId: spec.slack.notificationChannelId } : {},
|
|
82364
|
+
bindRules: spec.slack.bindRules
|
|
82365
|
+
}
|
|
82366
|
+
};
|
|
82367
|
+
}
|
|
82368
|
+
var CpIntegrationRegistry = class {
|
|
82369
|
+
onChange;
|
|
82370
|
+
byId = /* @__PURE__ */ new Map();
|
|
82371
|
+
constructor(onChange) {
|
|
82372
|
+
this.onChange = onChange;
|
|
82373
|
+
}
|
|
82374
|
+
/** Add or replace one CP integration (integration/upsert EVT). */
|
|
82375
|
+
upsert(spec) {
|
|
82376
|
+
this.byId.set(spec.integrationId, spec);
|
|
82377
|
+
this.onChange();
|
|
82378
|
+
}
|
|
82379
|
+
/** Drop one CP integration (integration/remove EVT). */
|
|
82380
|
+
remove(integrationId) {
|
|
82381
|
+
if (this.byId.delete(integrationId)) this.onChange();
|
|
82382
|
+
}
|
|
82383
|
+
/**
|
|
82384
|
+
* Replace the whole CP-owned set from the `register/ok` reconcile roster (the CP
|
|
82385
|
+
* is authoritative and the roster is already filtered to THIS daemon).
|
|
82386
|
+
*/
|
|
82387
|
+
converge(specs) {
|
|
82388
|
+
this.byId = new Map((specs ?? []).map((s) => [s.integrationId, s]));
|
|
82389
|
+
this.onChange();
|
|
82390
|
+
}
|
|
82391
|
+
/** CP integrations grouped by owning agentId, as daemon `Integration[]`. */
|
|
82392
|
+
byAgent() {
|
|
82393
|
+
const out = /* @__PURE__ */ new Map();
|
|
82394
|
+
for (const spec of this.byId.values()) {
|
|
82395
|
+
const list = out.get(spec.agentId) ?? [];
|
|
82396
|
+
list.push(toIntegration(spec));
|
|
82397
|
+
out.set(spec.agentId, list);
|
|
82398
|
+
}
|
|
82399
|
+
return out;
|
|
82400
|
+
}
|
|
82401
|
+
get size() {
|
|
82402
|
+
return this.byId.size;
|
|
82403
|
+
}
|
|
82404
|
+
};
|
|
82405
|
+
//#endregion
|
|
82262
82406
|
//#region src/cp/config-apply.ts
|
|
82263
82407
|
const LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
82264
82408
|
"trace",
|
|
@@ -82399,6 +82543,8 @@ var Daemon = class Daemon {
|
|
|
82399
82543
|
cpClient;
|
|
82400
82544
|
cpCrons;
|
|
82401
82545
|
cpAgents;
|
|
82546
|
+
cpIntegrations;
|
|
82547
|
+
warnedOrphanIntegrations = /* @__PURE__ */ new Set();
|
|
82402
82548
|
botUserIds = {};
|
|
82403
82549
|
cpRouting;
|
|
82404
82550
|
clock;
|
|
@@ -82461,6 +82607,7 @@ var Daemon = class Daemon {
|
|
|
82461
82607
|
knownRuntimes: Object.keys(this.runtimes),
|
|
82462
82608
|
warn: (m) => this.log.warn(m)
|
|
82463
82609
|
}, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
|
|
82610
|
+
this.cpIntegrations = new CpIntegrationRegistry(() => void this.reconcile().catch((err) => this.log.error(`cp: integration reconcile failed: ${err.stack ?? err}`)));
|
|
82464
82611
|
for (const a of this.effectiveAgents()) {
|
|
82465
82612
|
this.agents.set(a.id, a);
|
|
82466
82613
|
this.prefetchClone(a);
|
|
@@ -82559,13 +82706,50 @@ var Daemon = class Daemon {
|
|
|
82559
82706
|
return this.opts.agentName ? [selectAgent(this.agentsDir, this.opts.agentName)] : loadAgents(this.agentsDir);
|
|
82560
82707
|
}
|
|
82561
82708
|
/**
|
|
82562
|
-
* Effective agent set = the on-disk `agent.json` files
|
|
82563
|
-
*
|
|
82564
|
-
* cp/cp-agent-registry.ts
|
|
82565
|
-
*
|
|
82709
|
+
* Effective agent set = the on-disk `agent.json` files, with the CP-owned
|
|
82710
|
+
* integrations overlaid in memory. Agent SPECS are no longer overlaid — `agent/
|
|
82711
|
+
* upsert` writes those straight to disk (cp/cp-agent-registry.ts). INTEGRATIONS,
|
|
82712
|
+
* however, carry plaintext tokens and are kept in memory only (cp/cp-integration-
|
|
82713
|
+
* registry.ts); they are merged onto the matching file agent's `integrations[]`
|
|
82714
|
+
* HERE — the single seam so consolidate, routing rules, tool injection, and reply
|
|
82715
|
+
* routing (all of which read `agent.integrations`) see one merged view.
|
|
82566
82716
|
*/
|
|
82567
82717
|
effectiveAgents() {
|
|
82568
|
-
|
|
82718
|
+
const files = [...this.fileAgents.values()];
|
|
82719
|
+
const cpByAgent = this.cpIntegrations?.byAgent();
|
|
82720
|
+
if (!cpByAgent || cpByAgent.size === 0) return files;
|
|
82721
|
+
return files.map((a) => {
|
|
82722
|
+
const cp = cpByAgent.get(a.id);
|
|
82723
|
+
if (!cp || cp.length === 0) return a;
|
|
82724
|
+
const byId = new Map(a.integrations.map((i) => [i.id, i]));
|
|
82725
|
+
for (const i of cp) byId.set(i.id, i);
|
|
82726
|
+
return {
|
|
82727
|
+
...a,
|
|
82728
|
+
integrations: [...byId.values()]
|
|
82729
|
+
};
|
|
82730
|
+
});
|
|
82731
|
+
}
|
|
82732
|
+
/**
|
|
82733
|
+
* Warn (once per orphan) about CP integrations whose owning agent is absent /
|
|
82734
|
+
* inactive on disk: they are held in the registry but overlaid onto no agent, so
|
|
82735
|
+
* no socket opens. Self-heals when the agent appears (a later reconcile connects
|
|
82736
|
+
* it). Logs integrationId + agentId ONLY — never the token material.
|
|
82737
|
+
*/
|
|
82738
|
+
warnOrphanIntegrations() {
|
|
82739
|
+
const cpByAgent = this.cpIntegrations?.byAgent();
|
|
82740
|
+
if (!cpByAgent) return;
|
|
82741
|
+
const live = /* @__PURE__ */ new Set();
|
|
82742
|
+
for (const [agentId, ints] of cpByAgent) {
|
|
82743
|
+
if (this.fileAgents.has(agentId)) continue;
|
|
82744
|
+
for (const int of ints) {
|
|
82745
|
+
live.add(int.id);
|
|
82746
|
+
if (!this.warnedOrphanIntegrations.has(int.id)) {
|
|
82747
|
+
this.warnedOrphanIntegrations.add(int.id);
|
|
82748
|
+
this.log.warn(`slack: integration ${int.id} references missing/inactive agent ${agentId} — not connected`);
|
|
82749
|
+
}
|
|
82750
|
+
}
|
|
82751
|
+
}
|
|
82752
|
+
for (const id of this.warnedOrphanIntegrations) if (!live.has(id)) this.warnedOrphanIntegrations.delete(id);
|
|
82569
82753
|
}
|
|
82570
82754
|
/**
|
|
82571
82755
|
* Converge the running set to the desired effective agents. Driven by both the
|
|
@@ -82599,6 +82783,7 @@ var Daemon = class Daemon {
|
|
|
82599
82783
|
async runReconcile() {
|
|
82600
82784
|
const files = this.loadAgentList();
|
|
82601
82785
|
this.fileAgents = new Map(files.map((a) => [a.id, a]));
|
|
82786
|
+
this.warnOrphanIntegrations();
|
|
82602
82787
|
const desired = this.effectiveAgents();
|
|
82603
82788
|
const { toStart, toStop, toChange } = diffAgents(desired, this.agents);
|
|
82604
82789
|
if (toStart.length || toStop.length || toChange.length) this.log.info(`reconcile: ${desired.length} desired agent(s) from ${this.agentsDir}; start=[${toStart.map((a) => a.id).join(", ")}] stop=[${toStop.join(", ")}] change=[${toChange.map((c) => {
|
|
@@ -83250,6 +83435,7 @@ var Daemon = class Daemon {
|
|
|
83250
83435
|
applyReconcileSnapshot: (snap) => {
|
|
83251
83436
|
this.cpCrons?.converge(snap.crons);
|
|
83252
83437
|
this.cpAgents?.converge(snap.agents);
|
|
83438
|
+
this.cpIntegrations?.converge(snap.integrations);
|
|
83253
83439
|
this.cpRouting?.converge({
|
|
83254
83440
|
routingEpoch: snap.routingEpoch,
|
|
83255
83441
|
assignments: snap.assignments,
|
|
@@ -83258,9 +83444,12 @@ var Daemon = class Daemon {
|
|
|
83258
83444
|
if (snap.leases.length) this.log.debug(`cp: ${snap.leases.length} lease(s) noted (secrets handled later)`);
|
|
83259
83445
|
if (snap.assignments.length) this.log.debug(`cp: converged ${snap.assignments.length} assignment(s)`);
|
|
83260
83446
|
if (snap.agents.length) this.log.debug(`cp: converged ${snap.agents.length} agent spec(s)`);
|
|
83447
|
+
if (snap.integrations?.length) this.log.debug(`cp: converged ${snap.integrations.length} integration(s)`);
|
|
83261
83448
|
},
|
|
83262
83449
|
applyAgentUpsert: (agentId, spec) => this.cpAgents?.upsert(agentId, spec),
|
|
83263
83450
|
applyAgentRemove: (agentId) => this.cpAgents?.remove(agentId),
|
|
83451
|
+
applyIntegrationUpsert: (spec) => this.cpIntegrations?.upsert(spec),
|
|
83452
|
+
applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
|
|
83264
83453
|
upsertCron: (cron) => this.cpCrons.upsert(cron),
|
|
83265
83454
|
removeCron: (cronId) => this.cpCrons.remove(cronId),
|
|
83266
83455
|
applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
|
|
@@ -83670,6 +83859,9 @@ program.name("agentconnect").description("AgentConnect daemon — edge message +
|
|
|
83670
83859
|
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)");
|
|
83671
83860
|
program.command("run").description("Run the daemon in the foreground").action(async () => {
|
|
83672
83861
|
const opts = program.opts();
|
|
83862
|
+
process.on("unhandledRejection", (reason) => {
|
|
83863
|
+
console.error(`agentconnect: unhandled rejection (continuing): ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
83864
|
+
});
|
|
83673
83865
|
let lock;
|
|
83674
83866
|
try {
|
|
83675
83867
|
lock = acquireSingletonLock(resolveRoot(opts.root));
|