clay-server 3.3.2-beta.3 → 3.4.0-beta.2
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/project-shell-command.js +160 -0
- package/lib/project-user-message.js +10 -0
- package/lib/project.js +13 -0
- package/lib/public/app.js +10 -0
- package/lib/public/css/input.css +67 -0
- package/lib/public/css/mates.css +1 -0
- package/lib/public/gemini-avatar.svg +11 -0
- package/lib/public/index.html +13 -0
- package/lib/public/modules/app-messages.js +16 -1
- package/lib/public/modules/app-panels.js +13 -1
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rendering.js +7 -1
- package/lib/public/modules/input.js +58 -28
- package/lib/public/modules/mate-sidebar.js +9 -3
- package/lib/public/modules/shell-command.js +148 -0
- 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/sdk-message-processor.js +10 -1
- package/lib/ws-schema.js +3 -0
- 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
|
@@ -7,6 +7,7 @@ import { mateAvatarUrl } from './avatar.js';
|
|
|
7
7
|
import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
|
|
8
8
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
9
9
|
import { showToast } from './utils.js';
|
|
10
|
+
import { isShellCommandMode, submitShellCommand } from './shell-command.js';
|
|
10
11
|
|
|
11
12
|
var ctx;
|
|
12
13
|
|
|
@@ -75,6 +76,37 @@ export var builtinCommands = [
|
|
|
75
76
|
{ name: "status", desc: "Process status and resource usage" },
|
|
76
77
|
];
|
|
77
78
|
|
|
79
|
+
function commitVendorForTurn() {
|
|
80
|
+
var committedVendor = store.get('currentVendor');
|
|
81
|
+
var vendorToggle = document.getElementById("vendor-toggle-wrap");
|
|
82
|
+
var activeIndicator = document.getElementById("active-vendor-indicator");
|
|
83
|
+
var activeIcon = document.getElementById("active-vendor-icon");
|
|
84
|
+
if (committedVendor) {
|
|
85
|
+
if (vendorToggle) {
|
|
86
|
+
vendorToggle.classList.add("hidden");
|
|
87
|
+
vendorToggle.classList.remove("locked");
|
|
88
|
+
}
|
|
89
|
+
if (activeIndicator && activeIcon) {
|
|
90
|
+
activeIcon.src = VENDOR_AVATARS[committedVendor] || VENDOR_AVATARS.claude;
|
|
91
|
+
activeIcon.alt = VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude;
|
|
92
|
+
activeIndicator.title = (VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude) + " session";
|
|
93
|
+
activeIndicator.classList.remove("hidden");
|
|
94
|
+
}
|
|
95
|
+
} else if (vendorToggle) {
|
|
96
|
+
vendorToggle.classList.remove("hidden");
|
|
97
|
+
vendorToggle.classList.add("locked");
|
|
98
|
+
}
|
|
99
|
+
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function showPreThinkingForTurn() {
|
|
103
|
+
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
104
|
+
ctx.showMatePreThinking();
|
|
105
|
+
} else if (ctx.showClaudePreThinking) {
|
|
106
|
+
ctx.showClaudePreThinking();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
// --- Send ---
|
|
79
111
|
export function sendMessage() {
|
|
80
112
|
// Debate ended mode intercept: route to resume handler
|
|
@@ -92,6 +124,14 @@ export function sendMessage() {
|
|
|
92
124
|
ctx.handleDebateFloorSend();
|
|
93
125
|
return;
|
|
94
126
|
}
|
|
127
|
+
if (isShellCommandMode()) {
|
|
128
|
+
var shellText = ctx.inputEl.value.trim();
|
|
129
|
+
if (!shellText || !submitShellCommand(shellText)) return;
|
|
130
|
+
ctx.inputEl.value = "";
|
|
131
|
+
sendInputSync();
|
|
132
|
+
autoResize();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
95
135
|
// DM mode intercept: if in DM mode, route to DM handler instead
|
|
96
136
|
if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
|
|
97
137
|
ctx.handleDmSend();
|
|
@@ -304,36 +344,10 @@ export function sendMessage() {
|
|
|
304
344
|
// Bumping sessionHasHistory drives in-session lock states (e.g. the
|
|
305
345
|
// Codex model picker that becomes informational once the thread is
|
|
306
346
|
// bound to a model).
|
|
307
|
-
|
|
308
|
-
var _vtw2 = document.getElementById("vendor-toggle-wrap");
|
|
309
|
-
var _avi = document.getElementById("active-vendor-indicator");
|
|
310
|
-
var _avIcon = document.getElementById("active-vendor-icon");
|
|
311
|
-
if (_committedVendor) {
|
|
312
|
-
if (_vtw2) {
|
|
313
|
-
_vtw2.classList.add("hidden");
|
|
314
|
-
_vtw2.classList.remove("locked");
|
|
315
|
-
}
|
|
316
|
-
if (_avi && _avIcon) {
|
|
317
|
-
_avIcon.src = VENDOR_AVATARS[_committedVendor] || VENDOR_AVATARS.claude;
|
|
318
|
-
_avIcon.alt = VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude;
|
|
319
|
-
_avi.title = (VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude) + " session";
|
|
320
|
-
_avi.classList.remove("hidden");
|
|
321
|
-
}
|
|
322
|
-
} else if (_vtw2) {
|
|
323
|
-
// No committed vendor (defensive — shouldn't happen because the
|
|
324
|
-
// input is otherwise gated on a vendor pick). Fall back to the
|
|
325
|
-
// locked toggle so the running vendor is still visible.
|
|
326
|
-
_vtw2.classList.remove("hidden");
|
|
327
|
-
_vtw2.classList.add("locked");
|
|
328
|
-
}
|
|
329
|
-
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
347
|
+
commitVendorForTurn();
|
|
330
348
|
|
|
331
349
|
// Show pre-thinking dots before server responds
|
|
332
|
-
|
|
333
|
-
ctx.showMatePreThinking();
|
|
334
|
-
} else if (ctx.showClaudePreThinking) {
|
|
335
|
-
ctx.showClaudePreThinking();
|
|
336
|
-
}
|
|
350
|
+
showPreThinkingForTurn();
|
|
337
351
|
|
|
338
352
|
ctx.inputEl.value = "";
|
|
339
353
|
sendInputSync();
|
|
@@ -354,6 +368,22 @@ export function sendTextMessage(text) {
|
|
|
354
368
|
return true;
|
|
355
369
|
}
|
|
356
370
|
|
|
371
|
+
export function sendShellResultToAgent(msg) {
|
|
372
|
+
if (!ctx || !ctx.connected || !msg || msg.error || !msg.command) return false;
|
|
373
|
+
if (String(msg.sessionId) !== String(store.get("activeSessionId"))) return false;
|
|
374
|
+
var payload = {
|
|
375
|
+
type: "message",
|
|
376
|
+
text: "$ " + msg.command,
|
|
377
|
+
shellCommandResponse: true,
|
|
378
|
+
};
|
|
379
|
+
var selectedVendor = store.get("currentVendor") || null;
|
|
380
|
+
if (selectedVendor) payload.vendor = selectedVendor;
|
|
381
|
+
ctx.ws.send(JSON.stringify(payload));
|
|
382
|
+
commitVendorForTurn();
|
|
383
|
+
showPreThinkingForTurn();
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
357
387
|
export function autoResize() {
|
|
358
388
|
ctx.inputEl.style.height = "auto";
|
|
359
389
|
ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
|
|
@@ -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];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
+
import { showToast } from './utils.js';
|
|
5
|
+
|
|
6
|
+
var defaultPlaceholder = "";
|
|
7
|
+
|
|
8
|
+
function getInput() {
|
|
9
|
+
return document.getElementById("input");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendToMessages(element) {
|
|
13
|
+
var messages = document.getElementById("messages");
|
|
14
|
+
if (!messages) return;
|
|
15
|
+
messages.appendChild(element);
|
|
16
|
+
requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setMode(active) {
|
|
20
|
+
var input = getInput();
|
|
21
|
+
var button = document.getElementById("shell-command-btn");
|
|
22
|
+
var row = document.getElementById("input-row");
|
|
23
|
+
store.set({ shellCommandMode: active });
|
|
24
|
+
if (button) {
|
|
25
|
+
button.classList.toggle("active", active);
|
|
26
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
27
|
+
}
|
|
28
|
+
if (row) row.classList.toggle("shell-command-mode", active);
|
|
29
|
+
if (input) {
|
|
30
|
+
if (!defaultPlaceholder) defaultPlaceholder = input.placeholder;
|
|
31
|
+
input.placeholder = active ? "Run a shell command in this project…" : defaultPlaceholder;
|
|
32
|
+
input.focus();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isShellCommandMode() {
|
|
37
|
+
return !!store.get("shellCommandMode");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toggleShellCommandMode() {
|
|
41
|
+
if (store.get("shellCommandRunning")) return;
|
|
42
|
+
var target = store.get("dmTargetUser");
|
|
43
|
+
if (store.get("dmMode") && target && !target.isMate) {
|
|
44
|
+
showToast("Shell commands are available in agent sessions, not user DMs.", "error");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setMode(!isShellCommandMode());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderPendingCommand(requestId, command) {
|
|
51
|
+
var card = document.createElement("div");
|
|
52
|
+
card.className = "shell-command-card running";
|
|
53
|
+
card.dataset.requestId = requestId;
|
|
54
|
+
card.innerHTML =
|
|
55
|
+
'<div class="shell-command-header">' +
|
|
56
|
+
'<span class="shell-command-icon">' + iconHtml("square-terminal") + '</span>' +
|
|
57
|
+
'<code></code><span class="shell-command-status">Running…</span>' +
|
|
58
|
+
'</div>' +
|
|
59
|
+
'<pre class="shell-command-output">Waiting for output…</pre>';
|
|
60
|
+
card.querySelector("code").textContent = "$ " + command;
|
|
61
|
+
appendToMessages(card);
|
|
62
|
+
refreshIcons();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function submitShellCommand(command) {
|
|
66
|
+
command = String(command || "").trim();
|
|
67
|
+
if (!command || store.get("shellCommandRunning")) return false;
|
|
68
|
+
var ws = getWs();
|
|
69
|
+
if (!ws || ws.readyState !== 1) {
|
|
70
|
+
showToast("Not connected — command not run.", "error");
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var requestId = "shell_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9);
|
|
75
|
+
store.set({ shellCommandRunning: true, pendingShellCommandId: requestId });
|
|
76
|
+
var input = getInput();
|
|
77
|
+
if (input) {
|
|
78
|
+
input.disabled = true;
|
|
79
|
+
input.placeholder = "Running command…";
|
|
80
|
+
}
|
|
81
|
+
renderPendingCommand(requestId, command);
|
|
82
|
+
ws.send(JSON.stringify({ type: "shell_command", requestId: requestId, command: command }));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function handleShellCommandResult(msg) {
|
|
87
|
+
var cards = document.querySelectorAll(".shell-command-card[data-request-id]");
|
|
88
|
+
var card = null;
|
|
89
|
+
for (var i = 0; i < cards.length; i++) {
|
|
90
|
+
if (cards[i].dataset.requestId === (msg.requestId || "")) {
|
|
91
|
+
card = cards[i];
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (card) {
|
|
96
|
+
var status = card.querySelector(".shell-command-status");
|
|
97
|
+
var output = card.querySelector(".shell-command-output");
|
|
98
|
+
card.classList.remove("running");
|
|
99
|
+
if (msg.error) {
|
|
100
|
+
card.classList.add("error");
|
|
101
|
+
if (status) status.textContent = "Failed";
|
|
102
|
+
if (output) output.textContent = msg.error;
|
|
103
|
+
} else {
|
|
104
|
+
card.classList.toggle("error", msg.exitCode !== 0);
|
|
105
|
+
if (status) status.textContent = msg.timedOut ? "Timed out" : "Exit " + (msg.exitCode == null ? "—" : msg.exitCode);
|
|
106
|
+
if (output) output.textContent = msg.output || "(no output)";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
store.set({ shellCommandRunning: false, pendingShellCommandId: null });
|
|
111
|
+
var input = getInput();
|
|
112
|
+
if (input) input.disabled = false;
|
|
113
|
+
if (msg.error) {
|
|
114
|
+
setMode(true);
|
|
115
|
+
} else {
|
|
116
|
+
setMode(false);
|
|
117
|
+
}
|
|
118
|
+
var messages = document.getElementById("messages");
|
|
119
|
+
if (messages) requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function initShellCommand() {
|
|
123
|
+
var button = document.getElementById("shell-command-btn");
|
|
124
|
+
var mobileButton = document.getElementById("input-more-shell");
|
|
125
|
+
if (button) button.addEventListener("click", toggleShellCommandMode);
|
|
126
|
+
if (mobileButton) {
|
|
127
|
+
mobileButton.addEventListener("click", function () {
|
|
128
|
+
var sheet = document.getElementById("input-more-sheet");
|
|
129
|
+
if (sheet) {
|
|
130
|
+
sheet.classList.remove("open");
|
|
131
|
+
setTimeout(function () { sheet.classList.add("hidden"); }, 250);
|
|
132
|
+
}
|
|
133
|
+
toggleShellCommandMode();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
store.subscribe(function (state, previous) {
|
|
137
|
+
if (previous.connected && !state.connected && state.shellCommandRunning) {
|
|
138
|
+
resetShellCommand();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resetShellCommand() {
|
|
144
|
+
store.set({ shellCommandMode: false, shellCommandRunning: false, pendingShellCommandId: null });
|
|
145
|
+
var input = getInput();
|
|
146
|
+
if (input) input.disabled = false;
|
|
147
|
+
setMode(false);
|
|
148
|
+
}
|
|
@@ -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");
|
|
@@ -792,7 +792,16 @@ function attachMessageProcessor(ctx) {
|
|
|
792
792
|
// processQueryStream can finish the turn when the iterator closes.
|
|
793
793
|
var adapterErrorText = parsed.text || parsed.message || parsed.error || "Agent runtime error";
|
|
794
794
|
session._lastAdapterError = adapterErrorText;
|
|
795
|
-
|
|
795
|
+
var isSessionWriterConflict = /thread-store conflict|already has an active writer/i.test(adapterErrorText);
|
|
796
|
+
if (isSessionWriterConflict) {
|
|
797
|
+
sendAndRecord(session, {
|
|
798
|
+
type: "session_writer_conflict",
|
|
799
|
+
vendor: session.vendor || "codex",
|
|
800
|
+
text: "This Codex session is already open in another Clay or Codex process. Stop the other server or close the other session, then try again.",
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
sendAndRecord(session, { type: "error", text: adapterErrorText });
|
|
804
|
+
}
|
|
796
805
|
|
|
797
806
|
} else if (parsed.yokeType === "model_refusal") {
|
|
798
807
|
// Model declined the request. "fallback" => the CLI retried on another
|
package/lib/ws-schema.js
CHANGED
|
@@ -145,6 +145,7 @@ var schema = {
|
|
|
145
145
|
"kill_process": { direction: "c2s", handler: "lib/project-sessions.js", description: "Kill a system process by PID" },
|
|
146
146
|
"process_killed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Process was successfully killed" },
|
|
147
147
|
"process_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Conflict: another process is using the session" },
|
|
148
|
+
"session_writer_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Codex session is already open in another process" },
|
|
148
149
|
|
|
149
150
|
// -----------------------------------------------------------------------
|
|
150
151
|
// Context / usage
|
|
@@ -353,6 +354,8 @@ var schema = {
|
|
|
353
354
|
"term_closed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal session was closed" },
|
|
354
355
|
"term_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of open terminals" },
|
|
355
356
|
"term_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal error (e.g. access denied)" },
|
|
357
|
+
"shell_command": { direction: "c2s", handler: "lib/project-shell-command.js", description: "Run a one-shot shell command for agent context" },
|
|
358
|
+
"shell_command_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "One-shot shell command output and exit status" },
|
|
356
359
|
|
|
357
360
|
// -----------------------------------------------------------------------
|
|
358
361
|
// Sticky notes
|
|
@@ -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
|
+
};
|