@pwrdrvr/agent-acp 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,36 @@
1
+ import {
2
+ ACP_REGISTRY_URL,
3
+ AcpAgentAllowlist,
4
+ AcpRegistryService,
5
+ BANNED_ACP_REGISTRY_IDS,
6
+ DEFAULT_ACP_AGENT_ALLOWLIST,
7
+ DEFAULT_ACP_PROBE_TIMEOUT_MS,
8
+ defaultAcpAgentAllowlist,
9
+ discoverLocalAcpAgentInstances,
10
+ discoverLocalAcpAgents,
11
+ evaluateAcpDistributionPolicy,
12
+ isBannedAcpRegistryId,
13
+ normalizeRegistry,
14
+ wellKnownAgentBinDirs
15
+ } from "./chunk-GCWY5T5H.js";
16
+ import {
17
+ BUILT_IN_ACP_STRATEGIES,
18
+ buildAcpBackendId,
19
+ buildStrategyTable,
20
+ defaultQuirks,
21
+ geminiStrategy,
22
+ grokStrategy,
23
+ kimiStrategy,
24
+ qwenStrategy,
25
+ strategyByBackendId,
26
+ strategyById
27
+ } from "./chunk-OFMIS7W6.js";
28
+ import {
29
+ normalizeAcpMcpServerConfig,
30
+ normalizeAcpMcpServerConfigs,
31
+ redactAcpMcpCredentials
32
+ } from "./chunk-64RRSPSB.js";
33
+
1
34
  // src/acp-connection.ts
2
35
  import { spawn } from "child_process";
3
36
  import { Readable, Writable } from "stream";
@@ -265,9 +298,9 @@ function readFirstLocationPath(record) {
265
298
  return void 0;
266
299
  }
267
300
  for (const location of locations) {
268
- const path5 = readString(asRecord(location), "path");
269
- if (path5 && path5.trim()) {
270
- return path5;
301
+ const path = readString(asRecord(location), "path");
302
+ if (path && path.trim()) {
303
+ return path;
271
304
  }
272
305
  }
273
306
  return void 0;
@@ -303,7 +336,7 @@ function normalizeStatus(status) {
303
336
  return void 0;
304
337
  }
305
338
  }
306
- function toolKindFor(acpKind, command, path5, label) {
339
+ function toolKindFor(acpKind, command, path, label) {
307
340
  switch (acpKind) {
308
341
  case "edit":
309
342
  case "write":
@@ -324,18 +357,18 @@ function toolKindFor(acpKind, command, path5, label) {
324
357
  if (command) return "command";
325
358
  const inferred = inferToolKind(label);
326
359
  if (inferred !== "other") return inferred;
327
- return path5 ? "read" : "other";
360
+ return path ? "read" : "other";
328
361
  }
329
362
  function toolCallFromUpdate(update, kind, sessionId) {
330
363
  const id = readToolCallId(update, kind, sessionId);
331
364
  const label = readString(update, "title") ?? readString(update, "name") ?? readString(update, "kind") ?? kind.replaceAll("_", " ");
332
365
  const acpKind = readString(update, "kind");
333
- const path5 = readString(update, "path") ?? readFirstLocationPath(update);
366
+ const path = readString(update, "path") ?? readFirstLocationPath(update);
334
367
  const command = readString(update, "command");
335
368
  const output = readToolOutput(update);
336
369
  const exitCode = readNumber(update, "exitCode") ?? readNumber(update, "exit_code");
337
370
  const status = normalizeStatus(readString(update, "status"));
338
- const toolKind = toolKindFor(acpKind, command, path5, label);
371
+ const toolKind = toolKindFor(acpKind, command, path, label);
339
372
  const call = {
340
373
  id,
341
374
  name: readString(update, "name") ?? acpKind ?? label,
@@ -369,9 +402,9 @@ function readToolLocations(update) {
369
402
  const out = [];
370
403
  for (const item of raw) {
371
404
  const record = asRecord(item);
372
- const path5 = record ? readString(record, "path") : void 0;
373
- if (path5 === void 0) continue;
374
- const location = { path: path5 };
405
+ const path = record ? readString(record, "path") : void 0;
406
+ if (path === void 0) continue;
407
+ const location = { path };
375
408
  const line = record ? readNumber(record, "line") : void 0;
376
409
  if (line !== void 0) location.line = line;
377
410
  out.push(location);
@@ -702,9 +735,10 @@ function acpRuntimeSupportsSseMcp(capabilities) {
702
735
  return capabilities?.agentCapabilities?.mcp?.sse === true;
703
736
  }
704
737
  function modelConfigOption(capabilities) {
705
- return capabilities?.configOptions?.find(
706
- (option) => option.category === "model" || option.id === "model"
707
- );
738
+ return capabilities?.configOptions?.find(isModelConfigOption);
739
+ }
740
+ function isModelConfigOption(option) {
741
+ return option.category === "model" || option.id === "model";
708
742
  }
709
743
  function modelIdFromCapabilities(capabilities) {
710
744
  return capabilities?.models?.currentModelId ?? modelConfigOption(capabilities)?.currentValue;
@@ -751,6 +785,8 @@ function acpSessionRuntimeStateFromUpdate(update, now) {
751
785
  return currentModeId ? { currentModeId, updatedAt: now } : void 0;
752
786
  }
753
787
  if (kind === "config_option_update") {
788
+ const fullSet = configOptionUpdateSet(update);
789
+ if (fullSet !== void 0) return runtimeStateFromConfigOptionSet(fullSet, now);
754
790
  const configOption = asRecord(update.configOption ?? update.config_option) ?? update;
755
791
  const id = readString(configOption, "id") ?? readString(configOption, "configOptionId") ?? readString(configOption, "configId");
756
792
  const value = readString(configOption, "currentValue") ?? readString(configOption, "value");
@@ -768,6 +804,24 @@ function mergeAcpRuntimeState(existing, update) {
768
804
  }
769
805
  };
770
806
  }
807
+ function configOptionUpdateSet(update) {
808
+ if (readKind(update) !== "config_option_update") return void 0;
809
+ const set = update.configOptions ?? update.config_options;
810
+ return Array.isArray(set) ? set : void 0;
811
+ }
812
+ function runtimeStateFromConfigOptionSet(set, now) {
813
+ const options = readConfigOptions(set);
814
+ const configValues = Object.fromEntries(
815
+ options.flatMap(
816
+ (option) => option.currentValue !== void 0 ? [[option.id, option.currentValue]] : []
817
+ )
818
+ );
819
+ if (Object.keys(configValues).length === 0) return void 0;
820
+ const state = { configValues, updatedAt: now };
821
+ const model = options.find(isModelConfigOption)?.currentValue;
822
+ if (model !== void 0) state.currentModelId = model;
823
+ return state;
824
+ }
771
825
  function modeLabelFor(capabilities, modeId) {
772
826
  return capabilities?.modes?.availableModes.find((mode) => mode.id === modeId)?.label;
773
827
  }
@@ -929,51 +983,6 @@ function readAgentInfo(value) {
929
983
  return Object.keys(agentInfo).length > 0 ? agentInfo : void 0;
930
984
  }
931
985
 
932
- // src/mcp-server-config.ts
933
- function isKeyValueArray(collection) {
934
- return Array.isArray(collection);
935
- }
936
- function normalizeKeyValues(collection) {
937
- if (collection === void 0) return [];
938
- if (isKeyValueArray(collection)) {
939
- return collection.map(({ name, value }) => ({ name, value }));
940
- }
941
- return Object.entries(collection).map(([name, value]) => ({ name, value }));
942
- }
943
- function normalizeAcpMcpServerConfig(server) {
944
- if ("type" in server) {
945
- return {
946
- name: server.name,
947
- type: server.type,
948
- url: server.url,
949
- headers: normalizeKeyValues(server.headers)
950
- };
951
- }
952
- return {
953
- name: server.name,
954
- command: server.command,
955
- args: [...server.args ?? []],
956
- env: normalizeKeyValues(server.env)
957
- };
958
- }
959
- function normalizeAcpMcpServerConfigs(servers) {
960
- return servers.map(normalizeAcpMcpServerConfig);
961
- }
962
- function redactAcpMcpCredentials(message, servers) {
963
- const secrets = /* @__PURE__ */ new Set();
964
- for (const server of servers) {
965
- const values = "type" in server ? [server.url, ...normalizeKeyValues(server.headers).map(({ value }) => value)] : normalizeKeyValues(server.env).map(({ value }) => value);
966
- for (const value of values) {
967
- if (value.length > 0) secrets.add(value);
968
- }
969
- }
970
- let redacted = message;
971
- for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
972
- redacted = redacted.split(secret).join("[REDACTED]");
973
- }
974
- return redacted;
975
- }
976
-
977
986
  // src/acp-client.ts
978
987
  var ACP_PROTOCOL_VERSION = 1;
979
988
  var ACP_PROMPT_REQUEST_TIMEOUT_MS = 60 * 6e4;
@@ -1006,6 +1015,11 @@ var AcpAgentClient = class {
1006
1015
  unsubscribeRequest = void 0;
1007
1016
  initialized = false;
1008
1017
  runtimeCapabilities;
1018
+ /** What `initialize` reported: agent-level, and never any one session's. The
1019
+ * base a session's own menus are built on — the client-wide snapshot above is
1020
+ * whichever session wrote last, and a fresh session must not inherit its
1021
+ * menus or their current values. */
1022
+ initializeCapabilities;
1009
1023
  constructor(options) {
1010
1024
  this.transport = options.transport;
1011
1025
  this.strategy = options.strategy;
@@ -1219,6 +1233,7 @@ var AcpAgentClient = class {
1219
1233
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1220
1234
  turnId: void 0,
1221
1235
  runtimeState: void 0,
1236
+ runtimeCapabilities: void 0,
1222
1237
  pendingTurn: void 0,
1223
1238
  pendingInstructions: options.instructions,
1224
1239
  mcpServerNames: configuredMcpServers.map((server) => server.name)
@@ -1260,6 +1275,7 @@ var AcpAgentClient = class {
1260
1275
  normalizer: new AcpSessionNormalizer({ quirks: this.strategy.quirks }),
1261
1276
  turnId: void 0,
1262
1277
  runtimeState: void 0,
1278
+ runtimeCapabilities: void 0,
1263
1279
  pendingTurn: void 0,
1264
1280
  pendingInstructions: options.instructions,
1265
1281
  // Remember ONLY the MCP servers attached to THIS session so the host's
@@ -1272,9 +1288,11 @@ var AcpAgentClient = class {
1272
1288
  }
1273
1289
  const runtimeCapabilities = this.captureRuntimeCapabilities(
1274
1290
  options.lifecycle === "load" ? "session-load" : "session-new",
1275
- result
1291
+ result,
1292
+ this.sessionBase(session)
1276
1293
  );
1277
1294
  if (runtimeCapabilities) {
1295
+ session.runtimeCapabilities = runtimeCapabilities;
1278
1296
  const runtimeState = acpSessionRuntimeStateFromCapabilities(
1279
1297
  runtimeCapabilities,
1280
1298
  this.now()
@@ -1459,11 +1477,12 @@ var AcpAgentClient = class {
1459
1477
  * `session/set_model` answers `{}` and refreshes only via an async
1460
1478
  * notification. On Kimi the model DETERMINES which thought levels exist —
1461
1479
  * `K2.7 Coding` offers only `on`, `K3` offers `low`/`high`/`max` — and
1462
- * `applyReasoning` picks from `runtimeCapabilities` on the following
1480
+ * `applyReasoning` picks from THIS session's menus on the following
1463
1481
  * `startTurn`, so the refresh this write leaves behind is what it reads. */
1464
1482
  async setModel(threadId, modelId) {
1465
- const advertisesModelsCapability = (this.runtimeCapabilities?.models?.availableModels?.length ?? 0) > 0;
1466
- const option = modelConfigOption(this.runtimeCapabilities);
1483
+ const capabilities = this.sessionCapabilities(threadId);
1484
+ const advertisesModelsCapability = (capabilities?.models?.availableModels?.length ?? 0) > 0;
1485
+ const option = modelConfigOption(capabilities);
1467
1486
  if (!advertisesModelsCapability && option !== void 0) {
1468
1487
  await this.setRuntimeOption(threadId, "modelConfigOption", option.id, modelId);
1469
1488
  return;
@@ -1516,8 +1535,13 @@ var AcpAgentClient = class {
1516
1535
  },
1517
1536
  ACP_REQUEST_TIMEOUT_MS
1518
1537
  );
1519
- const runtimeCapabilities = this.captureRuntimeCapabilities("initialize", result);
1538
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1539
+ "initialize",
1540
+ result,
1541
+ this.initializeCapabilities
1542
+ );
1520
1543
  if (runtimeCapabilities) {
1544
+ this.initializeCapabilities = runtimeCapabilities;
1521
1545
  this.notifyRuntimeCapabilities({ runtimeCapabilities });
1522
1546
  }
1523
1547
  this.initialized = true;
@@ -1543,17 +1567,28 @@ var AcpAgentClient = class {
1543
1567
  this.logger.debug("acp session/update for unknown session", { protocolSessionId });
1544
1568
  return;
1545
1569
  }
1570
+ if (configOptionUpdateSet(update) !== void 0) {
1571
+ const refreshed = this.captureRuntimeCapabilities(
1572
+ // A live refresh of this session's menus, not a load: keep the source
1573
+ // they were opened with.
1574
+ session.runtimeCapabilities?.source ?? "session-new",
1575
+ update,
1576
+ this.sessionBase(session)
1577
+ );
1578
+ if (refreshed) session.runtimeCapabilities = refreshed;
1579
+ }
1546
1580
  const runtimeState = acpSessionRuntimeStateFromUpdate(update, this.now());
1547
1581
  if (runtimeState) {
1548
1582
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, runtimeState);
1549
- if (this.runtimeCapabilities) {
1583
+ const capabilities = this.sessionCapabilities(session.threadId);
1584
+ if (capabilities) {
1550
1585
  this.notifyRuntimeCapabilities({
1551
1586
  threadId: session.threadId,
1552
- runtimeCapabilities: this.runtimeCapabilities,
1587
+ runtimeCapabilities: capabilities,
1553
1588
  runtimeState: session.runtimeState
1554
1589
  });
1555
1590
  }
1556
- this.emitThreadSettings(session, this.runtimeCapabilities, session.runtimeState);
1591
+ this.emitThreadSettings(session, capabilities, session.runtimeState);
1557
1592
  return;
1558
1593
  }
1559
1594
  const updateKind = readString2(update, "sessionUpdate");
@@ -1623,7 +1658,12 @@ var AcpAgentClient = class {
1623
1658
  optionId,
1624
1659
  value
1625
1660
  );
1626
- const runtimeCapabilities = this.captureRuntimeCapabilities("session-load", result);
1661
+ const runtimeCapabilities = this.captureRuntimeCapabilities(
1662
+ "session-load",
1663
+ result,
1664
+ this.sessionBase(session)
1665
+ );
1666
+ if (runtimeCapabilities) session.runtimeCapabilities = runtimeCapabilities;
1627
1667
  const requested = source === "configOption" ? { configValues: { [optionId]: value }, updatedAt: this.now() } : source === "mode" ? { currentModeId: value, updatedAt: this.now() } : source === "modelConfigOption" ? (
1628
1668
  // Record BOTH: it IS the config option's value, and it IS the
1629
1669
  // session's model. A consumer reading `currentModelId` must not
@@ -1635,7 +1675,13 @@ var AcpAgentClient = class {
1635
1675
  }
1636
1676
  ) : { currentModelId: value, updatedAt: this.now() };
1637
1677
  session.runtimeState = mergeAcpRuntimeState(session.runtimeState, requested);
1638
- const effectiveCapabilities = runtimeCapabilities ?? this.runtimeCapabilities;
1678
+ const reply = asRecord2(result);
1679
+ const reported = runtimeStateFromConfigOptionSet(
1680
+ reply?.configOptions ?? reply?.config_options,
1681
+ this.now()
1682
+ );
1683
+ if (reported) session.runtimeState = mergeAcpRuntimeState(session.runtimeState, reported);
1684
+ const effectiveCapabilities = this.sessionCapabilities(threadId);
1639
1685
  if (effectiveCapabilities) {
1640
1686
  this.notifyRuntimeCapabilities({
1641
1687
  threadId,
@@ -1676,7 +1722,8 @@ var AcpAgentClient = class {
1676
1722
  * If nothing matches we leave it alone — the caller's `.catch` debug-logs. */
1677
1723
  async applyReasoning(threadId, reasoning) {
1678
1724
  const target = reasoning.toLowerCase();
1679
- const modes = this.runtimeCapabilities?.modes?.availableModes ?? [];
1725
+ const capabilities = this.sessionCapabilities(threadId);
1726
+ const modes = capabilities?.modes?.availableModes ?? [];
1680
1727
  const modeMatch = modes.find(
1681
1728
  (mode) => mode.id.toLowerCase() === target || typeof mode.label === "string" && mode.label.toLowerCase() === target
1682
1729
  );
@@ -1684,7 +1731,7 @@ var AcpAgentClient = class {
1684
1731
  await this.setMode(threadId, modeMatch.id);
1685
1732
  return;
1686
1733
  }
1687
- const thinking = this.runtimeCapabilities?.configOptions?.find(
1734
+ const thinking = capabilities?.configOptions?.find(
1688
1735
  (option) => option.category === "thought_level" || option.id === "thinking"
1689
1736
  );
1690
1737
  if (thinking !== void 0) {
@@ -1714,18 +1761,32 @@ var AcpAgentClient = class {
1714
1761
  data: data.toString("base64")
1715
1762
  };
1716
1763
  }
1717
- captureRuntimeCapabilities(source, result) {
1764
+ /** Normalize `result` into a capability snapshot, filling what it omits from
1765
+ * `base`. Required, not defaulted: an explicit `undefined` would trigger a
1766
+ * default parameter, and the only default on hand is the client-wide
1767
+ * snapshot — which may be another session's. */
1768
+ captureRuntimeCapabilities(source, result, base) {
1718
1769
  const runtimeCapabilities = normalizeAcpRuntimeCapabilities({
1719
1770
  value: result,
1720
1771
  now: this.now(),
1721
1772
  source,
1722
- ...this.runtimeCapabilities !== void 0 ? { initialize: this.runtimeCapabilities } : {}
1773
+ ...base !== void 0 ? { initialize: base } : {}
1723
1774
  });
1724
1775
  if (runtimeCapabilities) {
1725
1776
  this.runtimeCapabilities = runtimeCapabilities;
1726
1777
  }
1727
1778
  return runtimeCapabilities;
1728
1779
  }
1780
+ /** The option menus to consult when choosing a value for `threadId`. A
1781
+ * session with none of its own gets the agent-level `initialize` menus, never
1782
+ * another session's. */
1783
+ sessionCapabilities(threadId) {
1784
+ return this.sessionBase(this.sessions.get(threadId));
1785
+ }
1786
+ /** What a session's menus are built on: its own, or `initialize`'s. */
1787
+ sessionBase(session) {
1788
+ return session?.runtimeCapabilities ?? this.initializeCapabilities;
1789
+ }
1729
1790
  notifyRuntimeCapabilities(event) {
1730
1791
  this.runtimeCapabilitiesHandler?.({
1731
1792
  ...event.threadId !== void 0 ? { threadId: event.threadId } : {},
@@ -1803,6 +1864,7 @@ function thoughtLevelPolarity(value) {
1803
1864
  const haystack = `${value.value} ${value.label ?? ""}`.toLowerCase();
1804
1865
  if (/\b(off|none|disabled?|no)\b/.test(haystack)) return "off";
1805
1866
  if (/\b(on|enabled?|high|full|yes)\b/.test(haystack)) return "on";
1867
+ if (/\b(low|minimal|min|minimum)\b/.test(haystack)) return "low";
1806
1868
  return "other";
1807
1869
  }
1808
1870
  function reasoningValueForThoughtLevel(reasoning, values) {
@@ -1810,8 +1872,9 @@ function reasoningValueForThoughtLevel(reasoning, values) {
1810
1872
  const wantOff = LOW_EFFORT_TOKENS.has(token);
1811
1873
  const wantOn = HIGH_EFFORT_TOKENS.has(token);
1812
1874
  if (!wantOff && !wantOn) return void 0;
1813
- const desired = wantOff ? "off" : "on";
1814
- return values.find((value) => thoughtLevelPolarity(value) === desired)?.value;
1875
+ const withPolarity = (polarity) => values.find((value) => thoughtLevelPolarity(value) === polarity);
1876
+ if (wantOn) return withPolarity("on")?.value;
1877
+ return (withPolarity("off") ?? withPolarity("low"))?.value;
1815
1878
  }
1816
1879
  function buildApprovalRequest(args) {
1817
1880
  const toolCall = asRecord2(args.params.toolCall) ?? {};
@@ -2003,10 +2066,10 @@ function describeUsageShape(result) {
2003
2066
  const paths = [];
2004
2067
  const walk = (record, prefix, depth) => {
2005
2068
  for (const [key, value] of Object.entries(record)) {
2006
- const path5 = prefix ? `${prefix}.${key}` : key;
2007
- if (/token|usage|quota/i.test(key)) paths.push(path5);
2069
+ const path = prefix ? `${prefix}.${key}` : key;
2070
+ if (/token|usage|quota/i.test(key)) paths.push(path);
2008
2071
  const child = asRecord2(value);
2009
- if (child && depth < 3) walk(child, path5, depth + 1);
2072
+ if (child && depth < 3) walk(child, path, depth + 1);
2010
2073
  }
2011
2074
  };
2012
2075
  walk(root, "", 0);
@@ -2218,810 +2281,6 @@ var AcpOneShotClient = class {
2218
2281
  await this.client.close();
2219
2282
  }
2220
2283
  };
2221
-
2222
- // src/strategies/strategy-types.ts
2223
- function buildAcpBackendId(registryId) {
2224
- return `acp:${registryId}`;
2225
- }
2226
- var DEFAULT_QUIRKS = {
2227
- surfaceThoughts: true,
2228
- titleFrom: "topic-update"
2229
- };
2230
- function defaultQuirks(overrides = {}) {
2231
- return { ...DEFAULT_QUIRKS, ...overrides };
2232
- }
2233
-
2234
- // src/strategies/gemini.ts
2235
- var geminiStrategy = {
2236
- id: "gemini",
2237
- backendId: buildAcpBackendId("gemini"),
2238
- displayName: "Gemini CLI",
2239
- authors: ["Google"],
2240
- discoveryProbe: {
2241
- command: "gemini",
2242
- versionArgs: ["--version"],
2243
- helpArgs: ["--help"],
2244
- helpMatches: /(^|\s)--acp(\s|,|$)/
2245
- },
2246
- spawn: {
2247
- command: "gemini",
2248
- args: ["--acp"],
2249
- // Gemini refuses to operate without workspace trust; matching PwrAgnt's
2250
- // launch-descriptor normalization, append --skip-trust + set the env flag.
2251
- ensureArgs: ["--skip-trust"],
2252
- env: { GEMINI_CLI_TRUST_WORKSPACE: "true" }
2253
- },
2254
- quirks: defaultQuirks({ surfaceThoughts: true, titleFrom: "topic-update" })
2255
- };
2256
-
2257
- // src/strategies/grok.ts
2258
- import { homedir } from "os";
2259
- import path from "path";
2260
- var grokStrategy = {
2261
- id: "grok",
2262
- backendId: buildAcpBackendId("grok"),
2263
- displayName: "Grok",
2264
- authors: ["xAI"],
2265
- discoveryProbe: {
2266
- command: "grok",
2267
- versionArgs: ["--version"],
2268
- helpArgs: ["agent", "stdio", "--help"],
2269
- helpMatches: /Run the agent over stdio/i,
2270
- fallbackCommands: [
2271
- path.join(homedir(), ".grok", "bin", "grok"),
2272
- "/opt/homebrew/bin/grok",
2273
- "/usr/local/bin/grok"
2274
- ]
2275
- },
2276
- spawn: {
2277
- command: "grok",
2278
- args: ["agent", "stdio"]
2279
- },
2280
- // Grok auto-generates the thread title via its vendor notification
2281
- // `_x.ai/session_notification` carrying `session_summary_generated`.
2282
- quirks: defaultQuirks({
2283
- surfaceThoughts: true,
2284
- titleFrom: "session-summary",
2285
- vendorNotificationMethods: ["_x.ai/session_notification"]
2286
- })
2287
- };
2288
-
2289
- // src/strategies/kimi.ts
2290
- import { homedir as homedir2 } from "os";
2291
- import path2 from "path";
2292
- var kimiStrategy = {
2293
- id: "kimi",
2294
- backendId: buildAcpBackendId("kimi"),
2295
- displayName: "Kimi Code CLI",
2296
- authors: ["Moonshot AI"],
2297
- discoveryProbe: {
2298
- command: "kimi",
2299
- versionArgs: ["--version"],
2300
- // Capability signal is the EXIT CODE of `kimi acp --help`, NOT its prose.
2301
- // kimi's commander CLI exits non-zero for an unknown subcommand, so a
2302
- // zero-exit proves the `acp` subcommand exists. No `helpMatches`: the help
2303
- // wording has already drifted across kimi versions (0.11.0 prints "Agent
2304
- // Client Protocol (ACP) server over stdio"), so matching it is fragile.
2305
- helpArgs: ["acp", "--help"],
2306
- // The official Kimi Code installer drops a standalone binary at
2307
- // ~/.kimi-code/bin/kimi and does NOT add it to PATH — so a Finder-launched
2308
- // app misses it without this fallback (mirrors grok/qwen).
2309
- fallbackCommands: [
2310
- path2.join(homedir2(), ".kimi-code", "bin", "kimi"),
2311
- "/opt/homebrew/bin/kimi",
2312
- "/usr/local/bin/kimi"
2313
- ]
2314
- },
2315
- spawn: {
2316
- command: "kimi",
2317
- args: ["acp"]
2318
- },
2319
- quirks: defaultQuirks({ surfaceThoughts: true, titleFrom: "topic-update" })
2320
- };
2321
-
2322
- // src/strategies/qwen.ts
2323
- import { homedir as homedir3 } from "os";
2324
- import path3 from "path";
2325
- var qwenStrategy = {
2326
- id: "qwen",
2327
- backendId: buildAcpBackendId("qwen"),
2328
- displayName: "Qwen Code",
2329
- authors: ["Qwen Team"],
2330
- license: "Apache-2.0",
2331
- repositoryUrl: "https://github.com/QwenLM/qwen-code",
2332
- discoveryProbe: {
2333
- command: "qwen",
2334
- versionArgs: ["--version"],
2335
- helpArgs: ["--help"],
2336
- helpMatches: /(^|\s)--acp(\s|,|$)/,
2337
- fallbackCommands: [
2338
- path3.join(homedir3(), ".qwen", "bin", "qwen"),
2339
- "/opt/homebrew/bin/qwen",
2340
- "/usr/local/bin/qwen"
2341
- ]
2342
- },
2343
- spawn: {
2344
- command: "qwen",
2345
- args: ["--acp"]
2346
- },
2347
- // Qwen's thought chunks are noisy internal scaffolding — suppress them.
2348
- quirks: defaultQuirks({ surfaceThoughts: false, titleFrom: "topic-update" })
2349
- };
2350
-
2351
- // src/strategies/index.ts
2352
- var BUILT_IN_ACP_STRATEGIES = [
2353
- geminiStrategy,
2354
- kimiStrategy,
2355
- grokStrategy,
2356
- qwenStrategy
2357
- ];
2358
- function buildStrategyTable(strategies = BUILT_IN_ACP_STRATEGIES) {
2359
- const table = /* @__PURE__ */ new Map();
2360
- for (const strategy of strategies) {
2361
- table.set(strategy.id, strategy);
2362
- }
2363
- return table;
2364
- }
2365
- function strategyById(id, table = buildStrategyTable()) {
2366
- return table.get(id);
2367
- }
2368
- function strategyByBackendId(backendId, strategies = BUILT_IN_ACP_STRATEGIES) {
2369
- return strategies.find((strategy) => strategy.backendId === backendId);
2370
- }
2371
-
2372
- // src/discovery/acp-local-discovery.ts
2373
- import { execFile as execFileCallback } from "child_process";
2374
- import { readdirSync, realpathSync, statSync } from "fs";
2375
- import { homedir as homedir4 } from "os";
2376
- import path4 from "path";
2377
- import { promisify } from "util";
2378
- import {
2379
- createCommandInvocation as createCommandInvocation2,
2380
- prependCommandDirToPath as prependCommandDirToPath2
2381
- } from "@pwrdrvr/agent-transport";
2382
- var execFile = promisify(execFileCallback);
2383
- var DEFAULT_ACP_PROBE_TIMEOUT_MS = 5e3;
2384
- async function discoverLocalAcpAgentInstances(options = {}) {
2385
- const strategies = options.strategies ?? BUILT_IN_ACP_STRATEGIES;
2386
- const env = options.env ?? process.env;
2387
- const probe = options.probe ?? makeDefaultProbe(env);
2388
- const listExecutables = options.listExecutables ?? defaultListExecutables;
2389
- const now = options.now ?? Date.now;
2390
- const probeOptions = {
2391
- timeoutMs: options.probeTimeoutMs ?? DEFAULT_ACP_PROBE_TIMEOUT_MS,
2392
- signal: options.signal
2393
- };
2394
- const groups = await Promise.all(
2395
- strategies.map(
2396
- (strategy) => discoverStrategyInstances(
2397
- strategy,
2398
- probe,
2399
- now,
2400
- env,
2401
- listExecutables,
2402
- options.overrides?.[strategy.id],
2403
- options.includeRejectedCandidates === true,
2404
- probeOptions
2405
- )
2406
- )
2407
- );
2408
- return groups.filter((group) => group !== void 0);
2409
- }
2410
- async function discoverLocalAcpAgents(options = {}) {
2411
- const groups = await discoverLocalAcpAgentInstances(options);
2412
- return groups.flatMap((group) => {
2413
- const first = group.instances[0];
2414
- if (first === void 0) return [];
2415
- const agent = {
2416
- strategyId: group.strategyId,
2417
- backendId: group.backendId,
2418
- name: group.name,
2419
- command: first.command,
2420
- args: group.args,
2421
- env: group.env,
2422
- discoveredAt: group.discoveredAt
2423
- };
2424
- if (first.version !== void 0) agent.version = first.version;
2425
- return [agent];
2426
- });
2427
- }
2428
- async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false, probeOptions = {}) {
2429
- const candidates = candidateCommands(strategy, env, listExecutables, override);
2430
- const instances = [];
2431
- const rejectedInstances = [];
2432
- for (const candidate of candidates) {
2433
- if (probeOptions.signal?.aborted) break;
2434
- const [versionAttempt, helpAttempt] = await Promise.all([
2435
- runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs, probeOptions),
2436
- runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs, probeOptions)
2437
- ]);
2438
- if (probeOptions.signal?.aborted) break;
2439
- const versionResult = versionAttempt.ok ? versionAttempt.result : void 0;
2440
- const helpResult = helpAttempt.ok ? helpAttempt.result : void 0;
2441
- const version = versionResult ? parseCliVersion(resultText(versionResult)) : void 0;
2442
- if (!versionResult || !helpResult) {
2443
- const timedOut = !versionAttempt.ok && versionAttempt.timedOut || !helpAttempt.ok && helpAttempt.timedOut;
2444
- if (candidate.detected && (timedOut || includeRejectedCandidates)) {
2445
- const rejected = {
2446
- command: candidate.command,
2447
- source: candidate.source,
2448
- reason: timedOut ? "probe-timed-out" : versionResult ? "acp-probe-failed" : "version-probe-failed"
2449
- };
2450
- if (version !== void 0) rejected.version = version;
2451
- rejectedInstances.push(rejected);
2452
- }
2453
- continue;
2454
- }
2455
- if (strategy.discoveryProbe.helpMatches !== void 0 && !strategy.discoveryProbe.helpMatches.test(resultText(helpResult))) {
2456
- if (includeRejectedCandidates && candidate.detected) {
2457
- const rejected = {
2458
- command: candidate.command,
2459
- source: candidate.source,
2460
- reason: "acp-help-mismatch"
2461
- };
2462
- if (version !== void 0) rejected.version = version;
2463
- rejectedInstances.push(rejected);
2464
- }
2465
- continue;
2466
- }
2467
- const instance = {
2468
- command: candidate.command,
2469
- source: candidate.source
2470
- };
2471
- if (version !== void 0) instance.version = version;
2472
- instances.push(instance);
2473
- }
2474
- if (instances.length === 0 && rejectedInstances.length === 0) {
2475
- return void 0;
2476
- }
2477
- return {
2478
- strategyId: strategy.id,
2479
- backendId: strategy.backendId,
2480
- name: strategy.displayName,
2481
- args: ensureArgs(strategy.spawn.args, strategy.spawn.ensureArgs),
2482
- env: strategy.spawn.env ?? {},
2483
- instances,
2484
- ...rejectedInstances.length > 0 ? { rejectedInstances } : {},
2485
- discoveredAt: now()
2486
- };
2487
- }
2488
- function candidateCommands(strategy, env, listExecutables, override) {
2489
- const out = [];
2490
- const seen = /* @__PURE__ */ new Set();
2491
- const push = (command, source, detected) => {
2492
- const trimmed = command.trim();
2493
- if (trimmed.length === 0) return;
2494
- const key = canonicalize(trimmed);
2495
- if (seen.has(key)) return;
2496
- seen.add(key);
2497
- out.push({ command: trimmed, source, detected });
2498
- };
2499
- if (override && override.trim().length > 0) {
2500
- push(override, "override", isExecutableFile(override));
2501
- }
2502
- const pathMatches = listExecutables(strategy.discoveryProbe.command, env);
2503
- if (pathMatches.length > 0) {
2504
- for (const match of pathMatches) push(match, "path", true);
2505
- } else {
2506
- push(strategy.discoveryProbe.command, "path", false);
2507
- }
2508
- for (const fallback of strategy.discoveryProbe.fallbackCommands ?? []) {
2509
- push(fallback, "fallback", isExecutableFile(fallback));
2510
- }
2511
- return out;
2512
- }
2513
- function isExecutableFile(command) {
2514
- try {
2515
- const stat = statSync(command);
2516
- return stat.isFile() && (process.platform === "win32" || (stat.mode & 73) !== 0);
2517
- } catch {
2518
- return false;
2519
- }
2520
- }
2521
- function canonicalize(command) {
2522
- if (!path4.isAbsolute(command)) {
2523
- return process.platform === "win32" ? command.toLowerCase() : command;
2524
- }
2525
- try {
2526
- const canonical = realpathSync(command);
2527
- return process.platform === "win32" ? canonical.toLowerCase() : canonical;
2528
- } catch {
2529
- return process.platform === "win32" ? command.toLowerCase() : command;
2530
- }
2531
- }
2532
- function defaultListExecutables(command, env) {
2533
- if (command.includes("/") || command.includes("\\")) return [];
2534
- const found = [];
2535
- const seenDir = /* @__PURE__ */ new Set();
2536
- for (const dir of discoveryDirs(env)) {
2537
- const normalizedDir = normalizePathEntry(dir);
2538
- const dirKey = process.platform === "win32" ? normalizedDir.toLowerCase() : normalizedDir;
2539
- if (normalizedDir.length === 0 || seenDir.has(dirKey)) continue;
2540
- seenDir.add(dirKey);
2541
- for (const commandName of executableCommandNames(command, env)) {
2542
- const candidate = path4.join(normalizedDir, commandName);
2543
- try {
2544
- const stat = statSync(candidate);
2545
- if (stat.isFile() && (process.platform === "win32" || (stat.mode & 73) !== 0)) {
2546
- found.push(candidate);
2547
- }
2548
- } catch {
2549
- }
2550
- }
2551
- }
2552
- return found;
2553
- }
2554
- function executableCommandNames(command, env) {
2555
- if (process.platform !== "win32") return [command];
2556
- const extensions = (readWindowsEnv(env, "PATHEXT")?.trim() || ".COM;.EXE;.BAT;.CMD").split(";").map((extension) => extension.trim()).filter(Boolean).map((extension) => extension.startsWith(".") ? extension : `.${extension}`);
2557
- const commandExtension = path4.win32.extname(command);
2558
- if (commandExtension.length > 0 && extensions.some((extension) => extension.toLowerCase() === commandExtension.toLowerCase())) {
2559
- return [command];
2560
- }
2561
- return extensions.map((extension) => `${command}${extension}`);
2562
- }
2563
- function readWindowsEnv(env, name) {
2564
- const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
2565
- return key ? env[key] : void 0;
2566
- }
2567
- function normalizePathEntry(entry) {
2568
- const trimmed = entry.trim();
2569
- const quoted = trimmed.match(/^"(.*)"$/);
2570
- return quoted?.[1] ?? trimmed;
2571
- }
2572
- function discoveryDirs(env) {
2573
- const pathValue = process.platform === "win32" ? readWindowsEnv(env, "PATH") ?? "" : env.PATH ?? "";
2574
- const pathDirs = pathValue.split(path4.delimiter);
2575
- return process.platform === "win32" ? pathDirs : [...pathDirs, ...wellKnownAgentBinDirs(homedir4())];
2576
- }
2577
- function wellKnownAgentBinDirs(home = homedir4()) {
2578
- return [
2579
- // Every installed nvm node version's bin (the agent may be under any one).
2580
- ...nvmNodeBinDirs(home),
2581
- // Other version / package managers.
2582
- path4.join(home, ".volta", "bin"),
2583
- path4.join(home, ".bun", "bin"),
2584
- path4.join(home, ".asdf", "shims"),
2585
- path4.join(home, ".deno", "bin"),
2586
- path4.join(home, ".local", "bin"),
2587
- path4.join(home, ".npm-global", "bin"),
2588
- // Common system install prefixes.
2589
- "/opt/homebrew/bin",
2590
- "/usr/local/bin"
2591
- ];
2592
- }
2593
- function nvmNodeBinDirs(home) {
2594
- const base = path4.join(home, ".nvm", "versions", "node");
2595
- try {
2596
- return readdirSync(base).sort().reverse().map((version) => path4.join(base, version, "bin"));
2597
- } catch {
2598
- return [];
2599
- }
2600
- }
2601
- function ensureArgs(args, ensure) {
2602
- if (!ensure || ensure.length === 0) {
2603
- return args;
2604
- }
2605
- const result = [...args];
2606
- for (const arg of ensure) {
2607
- if (!result.includes(arg)) result.push(arg);
2608
- }
2609
- return result;
2610
- }
2611
- var AcpProbeTimeoutError = class extends Error {
2612
- constructor(command, timeoutMs) {
2613
- super(`acp probe did not answer within ${timeoutMs}ms: ${command}`);
2614
- this.name = "AcpProbeTimeoutError";
2615
- }
2616
- };
2617
- function isProbeTimeout(error) {
2618
- if (error instanceof AcpProbeTimeoutError) return true;
2619
- const name = error?.name;
2620
- return error?.killed === true || name === "AbortError" || name === "TimeoutError";
2621
- }
2622
- function normalizeProbeTimeoutMs(timeoutMs) {
2623
- if (timeoutMs === void 0 || !Number.isFinite(timeoutMs)) {
2624
- return DEFAULT_ACP_PROBE_TIMEOUT_MS;
2625
- }
2626
- return Math.max(1, Math.floor(timeoutMs));
2627
- }
2628
- function makeDefaultProbe(env) {
2629
- return async (command, args, options = {}) => {
2630
- const timeoutMs = normalizeProbeTimeoutMs(options.timeoutMs);
2631
- const launchEnv = prependCommandDirToPath2(command, env);
2632
- const invocation = createCommandInvocation2({ command, args, env: launchEnv });
2633
- const controller = new AbortController();
2634
- let timedOut = false;
2635
- const onAbort = () => controller.abort();
2636
- options.signal?.addEventListener("abort", onAbort, { once: true });
2637
- const timer = setTimeout(() => {
2638
- timedOut = true;
2639
- controller.abort();
2640
- }, timeoutMs);
2641
- const expired = new Promise((resolve) => {
2642
- controller.signal.addEventListener("abort", () => resolve("expired"), {
2643
- once: true
2644
- });
2645
- });
2646
- try {
2647
- const execution = execFile(invocation.command, invocation.args, {
2648
- timeout: timeoutMs,
2649
- signal: controller.signal,
2650
- maxBuffer: 1024 * 1024,
2651
- // A Windows `.cmd` shim runs through `cmd.exe`; without this every
2652
- // discovery pass flashes console windows over the host app.
2653
- windowsHide: true,
2654
- // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2655
- // installed `qwen`) finds its sibling `node` during the probe.
2656
- env: launchEnv,
2657
- windowsVerbatimArguments: invocation.windowsVerbatimArguments
2658
- }).then(
2659
- (result) => ({ ok: true, result }),
2660
- (error) => ({ ok: false, error })
2661
- );
2662
- const settled = await Promise.race([execution, expired]);
2663
- if (settled === "expired") {
2664
- if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2665
- throw new Error(`acp probe aborted: ${command}`);
2666
- }
2667
- if (!settled.ok) {
2668
- if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2669
- throw settled.error;
2670
- }
2671
- return settled.result;
2672
- } finally {
2673
- clearTimeout(timer);
2674
- options.signal?.removeEventListener("abort", onAbort);
2675
- }
2676
- };
2677
- }
2678
- async function runProbe(probe, command, args, options) {
2679
- try {
2680
- return { ok: true, result: await probe(command, args, options) };
2681
- } catch (error) {
2682
- return { ok: false, timedOut: isProbeTimeout(error) };
2683
- }
2684
- }
2685
- function resultText(result) {
2686
- return [result.stdout, result.stderr].flatMap((value) => value === void 0 ? [] : [value]).map((value) => Buffer.isBuffer(value) ? value.toString("utf8") : value).join("\n");
2687
- }
2688
- function parseCliVersion(output) {
2689
- const trimmed = output.trim();
2690
- if (!trimmed) {
2691
- return void 0;
2692
- }
2693
- return trimmed.match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/)?.[0] ?? trimmed;
2694
- }
2695
-
2696
- // src/discovery/acp-agent-allowlist.ts
2697
- var BANNED_ACP_REGISTRY_IDS = /* @__PURE__ */ new Set(["codex-acp"]);
2698
- var DEFAULT_ACP_AGENT_ALLOWLIST = [
2699
- { id: "local-grok-cli", registryId: "grok", distributionKinds: ["local"] },
2700
- { id: "local-qwen-code-cli", registryId: "qwen", distributionKinds: ["local"] }
2701
- ];
2702
- var AcpAgentAllowlist = class {
2703
- constructor(rules) {
2704
- this.rules = rules;
2705
- }
2706
- rules;
2707
- evaluate(agent) {
2708
- if (isBannedAcpRegistryId(agent.id)) {
2709
- return { allowed: false, reason: "banned" };
2710
- }
2711
- const matchingRules = this.rules.filter((rule) => rule.registryId === agent.id);
2712
- if (matchingRules.length === 0) {
2713
- return { allowed: false, reason: "not-allowlisted" };
2714
- }
2715
- for (const rule of matchingRules) {
2716
- const denial = evaluateRule(rule, agent);
2717
- if (!denial) {
2718
- return {
2719
- allowed: true,
2720
- ruleId: rule.id,
2721
- unverifiedBinaryAllowed: rule.allowUnverifiedBinary === true
2722
- };
2723
- }
2724
- }
2725
- return { allowed: false, reason: "allowlist-rule-mismatch" };
2726
- }
2727
- evaluateDistribution(agent, distribution) {
2728
- if (isBannedAcpRegistryId(agent.id)) {
2729
- return { allowed: false, reason: "banned" };
2730
- }
2731
- const matchingRules = this.rules.filter((rule) => rule.registryId === agent.id);
2732
- if (matchingRules.length === 0) {
2733
- return { allowed: false, reason: "not-allowlisted" };
2734
- }
2735
- for (const rule of matchingRules) {
2736
- const denial = evaluateDistributionRule(rule, agent, distribution);
2737
- if (!denial) {
2738
- return {
2739
- allowed: true,
2740
- ruleId: rule.id,
2741
- unverifiedBinaryAllowed: rule.allowUnverifiedBinary === true
2742
- };
2743
- }
2744
- }
2745
- return { allowed: false, reason: "allowlist-rule-mismatch" };
2746
- }
2747
- };
2748
- var defaultAcpAgentAllowlist = new AcpAgentAllowlist(
2749
- DEFAULT_ACP_AGENT_ALLOWLIST
2750
- );
2751
- function isBannedAcpRegistryId(registryId) {
2752
- return BANNED_ACP_REGISTRY_IDS.has(registryId);
2753
- }
2754
- function evaluateRule(rule, agent) {
2755
- if (rule.versions && (!agent.version || !rule.versions.includes(agent.version))) {
2756
- return "version-not-allowed";
2757
- }
2758
- if (isGplFamilyLicense(agent.license) && !rule.allowGplFamilyLicense) {
2759
- return "license-not-allowed";
2760
- }
2761
- let distributionDeniedBySource = false;
2762
- for (const distribution of agent.distributions) {
2763
- const denial = evaluateDistributionRule(rule, agent, distribution, {
2764
- skipAgentChecks: true
2765
- });
2766
- if (!denial) {
2767
- return void 0;
2768
- }
2769
- if (denial === "distribution-source-not-allowed") {
2770
- distributionDeniedBySource = true;
2771
- }
2772
- }
2773
- return distributionDeniedBySource ? "distribution-source-not-allowed" : "distribution-not-allowed";
2774
- }
2775
- function evaluateDistributionRule(rule, agent, distribution, options = {}) {
2776
- if (!options.skipAgentChecks && rule.versions && (!agent.version || !rule.versions.includes(agent.version))) {
2777
- return "version-not-allowed";
2778
- }
2779
- if (!options.skipAgentChecks && isGplFamilyLicense(agent.license) && !rule.allowGplFamilyLicense) {
2780
- return "license-not-allowed";
2781
- }
2782
- if (!distributionAllowedByKind(rule, distribution)) {
2783
- return "distribution-not-allowed";
2784
- }
2785
- if (!distributionSourceAllowed(rule, distribution)) {
2786
- return "distribution-source-not-allowed";
2787
- }
2788
- return void 0;
2789
- }
2790
- function distributionAllowedByKind(rule, distribution) {
2791
- return !rule.distributionKinds || rule.distributionKinds.includes(distribution.kind);
2792
- }
2793
- function distributionSourceAllowed(rule, distribution) {
2794
- if (distribution.kind === "npx" || distribution.kind === "uvx") {
2795
- return !rule.allowedPackageNames || rule.allowedPackageNames.includes(distribution.packageName);
2796
- }
2797
- if (distribution.kind !== "binary") {
2798
- return false;
2799
- }
2800
- if (!rule.allowedArchiveHosts) {
2801
- return true;
2802
- }
2803
- try {
2804
- return rule.allowedArchiveHosts.includes(new URL(distribution.archiveUrl).host);
2805
- } catch {
2806
- return false;
2807
- }
2808
- }
2809
- function isGplFamilyLicense(license) {
2810
- return /\b(?:GPL|AGPL|LGPL)\b/i.test(license ?? "");
2811
- }
2812
-
2813
- // src/discovery/acp-registry-types.ts
2814
- var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
2815
-
2816
- // src/discovery/acp-registry-service.ts
2817
- var AcpRegistryService = class {
2818
- allowlist;
2819
- fetcher;
2820
- now;
2821
- registryUrl;
2822
- constructor(options = {}) {
2823
- this.allowlist = options.allowlist ?? defaultAcpAgentAllowlist;
2824
- this.fetcher = options.fetch ?? (async (input, init) => {
2825
- const response = await globalThis.fetch(input, init);
2826
- return {
2827
- ok: response.ok,
2828
- status: response.status,
2829
- statusText: response.statusText,
2830
- json: () => response.json()
2831
- };
2832
- });
2833
- this.now = options.now ?? Date.now;
2834
- this.registryUrl = options.registryUrl ?? ACP_REGISTRY_URL;
2835
- }
2836
- async fetchRegistry() {
2837
- const response = await this.fetcher(this.registryUrl, {
2838
- headers: { accept: "application/json" }
2839
- });
2840
- if (!response.ok) {
2841
- throw new Error(
2842
- `ACP registry request failed: ${response.status} ${response.statusText}`
2843
- );
2844
- }
2845
- const raw = await response.json();
2846
- return { fetchedAt: this.now(), agents: normalizeRegistry(raw), raw };
2847
- }
2848
- applyAllowlist(snapshot) {
2849
- return snapshot.agents.map((agent) => {
2850
- const allowlist = this.allowlist.evaluate(agent);
2851
- const distributionPolicies = agent.distributions.map(
2852
- (distribution) => this.evaluateDistribution(agent, distribution)
2853
- );
2854
- const installablePolicy = distributionPolicies.find((policy) => policy.installable);
2855
- const firstBlockedAllowedPolicy = distributionPolicies.find(
2856
- (policy) => policy.allowlist.allowed
2857
- );
2858
- const installable = Boolean(installablePolicy);
2859
- const result = {
2860
- ...agent,
2861
- allowlist,
2862
- installable,
2863
- verificationStatus: installablePolicy?.verificationStatus ?? firstBlockedAllowedPolicy?.verificationStatus ?? "not-applicable"
2864
- };
2865
- const unavailableReason = installable ? void 0 : firstBlockedAllowedPolicy?.unavailableReason ?? (allowlist.allowed ? "distribution-not-installable" : allowlist.reason);
2866
- if (unavailableReason !== void 0) result.unavailableReason = unavailableReason;
2867
- return result;
2868
- });
2869
- }
2870
- evaluateDistribution(agent, distribution) {
2871
- const allowlist = this.allowlist.evaluateDistribution(agent, distribution);
2872
- return evaluateAcpDistributionPolicy(distribution, allowlist);
2873
- }
2874
- };
2875
- function evaluateAcpDistributionPolicy(distribution, allowlist) {
2876
- if (!allowlist.allowed) {
2877
- return {
2878
- allowlist,
2879
- installable: false,
2880
- verificationStatus: "not-applicable",
2881
- unavailableReason: allowlist.reason
2882
- };
2883
- }
2884
- if (distribution.kind !== "binary") {
2885
- return { allowlist, installable: true, verificationStatus: "not-applicable" };
2886
- }
2887
- if (distribution.checksum) {
2888
- return { allowlist, installable: true, verificationStatus: "verified" };
2889
- }
2890
- return {
2891
- allowlist,
2892
- installable: allowlist.unverifiedBinaryAllowed,
2893
- verificationStatus: allowlist.unverifiedBinaryAllowed ? "unverified-allowed" : "unverified-blocked",
2894
- ...allowlist.unverifiedBinaryAllowed ? {} : { unavailableReason: "binary-integrity-metadata-missing" }
2895
- };
2896
- }
2897
- function normalizeRegistry(raw) {
2898
- const record = asRecord3(raw);
2899
- const rawAgents = Array.isArray(record?.agents) ? record.agents : [];
2900
- return rawAgents.flatMap((item) => {
2901
- const agent = normalizeAgent(item);
2902
- return agent ? [agent] : [];
2903
- });
2904
- }
2905
- function normalizeAgent(raw) {
2906
- const record = asRecord3(raw);
2907
- if (!record || typeof record.id !== "string" || typeof record.name !== "string") {
2908
- return void 0;
2909
- }
2910
- const distributions = normalizeDistributions(record.distribution);
2911
- if (distributions.length === 0) {
2912
- return void 0;
2913
- }
2914
- const agent = {
2915
- id: record.id,
2916
- backendId: buildAcpBackendId(record.id),
2917
- name: record.name,
2918
- authors: Array.isArray(record.authors) ? record.authors.filter((author) => typeof author === "string") : [],
2919
- distributions,
2920
- distributionKinds: [...new Set(distributions.map((d) => d.kind))],
2921
- auth: normalizeAuth(record.auth),
2922
- raw
2923
- };
2924
- const version = stringValue(record.version);
2925
- if (version !== void 0) agent.version = version;
2926
- const description = stringValue(record.description);
2927
- if (description !== void 0) agent.description = description;
2928
- const license = stringValue(record.license);
2929
- if (license !== void 0) agent.license = license;
2930
- const repositoryUrl = stringValue(record.repository);
2931
- if (repositoryUrl !== void 0) agent.repositoryUrl = repositoryUrl;
2932
- const websiteUrl = stringValue(record.website);
2933
- if (websiteUrl !== void 0) agent.websiteUrl = websiteUrl;
2934
- const iconUrl = stringValue(record.icon);
2935
- if (iconUrl !== void 0) agent.iconUrl = iconUrl;
2936
- return agent;
2937
- }
2938
- function normalizeDistributions(raw) {
2939
- const record = asRecord3(raw);
2940
- if (!record) {
2941
- return [];
2942
- }
2943
- const distributions = [];
2944
- const npx = normalizePackageDistribution("npx", record.npx);
2945
- if (npx) distributions.push(npx);
2946
- const uvx = normalizePackageDistribution("uvx", record.uvx);
2947
- if (uvx) distributions.push(uvx);
2948
- const binaryRecord = asRecord3(record.binary);
2949
- if (binaryRecord) {
2950
- for (const [platform, value] of Object.entries(binaryRecord)) {
2951
- const binary = normalizeBinaryDistribution(platform, value);
2952
- if (binary) distributions.push(binary);
2953
- }
2954
- }
2955
- return distributions;
2956
- }
2957
- function normalizePackageDistribution(kind, raw) {
2958
- const record = asRecord3(raw);
2959
- if (!record || typeof record.package !== "string") {
2960
- return void 0;
2961
- }
2962
- return {
2963
- kind,
2964
- packageName: record.package,
2965
- args: stringArray(record.args),
2966
- env: stringRecord(record.env)
2967
- };
2968
- }
2969
- function normalizeBinaryDistribution(platform, raw) {
2970
- const record = asRecord3(raw);
2971
- if (!record || typeof record.archive !== "string" || typeof record.cmd !== "string") {
2972
- return void 0;
2973
- }
2974
- const distribution = {
2975
- kind: "binary",
2976
- platform,
2977
- archiveUrl: record.archive,
2978
- command: record.cmd,
2979
- args: stringArray(record.args),
2980
- env: stringRecord(record.env)
2981
- };
2982
- const checksum = stringValue(record.checksum) ?? stringValue(record.sha256);
2983
- if (checksum !== void 0) distribution.checksum = checksum;
2984
- const signatureUrl = stringValue(record.signature) ?? stringValue(record.signatureUrl);
2985
- if (signatureUrl !== void 0) distribution.signatureUrl = signatureUrl;
2986
- return distribution;
2987
- }
2988
- function normalizeAuth(raw) {
2989
- if (!raw) {
2990
- return { required: false, methods: [] };
2991
- }
2992
- const record = asRecord3(raw);
2993
- if (!record) {
2994
- return { required: true, methods: ["unknown"], raw };
2995
- }
2996
- const methods = stringArray(record.methods).map(
2997
- (method) => method === "agent-managed" || method === "terminal" ? method : "unknown"
2998
- );
2999
- return {
3000
- required: record.required === false ? false : true,
3001
- methods: methods.length > 0 ? [...new Set(methods)] : ["unknown"],
3002
- raw
3003
- };
3004
- }
3005
- function asRecord3(value) {
3006
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3007
- }
3008
- function stringValue(value) {
3009
- return typeof value === "string" && value.trim() ? value : void 0;
3010
- }
3011
- function stringArray(value) {
3012
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
3013
- }
3014
- function stringRecord(value) {
3015
- const record = asRecord3(value);
3016
- if (!record) {
3017
- return {};
3018
- }
3019
- return Object.fromEntries(
3020
- Object.entries(record).filter(
3021
- (entry) => typeof entry[1] === "string"
3022
- )
3023
- );
3024
- }
3025
2284
  export {
3026
2285
  ACP_REGISTRY_URL,
3027
2286
  ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX,