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.
- package/lib/project-file-watch.js +77 -16
- package/lib/public/css/input.css +4 -0
- package/lib/public/css/mates.css +1 -0
- package/lib/public/gemini-avatar.svg +11 -0
- package/lib/public/index.html +8 -0
- package/lib/public/modules/app-panels.js +13 -1
- package/lib/public/modules/app-rendering.js +7 -1
- package/lib/public/modules/mate-sidebar.js +9 -3
- package/lib/public/modules/sidebar-mates.js +7 -1
- package/lib/public/modules/tools.js +7 -1
- package/lib/public/opencode-avatar.svg +4 -0
- package/lib/sdk-bridge.js +18 -2
- package/lib/yoke/acp-agent-profiles.js +188 -0
- package/lib/yoke/acp-driver-runtime.js +50 -0
- package/lib/yoke/acp-event-normalizer.js +179 -0
- package/lib/yoke/acp-process-manager.js +264 -0
- package/lib/yoke/acp-query-handle.js +487 -0
- package/lib/yoke/adapters/acp.js +317 -0
- package/lib/yoke/adapters/gemini.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -4
- package/lib/yoke/adapters/opencode.js +7 -0
- package/lib/yoke/index.js +45 -11
- package/lib/yoke/interface.js +2 -0
- package/lib/yoke/kiro-acp-server.js +30 -276
- package/lib/yoke/vendor-registry.js +22 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/lib/public/css/input.css
CHANGED
|
@@ -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; }
|
package/lib/public/css/mates.css
CHANGED
|
@@ -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>
|
package/lib/public/index.html
CHANGED
|
@@ -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 = {
|
|
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 = {
|
|
153
|
-
|
|
154
|
-
|
|
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 = {
|
|
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 = {
|
|
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
|
|
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:
|
|
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
|
+
};
|