agents-can-communicate 0.8.5 → 0.9.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.
Files changed (133) hide show
  1. package/README.md +11 -7
  2. package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
  3. package/bin/entrypoints/acc-hook.mjs +33 -19
  4. package/bin/entrypoints/hook-output.mjs +14 -5
  5. package/docs/ADAPTER_AUTHORING.md +29 -0
  6. package/docs/CAPABILITIES.md +7 -6
  7. package/docs/CLI.md +5 -0
  8. package/docs/CONFIGURATION.md +15 -10
  9. package/docs/DESIGN_DECISIONS.md +0 -2
  10. package/docs/GETTING_STARTED.md +39 -6
  11. package/docs/HOW_IT_WORKS.md +13 -8
  12. package/docs/SECURITY_MODEL.md +26 -5
  13. package/docs/TROUBLESHOOTING.md +63 -8
  14. package/docs/UPGRADING.md +33 -5
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
  42. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
  43. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
  48. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
  49. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
  50. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
  51. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
  56. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
  58. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
  60. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
  61. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
  63. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
  64. package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
  65. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
  66. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
  67. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
  68. package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
  69. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
  70. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
  73. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  74. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
  75. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
  76. package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
  77. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
  78. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
  79. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
  80. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
  81. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
  82. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
  83. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
  84. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
  85. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
  86. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
  87. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
  88. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
  89. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
  90. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
  91. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
  92. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
  93. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
  95. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
  96. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
  97. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
  98. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  99. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
  103. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
  104. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
  105. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
  106. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
  109. package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
  110. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
  111. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
  112. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  113. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  114. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
  115. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
  116. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  117. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
  118. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
  119. package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
  120. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
  133. package/package.json +1 -5
@@ -1,8 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { constants } from "node:fs";
3
- import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
2
+ import { lstat, mkdir, open, readdir, realpath, rename, rm } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
 
5
+ import { closedTo, isWindowsPlatform, openRegularNoFollow } from "@agents-can-communicate/adapter-sdk";
6
+
6
7
  import { PROTOCOL_CONTRACT, versionOrder } from "./app-server-client.mjs";
7
8
 
8
9
  const KEYS = ["schemaVersion", "endpointId", "socketPath", "threadId", "cwd",
@@ -28,7 +29,7 @@ function valid(record) {
28
29
  && Number.isFinite(Date.parse(record.leaseUntil));
29
30
  }
30
31
 
31
- async function directory(runtimeDir, create = false) {
32
+ async function directory(runtimeDir, create = false, platform = process.platform) {
32
33
  if (!absolute(runtimeDir)) throw invalid();
33
34
  if (create) await mkdir(runtimeDir, { recursive: true, mode: 0o700 });
34
35
  const root = await realpath(runtimeDir);
@@ -37,11 +38,25 @@ async function directory(runtimeDir, create = false) {
37
38
  if (error.code !== "EEXIST") throw error;
38
39
  });
39
40
  const info = await lstat(dir);
40
- if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || (info.mode & 0o022) !== 0) throw invalid();
41
+ if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || !closedTo(info, 0o022, { platform })) {
42
+ throw invalid();
43
+ }
41
44
  return dir;
42
45
  }
43
46
 
44
- export async function socketIsReady(socketPath) {
47
+ /**
48
+ * An owned socket, never itself a link. On Windows Node's lstat refuses the
49
+ * daemon's AF_UNIX socket file with EACCES (measured on windows-latest, Codex
50
+ * 0.159.3), so there the socket counts while its directory lists it; Codex keeps
51
+ * that directory to its user.
52
+ */
53
+ export async function socketIsReady(socketPath, { platform = process.platform } = {}) {
54
+ if (isWindowsPlatform(platform)) {
55
+ if (!absolute(socketPath)) return false;
56
+ const name = path.basename(socketPath).toLowerCase();
57
+ const names = await readdir(path.dirname(socketPath)).catch(() => []);
58
+ return names.some(entry => entry.toLowerCase() === name);
59
+ }
45
60
  if (!absolute(socketPath)) return false;
46
61
  try {
47
62
  const info = await lstat(socketPath);
@@ -53,7 +68,14 @@ export async function socketIsReady(socketPath) {
53
68
  * symlink at the path it reports and the socket itself under /private/tmp
54
69
  * (measured 2026-09-26), so the path is followed once and the socket at its
55
70
  * end is held to socketIsReady: an owned socket, never itself a link. */
56
- export async function readySocketPath(socketPath) {
71
+ export async function readySocketPath(socketPath, { platform = process.platform } = {}) {
72
+ if (isWindowsPlatform(platform)) {
73
+ // The socket file itself refuses to be opened; its directory resolves.
74
+ if (!absolute(socketPath)) return null;
75
+ const directory = await realpath(path.dirname(socketPath)).catch(() => null);
76
+ const resolved = directory === null ? null : path.join(directory, path.basename(socketPath));
77
+ return resolved !== null && await socketIsReady(resolved, { platform }) ? resolved : null;
78
+ }
57
79
  if (!absolute(socketPath)) return null;
58
80
  const resolved = await realpath(socketPath).catch(() => null);
59
81
  return resolved !== null && await socketIsReady(resolved) ? resolved : null;
@@ -61,11 +83,11 @@ export async function readySocketPath(socketPath) {
61
83
 
62
84
  // A fresh, immutable registration for one hook binding. Refresh observes the
63
85
  // same registration again; it cannot overwrite a successor's random endpoint.
64
- export async function writeNativeEndpoint({ runtimeDir, record }) {
65
- if (!valid(record) || !await socketIsReady(record.socketPath)) throw invalid();
86
+ export async function writeNativeEndpoint({ runtimeDir, record, platform = process.platform }) {
87
+ if (!valid(record) || !await socketIsReady(record.socketPath, { platform })) throw invalid();
66
88
  const text = `${JSON.stringify(record)}\n`;
67
89
  if (Buffer.byteLength(text) > MAX_BYTES) throw invalid();
68
- const dir = await directory(runtimeDir, true);
90
+ const dir = await directory(runtimeDir, true, platform);
69
91
  const file = path.join(dir, `${record.endpointId}.json`);
70
92
  const temporary = path.join(dir, `.${record.endpointId}.${randomBytes(8).toString("hex")}.tmp`);
71
93
  let handle;
@@ -82,25 +104,26 @@ export async function writeNativeEndpoint({ runtimeDir, record }) {
82
104
 
83
105
  // Expired records are metadata for revalidation, never proof of reachability.
84
106
  // Refuse links and oversized files before reading; no foreign error text escapes.
85
- export async function readNativeEndpoint({ runtimeDir, endpointId }) {
107
+ export async function readNativeEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
86
108
  if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return null;
87
109
  let handle;
88
110
  try {
89
- const dir = await directory(runtimeDir);
90
- handle = await open(path.join(dir, `${endpointId}.json`),
91
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
111
+ const dir = await directory(runtimeDir, false, platform);
112
+ handle = await openRegularNoFollow(path.join(dir, `${endpointId}.json`), undefined, { platform });
92
113
  const info = await handle.stat();
93
- if (!info.isFile() || !own(info) || info.size > MAX_BYTES || (info.mode & 0o077) !== 0) return null;
114
+ if (!info.isFile() || !own(info) || info.size > MAX_BYTES || !closedTo(info, 0o077, { platform })) {
115
+ return null;
116
+ }
94
117
  const record = JSON.parse(await handle.readFile("utf8"));
95
118
  return valid(record) && record.endpointId === endpointId ? record : null;
96
119
  } catch { return null; }
97
120
  finally { await handle?.close().catch(() => null); }
98
121
  }
99
122
 
100
- export async function removeNativeEndpoint({ runtimeDir, endpointId }) {
123
+ export async function removeNativeEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
101
124
  if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return;
102
125
  try {
103
- const dir = await directory(runtimeDir);
126
+ const dir = await directory(runtimeDir, false, platform);
104
127
  await rm(path.join(dir, `${endpointId}.json`), { force: true });
105
128
  } catch { /* retirement is already recorded; cleanup is best effort */ }
106
129
  }
@@ -75,8 +75,10 @@ export function refusesConnections(socketPath, { timeoutMs = 1_000 } = {}) {
75
75
  });
76
76
  }
77
77
 
78
+ // `connect` returns the byte stream to speak over: a socket by default, or on
79
+ // Windows the stdio of `codex app-server proxy`, which relays to the same server.
78
80
  export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "localhost",
79
- retainNotifications = true }) {
81
+ retainNotifications = true, connect = () => net.createConnection(socketPath) }) {
80
82
  const notifications = [];
81
83
  const pending = new Map();
82
84
  const key = randomBytes(16).toString("base64");
@@ -90,7 +92,7 @@ export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "l
90
92
  const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
91
93
  ready.catch(() => {});
92
94
 
93
- const socket = net.createConnection(socketPath);
95
+ const socket = connect();
94
96
  socket.on("connect", () => socket.write(`GET ${path} HTTP/1.1\r\nHost: ${host}\r\n`
95
97
  + `Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\n`
96
98
  + "Sec-WebSocket-Version: 13\r\n\r\n"));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,7 +43,8 @@ export function createGeminiCliAdapter() {
43
43
  // The binary this client actually installs. Probed for a version to
44
44
  // decide whether the client is on this machine, so it has to be the
45
45
  // real command rather than the adapter id: `0.57.0`.
46
- client: { command: "gemini", certificationName: "gemini-cli", versionArgs: ["--version"] },
46
+ client: { command: "gemini", package: "@google/gemini-cli", certificationName: "gemini-cli",
47
+ versionArgs: ["--version"] },
47
48
  certification,
48
49
  capabilities: {
49
50
  lifecycle: { sessionStart: true, sessionEnd: true },
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
 
7
7
  import { acccreatedFile, bakeSkillCommand, blankJson, ownVersion, removeIfEmpty,
8
8
  removeInstalledTree,
9
- stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
9
+ shellQuote, stampPluginVersion, windowsHookCommand, writeCliShim, writeForeignJson, writeHookShim }
10
10
  from "@agents-can-communicate/adapter-sdk";
11
11
 
12
12
  const bundle = fileURLToPath(new URL("../extension", import.meta.url));
@@ -46,11 +46,16 @@ const writeJson = (file, value) => writeForeignJson(file, value,
46
46
  const isOurs = hook => typeof hook?.name === "string" && hook.name.startsWith(OWNER_PREFIX);
47
47
 
48
48
  // Replace the bundle's placeholder command with the shim that was just written.
49
- const withShim = (wiring, shim) => ({ hooks: Object.fromEntries(
49
+ // On Windows this client runs hooks through PowerShell, which has no `sh`: the
50
+ // pinned node runs the Node shim there.
51
+ const withShim = (wiring, shim, windowsNode = null) => ({ hooks: Object.fromEntries(
50
52
  Object.entries(wiring.hooks).map(([event, entries]) => [event, entries.map(entry => ({
51
53
  ...entry,
52
- hooks: entry.hooks.map(hook => ({ ...hook,
53
- command: `sh "${shim}" ${hook.command.split(" ").pop()}` })),
54
+ hooks: entry.hooks.map(hook => {
55
+ const kind = hook.command.split(" ").pop();
56
+ return { ...hook, command: windowsNode === null ? `sh ${shellQuote(shim)} ${kind}`
57
+ : windowsHookCommand("powershell", { node: windowsNode, shim, args: [kind] }) };
58
+ }),
54
59
  }))])) });
55
60
 
56
61
  /**
@@ -62,7 +67,8 @@ const withShim = (wiring, shim) => ({ hooks: Object.fromEntries(
62
67
  * No environment variable is copied or persisted. The extension declares what
63
68
  * it needs; secrets stay where the user put them.
64
69
  */
65
- export async function installGeminiExtension({ home, runner, cli, node }) {
70
+ export async function installGeminiExtension({ home, runner, cli, node,
71
+ hostPlatform = process.platform }) {
66
72
  // Read before writing: a settings file that will not parse must not be found
67
73
  // out after the extension tree is already on disk.
68
74
  const found = await readJson(settingsPath(home), null);
@@ -82,15 +88,15 @@ export async function installGeminiExtension({ home, runner, cli, node }) {
82
88
  await rm(path.join(target, "hooks", "hooks.json"), { force: true });
83
89
  // The skill ships with a placeholder where the command belongs: `acc` is
84
90
  // not on PATH everywhere, and an agent that cannot run it improvises.
85
- const cliShim = await writeCliShim({ dir: target, cli, node });
86
- await bakeSkillCommand({ root: target, cliShim });
91
+ const cliShim = await writeCliShim({ dir: target, cli, node, platform: hostPlatform });
92
+ await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
87
93
  // This client offers no plugin-root variable in a hook command, so the shim's
88
94
  // absolute path is written in at install time.
89
95
  const shim = await writeHookShim({ dir: path.join(target, "hooks"),
90
- adapterId: "gemini_cli", runner, node });
96
+ adapterId: "gemini_cli", runner, node, platform: hostPlatform });
91
97
 
92
98
  const ours = withShim(await readJson(path.join(bundle, "hooks", "hooks.json"),
93
- { hooks: {} }), shim);
99
+ { hooks: {} }), shim, hostPlatform === "win32" ? node ?? process.execPath : null);
94
100
  const file = settingsPath(home);
95
101
  const merged = { ...existing, hooks: { ...(existing.hooks ?? {}) } };
96
102
  // Recorded now: afterwards a settings file holding `{}` looks the same
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
5
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
6
- import { bakeSkillCommand, removeInstalledTree, writeCliShim, writeHookShim }
6
+ import { bakeSkillCommand, removeInstalledTree, shellQuote, windowsHookCommand, writeCliShim,
7
+ writeHookShim }
7
8
  from "@agents-can-communicate/adapter-sdk";
8
9
 
9
10
  const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
@@ -15,7 +16,9 @@ export const grokHomeOf = context =>
15
16
  context.grokHome ?? path.join(context.home, ".grok");
16
17
 
17
18
  export const hooksFile = home => path.join(home, "hooks", HOOKS_NAME);
18
- export const shimPath = home => path.join(home, "hooks", SHIM_NAME);
19
+ // Windows gets the Node shim: no Windows client can be relied on to have `sh`.
20
+ export const shimPath = (home, platform = process.platform) => path.join(home, "hooks",
21
+ platform === "win32" ? SHIM_NAME.replace(/\.sh$/, ".mjs") : SHIM_NAME);
19
22
  export const skillPath = home => path.join(home, "skills", "acc");
20
23
 
21
24
  async function readJson(file, fallback) {
@@ -46,37 +49,44 @@ const writeJson = async (file, value) => {
46
49
  // for plugins; we install into ~/.grok/hooks, which is always trusted and does
47
50
  // not need [plugins].enabled. The shim's absolute path is written in at install
48
51
  // time, the same lesson Codex taught: a relative hook command fails silently.
49
- const withShim = (wiring, shim) => ({
52
+ // On Windows this client runs hooks through the shell it detects - pwsh, Git
53
+ // Bash or cmd - so the command is written in the one form all three read.
54
+ const withShim = (wiring, shim, windowsNode = null) => ({
50
55
  description: wiring.description,
51
56
  hooks: Object.fromEntries(Object.entries(wiring.hooks).map(([event, entries]) =>
52
57
  [event, entries.map(entry => ({
53
58
  ...entry,
54
- hooks: entry.hooks.map(hook => ({ ...hook,
55
- command: `sh "${shim}" ${hook.command.split(" ").pop()}` })),
59
+ hooks: entry.hooks.map(hook => {
60
+ const kind = hook.command.split(" ").pop();
61
+ return { ...hook, command: windowsNode === null ? `sh ${shellQuote(shim)} ${kind}`
62
+ : windowsHookCommand("portable", { node: windowsNode, shim, args: [kind] }) };
63
+ }),
56
64
  }))])),
57
65
  });
58
66
 
59
- export async function installGrokHooks({ grokHome, home, runner, cli, node }) {
67
+ export async function installGrokHooks({ grokHome, home, runner, cli, node,
68
+ hostPlatform = process.platform }) {
60
69
  const root = grokHome ?? grokHomeOf({ home, grokHome });
61
70
  const template = await readJson(path.join(bundle, "hooks", "hooks.json"), { hooks: {} });
62
71
  const shim = await writeHookShim({ dir: path.join(root, "hooks"), adapterId: "grok",
63
- runner, node, name: SHIM_NAME });
72
+ runner, node, name: SHIM_NAME, platform: hostPlatform });
64
73
 
65
74
  const skills = skillPath(root);
66
75
  await rm(skills, { recursive: true, force: true });
67
76
  await mkdir(path.dirname(skills), { recursive: true });
68
77
  await cp(path.join(bundle, "skills", "acc"), skills, { recursive: true });
69
- const cliShim = await writeCliShim({ dir: skills, cli, node });
70
- await bakeSkillCommand({ root: skills, cliShim });
78
+ const cliShim = await writeCliShim({ dir: skills, cli, node, platform: hostPlatform });
79
+ await bakeSkillCommand({ root: skills, cliShim, platform: hostPlatform });
71
80
 
72
- await writeJson(hooksFile(root), withShim(template, shim));
81
+ await writeJson(hooksFile(root), withShim(template, shim,
82
+ hostPlatform === "win32" ? node ?? process.execPath : null));
73
83
  return { ok: true, changes: [hooksFile(root), shim, skills], diagnostics: [] };
74
84
  }
75
85
 
76
- export async function uninstallGrokHooks({ grokHome, home, keep = [] }) {
86
+ export async function uninstallGrokHooks({ grokHome, home, keep = [], hostPlatform }) {
77
87
  const root = grokHome ?? grokHomeOf({ home, grokHome });
78
88
  const changes = [];
79
- for (const target of [hooksFile(root), shimPath(root), skillPath(root)]) {
89
+ for (const target of [hooksFile(root), shimPath(root, hostPlatform), skillPath(root)]) {
80
90
  if (await removeInstalledTree(target, keep)) changes.push(target);
81
91
  }
82
92
  return { ok: true, changes, diagnostics: [] };
@@ -87,7 +97,8 @@ export async function detectGrok({ grokHome, home }) {
87
97
  const wired = await readJson(hooksFile(root), null);
88
98
  const installed = typeof wired?.hooks?.SessionStart?.[0]?.hooks?.[0]?.command
89
99
  === "string"
90
- && wired.hooks.SessionStart[0].hooks[0].command.includes(SHIM_NAME);
100
+ // The shell shim on POSIX, the Node shim on Windows.
101
+ && /acc-hook\.(?:sh|mjs)\b/.test(wired.hooks.SessionStart[0].hooks[0].command);
91
102
  return { ok: true, changes: [],
92
103
  diagnostics: [installed ? "acc hooks registered" : "acc hooks not registered"] };
93
104
  }
@@ -96,7 +107,7 @@ export function planGrokInstall(context) {
96
107
  const root = grokHomeOf(context);
97
108
  return [
98
109
  { path: hooksFile(root), kind: "tree" },
99
- { path: shimPath(root), kind: "tree" },
110
+ { path: shimPath(root, context.hostPlatform), kind: "tree" },
100
111
  { path: skillPath(root), kind: "tree" },
101
112
  ];
102
113
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,7 +32,8 @@ export function createKimiAdapter() {
32
32
  // The binary this client actually installs. Probed for a version to
33
33
  // decide whether the client is on this machine, so it has to be the
34
34
  // real command rather than the adapter id: `0.36.1`.
35
- client: { command: "kimi", certificationName: "kimi", versionArgs: ["--version"] },
35
+ client: { command: "kimi", package: "@moonshot-ai/kimi-code", certificationName: "kimi",
36
+ versionArgs: ["--version"] },
36
37
  certification,
37
38
  capabilities: {
38
39
  lifecycle: { sessionStart: true, heartbeat: true },
@@ -4,9 +4,9 @@ import path from "node:path";
4
4
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
- import { assertRunner, bakeSkillCommand, blankText, defaultRunner, removeIfEmpty,
8
- ownVersion, stampPluginVersion,
9
- removeInstalledTree, runnerExists, writeCliShim, writeForeignJson }
7
+ import { assertRunner, bakeSkillCommand, blankText, defaultRunner, ownVersion, removeIfEmpty,
8
+ removeInstalledTree, runnerExists, shellQuote, stampPluginVersion, windowsHookCommand,
9
+ writeCliShim, writeForeignJson }
10
10
  from "@agents-can-communicate/adapter-sdk";
11
11
 
12
12
  const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
@@ -99,13 +99,17 @@ export function stripBlock(source) {
99
99
  return kept.join("\n");
100
100
  }
101
101
 
102
- export function renderBlock(runner, node = process.execPath) {
102
+ export function renderBlock(runner, node = process.execPath, platform = process.platform) {
103
103
  const entries = WIRING.map(({ event, kind, matcher }) => {
104
104
  // Both paths are shell-quoted inside the TOML string: a hook runs through a
105
105
  // shell, and a space in either path would otherwise split the command.
106
106
  // The interpreter is named outright rather than left to PATH, which a hook's
107
- // environment does not reliably carry.
108
- const command = `${tomlString(node)} ${tomlString(runner)} kimi ${kind}`;
107
+ // environment does not reliably carry. On Windows that shell is cmd.exe, and
108
+ // a TOML-escaped path inside the command would reach it with every
109
+ // backslash doubled.
110
+ const command = platform === "win32"
111
+ ? windowsHookCommand("cmd", { node, shim: runner, args: ["kimi", kind] })
112
+ : `${shellQuote(node)} ${shellQuote(runner)} kimi ${kind}`;
109
113
  const lines = ["[[hooks]]", `event = ${tomlString(event)}`,
110
114
  `command = ${tomlString(command)}`];
111
115
  if (matcher !== undefined) lines.push(`matcher = ${tomlString(matcher)}`);
@@ -121,7 +125,8 @@ const registerPlugin = (registry, root) => {
121
125
  plugins: [...plugins, { id: PLUGIN_NAME, root, source: "local", enabled: true }] };
122
126
  };
123
127
 
124
- export async function installKimiPlugin({ home, runner = defaultRunner(), node, cli }) {
128
+ export async function installKimiPlugin({ home, runner = defaultRunner(), node, cli,
129
+ hostPlatform = process.platform }) {
125
130
  // A hook whose command does not exist fails silently, on every event, for as
126
131
  // long as it stays installed: the client reports nothing and ACC simply never
127
132
  // sees a session. Writing that entry and hoping is worse than refusing.
@@ -139,8 +144,8 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
139
144
  version: await ownVersion(import.meta.url), io: { readFile, writeFile } });
140
145
  // The skill ships with a placeholder where the command belongs: `acc` is
141
146
  // not on PATH everywhere, and an agent that cannot run it improvises.
142
- const cliShim = await writeCliShim({ dir: target, cli, node });
143
- await bakeSkillCommand({ root: target, cliShim });
147
+ const cliShim = await writeCliShim({ dir: target, cli, node, platform: hostPlatform });
148
+ await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
144
149
 
145
150
  const file = configPath(home);
146
151
  const existing = await readText(file, "");
@@ -149,7 +154,7 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
149
154
  // user's last section cannot swallow our entries.
150
155
  const separator = withoutOurs === "" || withoutOurs.endsWith("\n") ? "" : "\n";
151
156
  await mkdir(path.dirname(file), { recursive: true });
152
- await writeFile(file, `${withoutOurs}${separator}${renderBlock(runner, node)}\n`);
157
+ await writeFile(file, `${withoutOurs}${separator}${renderBlock(runner, node, hostPlatform)}\n`);
153
158
 
154
159
  const registry = registryPath(home);
155
160
  await mkdir(path.dirname(registry), { recursive: true });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,6 +81,41 @@ export function assertCapabilities(declared = {}, implementation = {}, certifica
81
81
  return freezeCapabilities(resolved);
82
82
  }
83
83
 
84
+ /**
85
+ * The other products whose hooks one adapter serves. Antigravity 2.0 runs the
86
+ * same `~/.gemini/config/hooks.json` as Antigravity CLI, on its own version
87
+ * line, so its evidence is recorded under its own certification name. A
88
+ * variant names a product, not a launch mode: one adapter's hooks, one more
89
+ * version line.
90
+ */
91
+ function validateClientVariants(variants, primary) {
92
+ if (variants === undefined) return [];
93
+ if (!Array.isArray(variants)) usage("client.variants must be an array", { variants });
94
+ const seen = new Set([primary]);
95
+ return variants.map((variant, index) => {
96
+ if (variant === null || typeof variant !== "object" || Array.isArray(variant)) {
97
+ usage(`client.variants entry ${index} must be an object`);
98
+ }
99
+ for (const key of Object.keys(variant)) {
100
+ if (!["certificationName", "displayName"].includes(key)) {
101
+ usage(`client.variants entry ${index} has unknown field ${key}`, { key });
102
+ }
103
+ }
104
+ const { certificationName, displayName } = variant;
105
+ if (typeof certificationName !== "string" || certificationName.trim() === "") {
106
+ usage(`client.variants entry ${index} certificationName must be a non-empty string`);
107
+ }
108
+ if (typeof displayName !== "string" || displayName.trim() === "") {
109
+ usage(`client.variants entry ${index} displayName must be a non-empty string`);
110
+ }
111
+ if (seen.has(certificationName)) {
112
+ usage(`client.variants entry ${index} repeats ${certificationName}`, { certificationName });
113
+ }
114
+ seen.add(certificationName);
115
+ return { certificationName, displayName };
116
+ });
117
+ }
118
+
84
119
  export function defineAdapter(manifest) {
85
120
  if (typeof manifest?.id !== "string") usage("an adapter must declare an id");
86
121
  assertPortableId(manifest.id, "adapter id");
@@ -104,15 +139,25 @@ export function defineAdapter(manifest) {
104
139
  const certification = validateCertification(manifest.certification);
105
140
  const known = new Set(Object.entries(CAPABILITY_SHAPE)
106
141
  .flatMap(([group, names]) => names.map(name => `${group}.${name}`)));
142
+ const primary = manifest.client.certificationName ?? manifest.client.command;
143
+ const clients = new Set([primary, ...validateClientVariants(manifest.client.variants, primary)
144
+ .map(variant => variant.certificationName)]);
107
145
  for (const item of certification.evidence) {
108
146
  if (!known.has(item.capability)) {
109
147
  usage(`unknown certified capability: ${item.capability}`,
110
148
  { capability: item.capability });
111
149
  }
112
- const client = manifest.client.certificationName ?? manifest.client.command;
113
- if (item.client !== client) {
114
- usage(`certification evidence client ${item.client} does not match ${client}`,
115
- { evidenceClient: item.client, client });
150
+ if (!clients.has(item.client)) {
151
+ usage(`certification evidence client ${item.client} does not match ${primary}`,
152
+ { evidenceClient: item.client, client: primary });
153
+ }
154
+ }
155
+ // How the hook runner learns which product and which version a session runs
156
+ // in, from the process that runs it rather than the binary on PATH. Both are
157
+ // optional; an adapter without them is matched by client.command.
158
+ for (const method of ["identifyClientProcess", "clientVersionOf"]) {
159
+ if (manifest[method] !== undefined && typeof manifest[method] !== "function") {
160
+ usage(`optional client method ${method} must be a function`, { id: manifest.id, method });
116
161
  }
117
162
  }
118
163
  // A floor said "judge later versions by this capture". Evidence now does that
@@ -139,7 +184,7 @@ export function defineAdapter(manifest) {
139
184
  if (manifest.nativeDelivery !== undefined) {
140
185
  const client = manifest.client.certificationName ?? manifest.client.command;
141
186
  native.nativeDelivery = validateNativeDeliveryContract(manifest.nativeDelivery,
142
- { certification, client });
187
+ { certification, client, clients: [...clients] });
143
188
  for (const method of NATIVE_METHODS) {
144
189
  if (typeof manifest[method] !== "function") {
145
190
  usage(`a native delivery contract requires ${method}()`, { id: manifest.id, method });
@@ -178,12 +178,21 @@ function falseCapabilities() {
178
178
  * first capture, whose version is returned) or `recorded-failure` (a capture at
179
179
  * the returned version recorded the loss).
180
180
  */
181
- export function capabilityEvidence(adapter, { clientVersion, platform } = {}, capability) {
181
+ export function capabilityEvidence(adapter, { clientVersion, platform, clientName } = {},
182
+ capability) {
182
183
  const [group, name] = capability.split(".");
183
184
  if (adapter.capabilities?.[group]?.[name] !== true) {
184
185
  return { granted: false, reason: "undeclared", version: null };
185
186
  }
186
- const client = adapter.client?.certificationName ?? adapter.client?.command;
187
+ // `clientName` names the product the session runs in when it is one of the
188
+ // adapter's variants. Another product's evidence never stands in for it: its
189
+ // versions count on a different line. A name the adapter does not declare
190
+ // has no evidence at all.
191
+ const primary = adapter.client?.certificationName ?? adapter.client?.command;
192
+ const declared = [primary, ...(adapter.client?.variants ?? [])
193
+ .map(variant => variant?.certificationName)];
194
+ const client = typeof clientName === "string" ? clientName : primary;
195
+ if (!declared.includes(client)) return { granted: false, reason: "unobserved", version: null };
187
196
  const named = (adapter.certification?.evidence ?? [])
188
197
  .filter(item => item.client === client && item.capability === capability);
189
198
  if (named.length === 0) return { granted: false, reason: "unobserved", version: null };