clay-server 3.3.2-beta.3 → 3.4.0-beta.1

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.
@@ -21,12 +21,22 @@ function attachFileWatch(ctx) {
21
21
  // one browser tab silently replace another tab's live preview subscription.
22
22
  var fileWatchers = new Map();
23
23
 
24
+ function settleFileWatchReady(entry, ready) {
25
+ if (!entry || !entry.resolveReady) return;
26
+ var resolve = entry.resolveReady;
27
+ entry.resolveReady = null;
28
+ resolve(ready);
29
+ }
30
+
24
31
  function closeFileWatch(key) {
25
32
  var entry = fileWatchers.get(key);
26
33
  if (!entry) return;
27
34
  clearTimeout(entry.debounce);
35
+ if (entry.reconcile) clearImmediate(entry.reconcile);
36
+ if (entry.pollTimer) clearInterval(entry.pollTimer);
28
37
  try { entry.watcher.close(); } catch (e) {}
29
38
  fileWatchers.delete(key);
39
+ settleFileWatchReady(entry, false);
30
40
  }
31
41
 
32
42
  function sendFileChanged(client, message) {
@@ -37,6 +47,36 @@ function attachFileWatch(ctx) {
37
47
  }
38
48
  }
39
49
 
50
+ function readFileSnapshot(absPath) {
51
+ var stat = fs.statSync(absPath);
52
+ var ext = path.extname(absPath).toLowerCase();
53
+ if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
54
+ return { content: fs.readFileSync(absPath, "utf8"), size: stat.size };
55
+ }
56
+
57
+ function publishFileChanged(key, client, relPath, absPath) {
58
+ var latest = fileWatchers.get(key);
59
+ if (!latest || latest.relPath !== relPath) return;
60
+ try {
61
+ var snapshot = readFileSnapshot(absPath);
62
+ if (!snapshot) return;
63
+ if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
64
+ latest.hasSnapshot = true;
65
+ latest.content = snapshot.content;
66
+ latest.size = snapshot.size;
67
+ sendFileChanged(client, {
68
+ type: "fs_file_changed",
69
+ path: relPath,
70
+ content: snapshot.content,
71
+ size: snapshot.size,
72
+ });
73
+ } catch (e) {
74
+ // Atomic saves can briefly remove the destination path between rename
75
+ // events. Keep the parent watcher alive for the next event.
76
+ if (e.code !== "ENOENT") closeFileWatch(key);
77
+ }
78
+ }
79
+
40
80
  function startFileWatch(client, relPath) {
41
81
  // Preserve the old single-argument API for callers outside the websocket
42
82
  // file browser. They share one legacy subscription.
@@ -45,10 +85,10 @@ function attachFileWatch(ctx) {
45
85
  client = null;
46
86
  }
47
87
  var absPath = safePath(cwd, relPath);
48
- if (!absPath) return;
88
+ if (!absPath) return Promise.resolve(false);
49
89
  var key = client || "_legacy";
50
90
  var existing = fileWatchers.get(key);
51
- if (existing && existing.relPath === relPath) return;
91
+ if (existing && existing.relPath === relPath) return existing.ready;
52
92
  closeFileWatch(key);
53
93
 
54
94
  // Watch the parent directory rather than the file inode. Editors and agent
@@ -56,6 +96,8 @@ function attachFileWatch(ctx) {
56
96
  // misses later edits even though the path still exists.
57
97
  var parentPath = path.dirname(absPath);
58
98
  var baseName = path.basename(absPath);
99
+ var initialSnapshot = null;
100
+ try { initialSnapshot = readFileSnapshot(absPath); } catch (e) {}
59
101
  try {
60
102
  var watcher = fs.watch(parentPath, function (eventType, filename) {
61
103
  if (filename && String(filename) !== baseName) return;
@@ -63,25 +105,44 @@ function attachFileWatch(ctx) {
63
105
  if (!active || active.relPath !== relPath) return;
64
106
  clearTimeout(active.debounce);
65
107
  active.debounce = setTimeout(function () {
66
- var latest = fileWatchers.get(key);
67
- if (!latest || latest.relPath !== relPath) return;
68
- try {
69
- var stat = fs.statSync(absPath);
70
- var ext = path.extname(absPath).toLowerCase();
71
- if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
72
- var content = fs.readFileSync(absPath, "utf8");
73
- sendFileChanged(client, { type: "fs_file_changed", path: relPath, content: content, size: stat.size });
74
- } catch (e) {
75
- // Atomic saves can briefly remove the destination path between
76
- // rename events. Keep the parent watcher alive for the next event.
77
- if (e.code !== "ENOENT") closeFileWatch(key);
78
- }
108
+ publishFileChanged(key, client, relPath, absPath);
79
109
  }, 200);
80
110
  });
81
- fileWatchers.set(key, { watcher: watcher, relPath: relPath, debounce: null });
111
+ var resolveReady = null;
112
+ var ready = new Promise(function (resolve) { resolveReady = resolve; });
113
+ var entry = {
114
+ watcher: watcher,
115
+ relPath: relPath,
116
+ debounce: null,
117
+ reconcile: null,
118
+ pollTimer: null,
119
+ ready: ready,
120
+ resolveReady: resolveReady,
121
+ hasSnapshot: !!initialSnapshot,
122
+ content: initialSnapshot ? initialSnapshot.content : null,
123
+ size: initialSnapshot ? initialSnapshot.size : null,
124
+ };
125
+ fileWatchers.set(key, entry);
126
+ // Directory events are the low-latency path, but macOS can coalesce or
127
+ // drop them under load. Periodic content reconciliation is the source of
128
+ // truth and also survives atomic replacements with identical metadata.
129
+ entry.pollTimer = setInterval(function () {
130
+ publishFileChanged(key, client, relPath, absPath);
131
+ }, 1000);
132
+ // fs.watch has no readiness event. Reconcile once on the next event-loop
133
+ // turn so a change between the initial read and native watcher activation
134
+ // cannot leave the browser showing stale content.
135
+ entry.reconcile = setImmediate(function () {
136
+ var active = fileWatchers.get(key);
137
+ if (active) active.reconcile = null;
138
+ publishFileChanged(key, client, relPath, absPath);
139
+ if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
140
+ });
82
141
  watcher.on("error", function () { closeFileWatch(key); });
142
+ return ready;
83
143
  } catch (e) {
84
144
  closeFileWatch(key);
145
+ return Promise.resolve(false);
85
146
  }
86
147
  }
87
148
 
@@ -799,6 +799,10 @@
799
799
  .vendor-toggle-label {
800
800
  pointer-events: none;
801
801
  }
802
+ @media (min-width: 901px) {
803
+ .vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
804
+ .vendor-toggle-btn:not(.active) { padding: 0 8px; }
805
+ }
802
806
  @media (max-width: 900px) {
803
807
  .vendor-toggle-label { display: none; }
804
808
  .vendor-toggle-btn { padding: 0 8px; }
@@ -682,6 +682,7 @@
682
682
  .mate-vendor-label {
683
683
  pointer-events: none;
684
684
  }
685
+ .mate-vendor-btn:not(.active) .mate-vendor-label { display: none; }
685
686
 
686
687
  /* Collapse button inside mate header: white to match header style */
687
688
  .mate-sidebar-header .sidebar-collapse-btn {
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Gemini CLI">
2
+ <defs>
3
+ <linearGradient id="g" x1="8" y1="56" x2="56" y2="8" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#4f7cff"/>
5
+ <stop offset="0.5" stop-color="#9b5cff"/>
6
+ <stop offset="1" stop-color="#ff65b3"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="64" height="64" rx="16" fill="#10131d"/>
10
+ <path d="M32 8c2.3 14.1 9.9 21.7 24 24-14.1 2.3-21.7 9.9-24 24-2.3-14.1-9.9-21.7-24-24 14.1-2.3 21.7-9.9 24-24Z" fill="url(#g)"/>
11
+ </svg>
@@ -527,6 +527,14 @@
527
527
  <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
528
528
  <span class="vendor-toggle-label">Codex</span>
529
529
  </button>
530
+ <button id="vendor-btn-gemini" class="vendor-toggle-btn" data-vendor="gemini">
531
+ <img src="/gemini-avatar.svg" class="vendor-toggle-icon" alt="Gemini">
532
+ <span class="vendor-toggle-label">Gemini CLI</span>
533
+ </button>
534
+ <button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
535
+ <img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
536
+ <span class="vendor-toggle-label">OpenCode</span>
537
+ </button>
530
538
  <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
531
539
  <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
532
540
  <span class="vendor-toggle-label">Kiro CLI</span>
@@ -89,6 +89,8 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
89
89
  var EFFORT_LEVELS_BY_VENDOR = {
90
90
  claude: ["low", "medium", "high", "xhigh", "max"],
91
91
  codex: ["minimal", "low", "medium", "high", "xhigh"],
92
+ gemini: [],
93
+ opencode: [],
92
94
  kiro: ["low", "medium", "high", "xhigh", "max"],
93
95
  };
94
96
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
@@ -441,8 +443,16 @@ export function initPanels() {
441
443
  var vendorToggleWrap = $("vendor-toggle-wrap");
442
444
  var vendorBtnClaude = $("vendor-btn-claude");
443
445
  var vendorBtnCodex = $("vendor-btn-codex");
446
+ var vendorBtnGemini = $("vendor-btn-gemini");
447
+ var vendorBtnOpenCode = $("vendor-btn-opencode");
444
448
  var vendorBtnKiro = $("vendor-btn-kiro");
445
- var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex, kiro: vendorBtnKiro };
449
+ var vendorBtns = {
450
+ claude: vendorBtnClaude,
451
+ codex: vendorBtnCodex,
452
+ gemini: vendorBtnGemini,
453
+ opencode: vendorBtnOpenCode,
454
+ kiro: vendorBtnKiro,
455
+ };
446
456
 
447
457
  function updateVendorToggle() {
448
458
  var installed = store.get('installedVendors') || [];
@@ -471,6 +481,8 @@ export function initPanels() {
471
481
 
472
482
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
473
483
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
484
+ if (vendorBtnGemini) vendorBtnGemini.addEventListener("click", function() { onVendorClick("gemini"); });
485
+ if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
474
486
  if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
475
487
 
476
488
  // --- Reactive UI sync ---
@@ -19,21 +19,27 @@ import { getScheduledMsgEl } from './app-rate-limit.js';
19
19
  export var VENDOR_AVATARS = {
20
20
  claude: "/claude-code-avatar.png",
21
21
  codex: "/codex-avatar.png",
22
+ gemini: "/gemini-avatar.svg",
23
+ opencode: "/opencode-avatar.svg",
22
24
  kiro: "/kiro-avatar.svg",
23
25
  };
24
26
  export var VENDOR_NAMES = {
25
27
  claude: "Claude Code",
26
28
  codex: "Codex",
29
+ gemini: "Gemini CLI",
30
+ opencode: "OpenCode",
27
31
  kiro: "Kiro CLI",
28
32
  };
29
33
  // Display order for every vendor Clay knows about, installed or not. Pickers
30
34
  // render the full list so a missing CLI reads as "not installed yet" rather
31
35
  // than "Clay doesn't support it".
32
- export var VENDOR_ORDER = ["claude", "codex", "kiro"];
36
+ export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
33
37
  // Where to send the user when they pick a vendor whose CLI isn't installed.
34
38
  export var VENDOR_HOMEPAGES = {
35
39
  claude: "https://claude.com/product/claude-code",
36
40
  codex: "https://openai.com/codex/",
41
+ gemini: "https://github.com/google-gemini/gemini-cli",
42
+ opencode: "https://opencode.ai/",
37
43
  kiro: "https://kiro.dev/",
38
44
  };
39
45
  var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
@@ -149,9 +149,15 @@ export function showMateSidebar(mateId, mateData) {
149
149
  if (mateVendorWrap) {
150
150
  var available = store.get('availableVendors') || [];
151
151
  var mateVendor = mateData.vendor || "claude";
152
- var vendorIcons = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
153
- var vendorNames = { claude: "Claude Code", codex: "Codex", kiro: "Kiro CLI" };
154
- var vendorKeys = ["claude", "codex", "kiro"];
152
+ var vendorIcons = {
153
+ claude: "/claude-code-avatar.png",
154
+ codex: "/codex-avatar.png",
155
+ gemini: "/gemini-avatar.svg",
156
+ opencode: "/opencode-avatar.svg",
157
+ kiro: "/kiro-avatar.svg",
158
+ };
159
+ var vendorNames = { claude: "Claude Code", codex: "Codex", gemini: "Gemini CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
160
+ var vendorKeys = ["claude", "codex", "gemini", "opencode", "kiro"];
155
161
  mateVendorWrap.innerHTML = "";
156
162
  for (var vi = 0; vi < vendorKeys.length; vi++) {
157
163
  var vk = vendorKeys[vi];
@@ -457,7 +457,13 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
457
457
  // Tooltip
458
458
  var displayName = mp.displayName || mate.name || "New Mate";
459
459
  var mateVendor = mate.vendor || "claude";
460
- var vendorLabels = { claude: "Claude Code", codex: "OpenAI Codex", kiro: "Kiro CLI" };
460
+ var vendorLabels = {
461
+ claude: "Claude Code",
462
+ codex: "OpenAI Codex",
463
+ gemini: "Gemini CLI",
464
+ opencode: "OpenCode",
465
+ kiro: "Kiro CLI",
466
+ };
461
467
  el.addEventListener("mouseenter", function () {
462
468
  var html = '<div style="font-weight:600">' + escapeHtml(displayName);
463
469
  if (mate.primary) {
@@ -814,7 +814,13 @@ function resolvePermissionIdentity(mateId, vendor) {
814
814
  }
815
815
  }
816
816
  // Project chat: use vendor name and avatar
817
- var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
817
+ var vendorAvatars = {
818
+ claude: "/claude-code-avatar.png",
819
+ codex: "/codex-avatar.png",
820
+ gemini: "/gemini-avatar.svg",
821
+ opencode: "/opencode-avatar.svg",
822
+ kiro: "/kiro-avatar.svg",
823
+ };
818
824
  var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
819
825
  return {
820
826
  name: vendorName,
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="OpenCode">
2
+ <rect width="64" height="64" rx="16" fill="#111814"/>
3
+ <path d="m26 19-13 13 13 13M38 19l13 13-13 13" fill="none" stroke="#78e08f" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
package/lib/sdk-bridge.js CHANGED
@@ -129,7 +129,7 @@ function createSDKBridge(opts) {
129
129
  var _cachedFreshAuthState = null;
130
130
  var _cachedFreshAuthAt = 0;
131
131
 
132
- function getKiroMcpServers(session) {
132
+ function getAcpMcpServers(session) {
133
133
  if (!session) return [];
134
134
  var bridgeArgs = [
135
135
  path.join(__dirname, "yoke", "mcp-bridge-server.js"),
@@ -1525,13 +1525,16 @@ function createSDKBridge(opts) {
1525
1525
  sandboxMode: codexConfig.sandbox,
1526
1526
  webSearchMode: codexConfig.webSearch,
1527
1527
  },
1528
+ ACP: {
1529
+ mcpServers: getAcpMcpServers(session),
1530
+ },
1528
1531
  KIRO: {
1529
1532
  engine: kiroConfig.engine,
1530
1533
  mode: kiroConfig.mode,
1531
1534
  // ACP accepts additional stdio MCP servers per session. The bridge is
1532
1535
  // bound to this Clay session so partner calls cannot leak across
1533
1536
  // concurrent Kiro sessions or users.
1534
- mcpServers: getKiroMcpServers(session),
1537
+ mcpServers: getAcpMcpServers(session),
1535
1538
  },
1536
1539
  },
1537
1540
  };
@@ -1787,6 +1790,19 @@ function createSDKBridge(opts) {
1787
1790
  } catch (e) {}
1788
1791
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1789
1792
 
1793
+ var acpVendorKeys = ["gemini", "opencode"];
1794
+ var acpProfiles = require("./yoke/acp-agent-profiles");
1795
+ for (var acpVendorIndex = 0; acpVendorIndex < acpVendorKeys.length; acpVendorIndex++) {
1796
+ var acpVendor = acpVendorKeys[acpVendorIndex];
1797
+ var acpInfo = yoke.getVendorInfo(acpVendor);
1798
+ if (!linuxUser || acpInfo.osUserIsolation) {
1799
+ var acpProfile = acpProfiles.getAcpAgentProfile(acpVendor);
1800
+ if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(acpVendor);
1801
+ } else {
1802
+ console.log("[sdk-bridge] " + acpInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1803
+ }
1804
+ }
1805
+
1790
1806
  // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1791
1807
  // binary to an OS-isolated user, even if that user also has Kiro installed.
1792
1808
  var kiroInfo = yoke.getVendorInfo("kiro");
@@ -0,0 +1,188 @@
1
+ // ACP Agent Drivers
2
+ // -----------------
3
+ // Vendor-specific facts and optional hooks for agents that implement ACP.
4
+ // Static profiles are sufficient for standard agents; richer runtimes can add
5
+ // hooks without forcing YOKE down to ACP's least-common-denominator feature set.
6
+
7
+ var fs = require("fs");
8
+ var execFile = require("child_process").execFile;
9
+ var execFileSync = require("child_process").execFileSync;
10
+
11
+ function findOnPath(binaryName, overrideName) {
12
+ var overridePath = overrideName && process.env[overrideName];
13
+ if (overridePath && fs.existsSync(overridePath)) return overridePath;
14
+ try {
15
+ var command = process.platform === "win32" ? "where" : "which";
16
+ var out = execFileSync(command, [binaryName], {
17
+ timeout: 3000,
18
+ encoding: "utf8",
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ });
21
+ return out.trim().split(/\r?\n/)[0] || null;
22
+ } catch (e) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function fetchOpenCodeModels(binaryPath, cwd) {
28
+ return new Promise(function(resolve) {
29
+ execFile(binaryPath, ["models"], {
30
+ cwd: cwd || process.cwd(),
31
+ timeout: 20000,
32
+ maxBuffer: 4 * 1024 * 1024,
33
+ }, function(err, stdout) {
34
+ if (err || !stdout) { resolve([]); return; }
35
+ var seen = {};
36
+ var models = [];
37
+ var lines = String(stdout).split(/\r?\n/);
38
+ for (var i = 0; i < lines.length; i++) {
39
+ var value = lines[i].trim();
40
+ if (!value || seen[value]) continue;
41
+ seen[value] = true;
42
+ models.push(value);
43
+ }
44
+ resolve(models);
45
+ });
46
+ });
47
+ }
48
+
49
+ function fetchOpenCodeResolvedConfig(binaryPath, cwd, env) {
50
+ return new Promise(function(resolve, reject) {
51
+ execFile(binaryPath, ["debug", "config"], {
52
+ cwd: cwd || process.cwd(),
53
+ env: Object.assign({}, process.env, env || {}),
54
+ timeout: 30000,
55
+ maxBuffer: 4 * 1024 * 1024,
56
+ }, function(err, stdout) {
57
+ if (err) { reject(err); return; }
58
+ try {
59
+ resolve(JSON.parse(String(stdout || "{}")));
60
+ } catch (e) {
61
+ reject(new Error("OpenCode returned invalid resolved configuration"));
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ function fetchOpenCodeAgentNames(binaryPath, cwd, env) {
68
+ return fetchOpenCodeResolvedConfig(binaryPath, cwd, env).then(function(config) {
69
+ return Object.keys(config.agent || {});
70
+ });
71
+ }
72
+
73
+ function isSafeOpenCodePermission(value) {
74
+ if (value === "ask" || value === "deny") return true;
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
76
+ var keys = Object.keys(value);
77
+ if (!keys.length) return false;
78
+ for (var i = 0; i < keys.length; i++) {
79
+ if (!isSafeOpenCodePermission(value[keys[i]])) return false;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ function validateOpenCodeConfig(config) {
85
+ if (!config || config.permission !== "ask") {
86
+ throw new Error("OpenCode resolved configuration does not preserve global ask permissions");
87
+ }
88
+ var agents = config.agent || {};
89
+ var names = Object.keys(agents);
90
+ for (var i = 0; i < names.length; i++) {
91
+ var agent = agents[names[i]] || {};
92
+ if (agent.permission === undefined) continue;
93
+ if (!isSafeOpenCodePermission(agent.permission)) {
94
+ throw new Error("OpenCode agent has unsafe resolved permissions: " + names[i]);
95
+ }
96
+ }
97
+ }
98
+
99
+ var ACP_AGENT_PROFILES = {
100
+ gemini: {
101
+ vendor: "gemini",
102
+ displayName: "Gemini CLI",
103
+ binaryName: "gemini",
104
+ overrideName: "GEMINI_CLI_PATH",
105
+ args: ["--acp", "--approval-mode=default"],
106
+ defaultModels: ["auto"],
107
+ defaultModel: "auto",
108
+ // Gemini advertises loadSession, but releases through 0.46.0 could load
109
+ // the transcript without restoring the model's conversation memory.
110
+ // Keep resume gated until Clay's live contract test proves it reliable.
111
+ sessionResume: false,
112
+ permissionModeGuaranteed: true,
113
+ },
114
+ opencode: {
115
+ vendor: "opencode",
116
+ displayName: "OpenCode",
117
+ binaryName: "opencode",
118
+ overrideName: "OPENCODE_CLI_PATH",
119
+ args: ["acp"],
120
+ defaultModels: ["auto"],
121
+ defaultModel: "auto",
122
+ sessionResume: null,
123
+ fetchModels: fetchOpenCodeModels,
124
+ permissionModeGuaranteed: true,
125
+ prepare: function(ctx) {
126
+ var injected = ctx.initOpts && ctx.initOpts._openCodeAgentNames;
127
+ if (Array.isArray(injected)) {
128
+ ctx.driverState.agentNames = injected.slice();
129
+ return;
130
+ }
131
+ return fetchOpenCodeAgentNames(ctx.binaryPath, ctx.cwd, ctx.initOpts && ctx.initOpts.env).then(function(names) {
132
+ ctx.driverState.agentNames = names;
133
+ });
134
+ },
135
+ buildProcessOptions: function(ctx, base) {
136
+ var existing = (base.env && base.env.OPENCODE_CONFIG_CONTENT) || process.env.OPENCODE_CONFIG_CONTENT;
137
+ var config = {};
138
+ if (existing) config = JSON.parse(existing);
139
+ var agent = Object.assign({}, config.agent || {});
140
+ var names = ["build", "plan"].concat(ctx.driverState.agentNames || []);
141
+ for (var i = 0; i < names.length; i++) {
142
+ agent[names[i]] = Object.assign({}, agent[names[i]] || {}, { permission: "ask" });
143
+ }
144
+ base.env = Object.assign({}, base.env || {}, {
145
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(Object.assign({}, config, { permission: "ask", agent: agent })),
146
+ });
147
+ return base;
148
+ },
149
+ validateProcessOptions: function(ctx) {
150
+ var injected = ctx.initOpts && ctx.initOpts._openCodeResolvedConfig;
151
+ var resolved = injected
152
+ ? Promise.resolve(injected)
153
+ : fetchOpenCodeResolvedConfig(ctx.binaryPath, ctx.cwd, ctx.processOptions.env);
154
+ return resolved.then(function(config) {
155
+ validateOpenCodeConfig(config);
156
+ });
157
+ },
158
+ ensureSafePermissionMode: function() {
159
+ // OpenCode ACP modes select agents, not approval policy. The process
160
+ // configuration above enforces ask at both global and effective-agent levels.
161
+ return Promise.resolve();
162
+ },
163
+ },
164
+ };
165
+
166
+ function getAcpAgentProfile(vendor) {
167
+ return ACP_AGENT_PROFILES[vendor] || null;
168
+ }
169
+
170
+ function getAcpAgentDriver(vendor) {
171
+ return getAcpAgentProfile(vendor);
172
+ }
173
+
174
+ function findAcpAgentPath(profile) {
175
+ if (!profile) return null;
176
+ return findOnPath(profile.binaryName, profile.overrideName);
177
+ }
178
+
179
+ module.exports = {
180
+ ACP_AGENT_PROFILES: ACP_AGENT_PROFILES,
181
+ getAcpAgentProfile: getAcpAgentProfile,
182
+ getAcpAgentDriver: getAcpAgentDriver,
183
+ findAcpAgentPath: findAcpAgentPath,
184
+ fetchOpenCodeModels: fetchOpenCodeModels,
185
+ fetchOpenCodeAgentNames: fetchOpenCodeAgentNames,
186
+ fetchOpenCodeResolvedConfig: fetchOpenCodeResolvedConfig,
187
+ validateOpenCodeConfig: validateOpenCodeConfig,
188
+ };
@@ -0,0 +1,50 @@
1
+ // ACP Vendor Driver Runtime
2
+ // -------------------------
3
+ // ACP supplies defaults. Trusted vendor drivers may extend or replace them so
4
+ // the shared protocol never becomes the ceiling of the richer YOKE contract.
5
+
6
+ function hasHook(driver, name) {
7
+ return !!(driver && typeof driver[name] === "function");
8
+ }
9
+
10
+ function call(driver, name, context, fallback) {
11
+ if (hasHook(driver, name)) return driver[name](context, fallback);
12
+ return fallback ? fallback() : undefined;
13
+ }
14
+
15
+ function callAsync(driver, name, context, fallback) {
16
+ try {
17
+ return Promise.resolve(call(driver, name, context, fallback));
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ }
22
+
23
+ function mergeCapabilities(driver, context, base) {
24
+ var defaults = Object.assign({}, base);
25
+ if (!hasHook(driver, "extendCapabilities")) return defaults;
26
+ var extended = driver.extendCapabilities(context, Object.assign({}, defaults));
27
+ return Object.assign({}, defaults, extended || {});
28
+ }
29
+
30
+ function buildParams(driver, hookName, context, base) {
31
+ var defaults = Object.assign({}, base);
32
+ if (!hasHook(driver, hookName)) return defaults;
33
+ var result = driver[hookName](context, Object.assign({}, defaults));
34
+ return result === undefined || result === null ? defaults : result;
35
+ }
36
+
37
+ function normalizeEvents(driver, context, fallback) {
38
+ var result = call(driver, "normalizeUpdate", context, fallback);
39
+ if (result === undefined || result === null) return [];
40
+ return Array.isArray(result) ? result : [result];
41
+ }
42
+
43
+ module.exports = {
44
+ hasHook: hasHook,
45
+ call: call,
46
+ callAsync: callAsync,
47
+ mergeCapabilities: mergeCapabilities,
48
+ buildParams: buildParams,
49
+ normalizeEvents: normalizeEvents,
50
+ };