@agentconnect.md/daemon 1.0.0-rc.10 → 1.0.0-rc.12

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
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
3
3
  import { EventEmitter } from "node:events";
4
4
  import childProcess, { execFile, spawn } from "node:child_process";
5
5
  import * as sp from "node:path";
6
- import path, { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
7
- import fs, { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, stat, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
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";
8
8
  import process$1 from "node:process";
9
9
  import { stripVTControlCharacters } from "node:util";
10
10
  import { fileURLToPath } from "node:url";
@@ -7483,6 +7483,19 @@ async function fetchRegistry(root, opts = {}) {
7483
7483
  clearTimeout(timer);
7484
7484
  }
7485
7485
  }
7486
+ /**
7487
+ * Display names (registry id -> human-facing `name`) read from the local cache.
7488
+ * Used for capability reporting — the daemon advertises the tool name to the CP
7489
+ * rather than the registry id. Entries with no `name` are omitted (callers fall
7490
+ * back to the id). Returns `{}` when no cached registry doc exists.
7491
+ */
7492
+ function cachedRuntimeNames(root) {
7493
+ const doc = readCachedDoc(root);
7494
+ if (!doc) return {};
7495
+ const out = {};
7496
+ for (const [id, entry] of Object.entries(doc.agents)) if (entry.name) out[id] = entry.name;
7497
+ return out;
7498
+ }
7486
7499
  async function defaultRuntimes(root, opts = {}) {
7487
7500
  const cached = (opts.mode ?? "blocking") === "cache-first" ? readCachedDoc(root) : null;
7488
7501
  let doc;
@@ -79998,6 +80011,104 @@ var Scheduler = class {
79998
80011
  }
79999
80012
  };
80000
80013
  //#endregion
80014
+ //#region src/runtimes/probe.ts
80015
+ /**
80016
+ * Host-availability probing for runtimes.
80017
+ *
80018
+ * `resolveRuntimes()` produces a runtime for every ACP-registry entry that has a
80019
+ * distribution for this platform — but that says nothing about whether the tool
80020
+ * is actually runnable *here*. This module answers that question so the daemon
80021
+ * only advertises genuinely-installed runtimes in `RegisterReq.capabilities`.
80022
+ *
80023
+ * Two layers:
80024
+ * 1. A generic launcher check — is the runtime's `command` resolvable? Most
80025
+ * agents ship as `npx`/`uvx` packages, which fetch on demand, so the launcher
80026
+ * being present is enough for them.
80027
+ * 2. Per-runtime custom probes (`CUSTOM_PROBES`) for the cases where (1) is too
80028
+ * weak — almost every agent runs via `npx`, so "npx exists" is meaningless.
80029
+ * For those we additionally look for the wrapped CLI's own config/state dir,
80030
+ * which appears once the user has installed and run/initialized it.
80031
+ *
80032
+ * The custom probes test for the config *directory* (the "installed & initialized
80033
+ * on this host" signal). A stricter "is logged in" check would look for the auth
80034
+ * file instead (e.g. `~/.codex/auth.json`), but that yields false negatives for
80035
+ * users who authenticate via an API-key env var, so directory presence is the
80036
+ * pragmatic signal. Paths verified against each tool's docs (June 2026).
80037
+ */
80038
+ const isWin = process.platform === "win32";
80039
+ function isExecutableFile(p) {
80040
+ try {
80041
+ accessSync(p, isWin ? constants.F_OK : constants.X_OK);
80042
+ return true;
80043
+ } catch {
80044
+ return false;
80045
+ }
80046
+ }
80047
+ /**
80048
+ * Resolve a launcher command the way a shell would. A command containing a path
80049
+ * separator is treated as a literal path; a bare name is searched across `$PATH`
80050
+ * (trying `$PATHEXT` extensions on Windows).
80051
+ */
80052
+ function isCommandAvailable(command, env = process.env) {
80053
+ const exts = isWin ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
80054
+ if (command.includes("/") || isWin && command.includes("\\")) return exts.some((ext) => isExecutableFile(command + ext));
80055
+ return (env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => exts.some((ext) => isExecutableFile(join(dir, command) + ext)));
80056
+ }
80057
+ function home(env) {
80058
+ return (isWin ? env.USERPROFILE : env.HOME) || homedir();
80059
+ }
80060
+ function xdgConfigHome(env) {
80061
+ return env.XDG_CONFIG_HOME || join(home(env), ".config");
80062
+ }
80063
+ function xdgDataHome(env) {
80064
+ return env.XDG_DATA_HOME || join(home(env), ".local", "share");
80065
+ }
80066
+ function pathExists(p) {
80067
+ return !!p && existsSync(p);
80068
+ }
80069
+ /** True if any candidate path exists (undefined candidates are skipped). */
80070
+ function anyExists(...candidates) {
80071
+ return candidates.some(pathExists);
80072
+ }
80073
+ /**
80074
+ * Custom probes keyed by ACP-registry runtime id. Add an entry here whenever the
80075
+ * launcher-on-PATH check is too weak to mean "actually installed & set up".
80076
+ * Each tests for the tool's home/XDG config dir (or, where the dir is ambiguous,
80077
+ * a tool-specific file), honoring the documented env overrides.
80078
+ */
80079
+ const CUSTOM_PROBES = {
80080
+ "claude-acp": (env) => anyExists(join(home(env), ".claude"), join(home(env), ".claude.json")),
80081
+ "codex-acp": (env) => anyExists(env.CODEX_HOME, join(home(env), ".codex")),
80082
+ gemini: (env) => pathExists(join(home(env), ".gemini")),
80083
+ "qwen-code": (env) => pathExists(join(home(env), ".qwen")),
80084
+ "github-copilot-cli": (env) => anyExists(env.COPILOT_HOME, join(home(env), ".copilot")),
80085
+ cursor: (env) => anyExists(env.CURSOR_CONFIG_DIR ? join(env.CURSOR_CONFIG_DIR, "cli-config.json") : void 0, join(home(env), ".cursor", "cli-config.json")),
80086
+ opencode: (env) => anyExists(join(xdgConfigHome(env), "opencode"), join(xdgDataHome(env), "opencode", "auth.json")),
80087
+ goose: (env) => pathExists(join(xdgConfigHome(env), "goose")),
80088
+ "amp-acp": (env) => anyExists(env.AMP_SETTINGS_FILE, join(xdgConfigHome(env), "amp")),
80089
+ auggie: (env) => pathExists(join(home(env), ".augment")),
80090
+ cline: (env) => anyExists(env.CLINE_DATA_DIR, join(home(env), ".cline")),
80091
+ "grok-build": (env) => pathExists(join(home(env), ".grok")),
80092
+ kimi: (env) => anyExists(env.KIMI_CODE_HOME, join(home(env), ".kimi"), join(home(env), ".kimi-code")),
80093
+ "factory-droid": (env) => pathExists(join(home(env), ".factory")),
80094
+ devin: (env) => anyExists(join(xdgConfigHome(env), "devin"), join(xdgDataHome(env), "devin"))
80095
+ };
80096
+ /** Is this runtime actually usable on this host? */
80097
+ function isRuntimeAvailable(id, rt, env = process.env) {
80098
+ if (!isCommandAvailable(rt.command, env)) return false;
80099
+ const custom = CUSTOM_PROBES[id];
80100
+ return custom ? custom(env) : true;
80101
+ }
80102
+ /**
80103
+ * Filter a runtime map down to those installed on this host — what the daemon
80104
+ * should report to the Control Plane and is able to launch.
80105
+ */
80106
+ function installedRuntimes(runtimes, env = process.env) {
80107
+ const out = {};
80108
+ for (const [id, rt] of Object.entries(runtimes)) if (isRuntimeAvailable(id, rt, env)) out[id] = rt;
80109
+ return out;
80110
+ }
80111
+ //#endregion
80001
80112
  //#region src/log.ts
80002
80113
  const ORDER = {
80003
80114
  trace: 10,
@@ -80442,6 +80553,7 @@ var Daemon = class {
80442
80553
  log = makeLogger("info");
80443
80554
  root = "";
80444
80555
  runtimes = {};
80556
+ runtimeNames = {};
80445
80557
  cpClient;
80446
80558
  cpCrons;
80447
80559
  botUserIds = {};
@@ -80471,11 +80583,15 @@ var Daemon = class {
80471
80583
  for (const a of agents) this.agents.set(a.id, a);
80472
80584
  this.log.info(`loaded ${agents.length} agent(s) from ${this.agentsDir}${agents.length ? `: ${agents.map((a) => a.id).join(", ")}` : ""}`);
80473
80585
  this.root = root;
80474
- this.runtimes = await resolveRuntimes(cfg, root, {
80586
+ const resolvedRuntimes = await resolveRuntimes(cfg, root, {
80475
80587
  neededRuntimes: agents.map((a) => a.runtime),
80476
80588
  mode: "cache-first"
80477
80589
  });
80590
+ this.runtimes = installedRuntimes(resolvedRuntimes);
80591
+ this.runtimeNames = cachedRuntimeNames(root);
80478
80592
  this.log.info(`runtimes ready: ${Object.keys(this.runtimes).join(", ") || "(none)"}`);
80593
+ const skipped = Object.keys(resolvedRuntimes).filter((id) => !this.runtimes[id]);
80594
+ if (skipped.length) this.log.info(`runtimes not installed (skipped): ${skipped.join(", ")}`);
80479
80595
  this.store = new LocalStore(statePath(root));
80480
80596
  this.cpRouting = new CpRoutingLayer({
80481
80597
  load: () => {
@@ -80564,7 +80680,7 @@ var Daemon = class {
80564
80680
  if (this.opts.hostFactory) host = this.opts.hostFactory(agent, onUpdate);
80565
80681
  else {
80566
80682
  const runtime = this.runtimes[agent.runtime];
80567
- if (!runtime) throw new Error(`runtime "${agent.runtime}" not in config.runtimes or ACP registry`);
80683
+ if (!runtime) throw new Error(`runtime "${agent.runtime}" not available: not installed on this host, or absent from config.runtimes / the ACP registry`);
80568
80684
  host = new AcpHost(runtime, {
80569
80685
  onUpdate,
80570
80686
  env: agentChildEnv(agent)
@@ -80729,7 +80845,7 @@ var Daemon = class {
80729
80845
  maxAgents: this.cfg.limits.maxAgents,
80730
80846
  capabilities: () => ({
80731
80847
  platforms: ["slack"],
80732
- runtimes: Object.keys(this.runtimes),
80848
+ runtimes: Object.keys(this.runtimes).map((id) => this.runtimeNames[id] ?? id),
80733
80849
  acp: true,
80734
80850
  features: []
80735
80851
  }),