clay-server 3.3.2-beta.3 → 3.4.0-beta.10
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/daemon-projects.js +3 -3
- package/lib/daemon.js +1 -1
- package/lib/project-connection.js +20 -5
- package/lib/project-debate.js +4 -0
- package/lib/project-file-watch.js +77 -16
- package/lib/project-mate-interaction.js +21 -1
- package/lib/project-memory.js +3 -0
- package/lib/project-shell-command.js +160 -0
- package/lib/project-user-message.js +10 -0
- package/lib/project.js +19 -4
- package/lib/public/antigravity-avatar.png +0 -0
- package/lib/public/app.js +28 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/input.css +79 -0
- package/lib/public/css/mates.css +7 -0
- package/lib/public/css/menus.css +48 -14
- package/lib/public/css/messages.css +9 -0
- package/lib/public/css/pane.css +4 -0
- package/lib/public/css/pwa-mobile.css +17 -0
- package/lib/public/css/title-bar.css +19 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +43 -4
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/app-header.js +4 -0
- package/lib/public/modules/app-messages.js +28 -5
- package/lib/public/modules/app-panels.js +37 -5
- package/lib/public/modules/app-projects.js +3 -1
- package/lib/public/modules/app-rendering.js +22 -1
- package/lib/public/modules/input.js +58 -28
- package/lib/public/modules/mate-sidebar.js +17 -3
- package/lib/public/modules/notifications.js +7 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/shell-command.js +148 -0
- package/lib/public/modules/sidebar-mates.js +7 -1
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/sidebar-sessions.js +6 -6
- package/lib/public/modules/split-pair-ui.js +3 -2
- package/lib/public/modules/tools.js +12 -1
- package/lib/public/modules/vendor-priority.js +14 -0
- package/lib/public/modules/vendor-selection.js +20 -0
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/public/opencode-avatar.svg +4 -0
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +3 -2
- package/lib/sdk-bridge.js +59 -19
- package/lib/sdk-message-processor.js +10 -1
- package/lib/session-notes-mcp-server.js +7 -1
- package/lib/worktree.js +9 -4
- package/lib/ws-schema.js +3 -0
- package/lib/yoke/acp-agent-profiles.js +238 -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/antigravity.js +417 -0
- package/lib/yoke/adapters/codex.js +98 -3
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -4
- package/lib/yoke/adapters/opencode.js +7 -0
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-app-server.js +25 -8
- package/lib/yoke/index.js +81 -13
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +2 -0
- package/lib/yoke/kiro-acp-server.js +30 -276
- package/lib/yoke/vendor-registry.js +77 -0
- package/package.json +1 -1
package/lib/sdk-bridge.js
CHANGED
|
@@ -110,6 +110,7 @@ function createSDKBridge(opts) {
|
|
|
110
110
|
var adapters = opts.adapters || {};
|
|
111
111
|
var mateDisplayName = opts.mateDisplayName || "";
|
|
112
112
|
var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
|
|
113
|
+
var osUsers = !!opts.osUsers;
|
|
113
114
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
114
115
|
// mcpServers may be either a static object or a getter function. The
|
|
115
116
|
// getter form lets callers gate individual servers at call time (e.g.
|
|
@@ -129,7 +130,7 @@ function createSDKBridge(opts) {
|
|
|
129
130
|
var _cachedFreshAuthState = null;
|
|
130
131
|
var _cachedFreshAuthAt = 0;
|
|
131
132
|
|
|
132
|
-
function
|
|
133
|
+
function getAcpMcpServers(session) {
|
|
133
134
|
if (!session) return [];
|
|
134
135
|
var bridgeArgs = [
|
|
135
136
|
path.join(__dirname, "yoke", "mcp-bridge-server.js"),
|
|
@@ -323,6 +324,14 @@ function createSDKBridge(opts) {
|
|
|
323
324
|
var mergeSkills = skills.mergeSkills;
|
|
324
325
|
|
|
325
326
|
// --- Message processing (extracted to sdk-message-processor.js) ---
|
|
327
|
+
function getSessionLinuxUser(session) {
|
|
328
|
+
if (!osUsers || !session) return null;
|
|
329
|
+
if (session.lastLinuxUser) return session.lastLinuxUser;
|
|
330
|
+
if (!session.ownerId) return null;
|
|
331
|
+
var owner = usersModule.findUserById(session.ownerId);
|
|
332
|
+
return owner && owner.linuxUser ? owner.linuxUser : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
326
335
|
// Auto-generate a session title via YOKE adapter.generateTitle().
|
|
327
336
|
// Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
|
|
328
337
|
function autoGenerateTitle(session) {
|
|
@@ -345,7 +354,10 @@ function createSDKBridge(opts) {
|
|
|
345
354
|
}
|
|
346
355
|
console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
|
|
347
356
|
|
|
348
|
-
sessionAdapter.generateTitle(userMessages, {
|
|
357
|
+
sessionAdapter.generateTitle(userMessages, {
|
|
358
|
+
cwd: cwd,
|
|
359
|
+
linuxUser: getSessionLinuxUser(session) || undefined,
|
|
360
|
+
}).then(function(title) {
|
|
349
361
|
if (!title || title.length < 2) return;
|
|
350
362
|
title = title.substring(0, 100);
|
|
351
363
|
if (!session.titleManuallySet) {
|
|
@@ -1131,7 +1143,9 @@ function createSDKBridge(opts) {
|
|
|
1131
1143
|
async function rollbackConversation(session, numTurns) {
|
|
1132
1144
|
var sessionAdapter = getAdapterForSession(session);
|
|
1133
1145
|
if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
|
|
1134
|
-
await sessionAdapter.rollbackThread(session.cliSessionId, numTurns
|
|
1146
|
+
await sessionAdapter.rollbackThread(session.cliSessionId, numTurns, {
|
|
1147
|
+
linuxUser: getSessionLinuxUser(session) || undefined,
|
|
1148
|
+
});
|
|
1135
1149
|
}
|
|
1136
1150
|
// Claude: conversation rollback is handled by rewindFiles + local history trim
|
|
1137
1151
|
}
|
|
@@ -1143,7 +1157,11 @@ function createSDKBridge(opts) {
|
|
|
1143
1157
|
|
|
1144
1158
|
async function forkSessionUnified(session, uuid) {
|
|
1145
1159
|
var sessionAdapter = getAdapterForSession(session);
|
|
1146
|
-
var result = await sessionAdapter.forkSession(session.cliSessionId, {
|
|
1160
|
+
var result = await sessionAdapter.forkSession(session.cliSessionId, {
|
|
1161
|
+
upToMessageId: uuid,
|
|
1162
|
+
dir: cwd,
|
|
1163
|
+
linuxUser: getSessionLinuxUser(session) || undefined,
|
|
1164
|
+
});
|
|
1147
1165
|
if (!result || !result.sessionId) throw new Error("Fork returned no session id");
|
|
1148
1166
|
|
|
1149
1167
|
// Adapters with rollbackThread (e.g. Codex) use local history copy
|
|
@@ -1471,6 +1489,7 @@ function createSDKBridge(opts) {
|
|
|
1471
1489
|
|
|
1472
1490
|
var queryOpts = {
|
|
1473
1491
|
cwd: cwd,
|
|
1492
|
+
linuxUser: linuxUser || undefined,
|
|
1474
1493
|
// appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
|
|
1475
1494
|
// REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
|
|
1476
1495
|
// would strip the driver session down to the two-sentence pair preset.
|
|
@@ -1525,13 +1544,20 @@ function createSDKBridge(opts) {
|
|
|
1525
1544
|
sandboxMode: codexConfig.sandbox,
|
|
1526
1545
|
webSearchMode: codexConfig.webSearch,
|
|
1527
1546
|
},
|
|
1547
|
+
ACP: {
|
|
1548
|
+
mcpServers: getAcpMcpServers(session),
|
|
1549
|
+
},
|
|
1550
|
+
ANTIGRAVITY: {
|
|
1551
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions
|
|
1552
|
+
|| session.permissionMode === "bypassPermissions",
|
|
1553
|
+
},
|
|
1528
1554
|
KIRO: {
|
|
1529
1555
|
engine: kiroConfig.engine,
|
|
1530
1556
|
mode: kiroConfig.mode,
|
|
1531
1557
|
// ACP accepts additional stdio MCP servers per session. The bridge is
|
|
1532
1558
|
// bound to this Clay session so partner calls cannot leak across
|
|
1533
1559
|
// concurrent Kiro sessions or users.
|
|
1534
|
-
mcpServers:
|
|
1560
|
+
mcpServers: getAcpMcpServers(session),
|
|
1535
1561
|
},
|
|
1536
1562
|
},
|
|
1537
1563
|
};
|
|
@@ -1787,6 +1813,23 @@ function createSDKBridge(opts) {
|
|
|
1787
1813
|
} catch (e) {}
|
|
1788
1814
|
if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
|
|
1789
1815
|
|
|
1816
|
+
var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
|
|
1817
|
+
var acpProfiles = require("./yoke/acp-agent-profiles");
|
|
1818
|
+
for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
|
|
1819
|
+
var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
|
|
1820
|
+
var subprocessInfo = yoke.getVendorInfo(subprocessVendor);
|
|
1821
|
+
if (!linuxUser || subprocessInfo.osUserIsolation) {
|
|
1822
|
+
if (subprocessVendor === "antigravity") {
|
|
1823
|
+
if (tryLookup(subprocessInfo.binaryName)) result.push(subprocessVendor);
|
|
1824
|
+
} else {
|
|
1825
|
+
var acpProfile = acpProfiles.getAcpAgentProfile(subprocessVendor);
|
|
1826
|
+
if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(subprocessVendor);
|
|
1827
|
+
}
|
|
1828
|
+
} else {
|
|
1829
|
+
console.log("[sdk-bridge] " + subprocessInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1790
1833
|
// Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
|
|
1791
1834
|
// binary to an OS-isolated user, even if that user also has Kiro installed.
|
|
1792
1835
|
var kiroInfo = yoke.getVendorInfo("kiro");
|
|
@@ -1816,6 +1859,13 @@ function createSDKBridge(opts) {
|
|
|
1816
1859
|
var defaultVendor = adapter ? adapter.vendor : "claude";
|
|
1817
1860
|
sm.defaultVendor = defaultVendor;
|
|
1818
1861
|
|
|
1862
|
+
// Installation detection is independent of adapter initialization. Publish
|
|
1863
|
+
// it before warmup so a new project does not render the initial empty state
|
|
1864
|
+
// as if every CLI were missing while the default adapter starts up.
|
|
1865
|
+
sm.installedVendors = detectInstalledVendors(linuxUser);
|
|
1866
|
+
sm.availableVendors = getAvailableVendors(linuxUser);
|
|
1867
|
+
sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
|
|
1868
|
+
|
|
1819
1869
|
// Initialize default adapter first (provides skills, slash commands, etc.)
|
|
1820
1870
|
if (adapter) {
|
|
1821
1871
|
try {
|
|
@@ -1867,19 +1917,8 @@ function createSDKBridge(opts) {
|
|
|
1867
1917
|
// actually issues a query with it.
|
|
1868
1918
|
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1869
1919
|
|
|
1870
|
-
//
|
|
1871
|
-
sm.
|
|
1872
|
-
sm.availableVendors = getAvailableVendors(linuxUser);
|
|
1873
|
-
|
|
1874
|
-
// Send initial state to client
|
|
1875
|
-
send({
|
|
1876
|
-
type: "model_info",
|
|
1877
|
-
model: sm.currentModel || "",
|
|
1878
|
-
models: getModelsForVendor(defaultVendor),
|
|
1879
|
-
vendor: defaultVendor,
|
|
1880
|
-
availableVendors: sm.availableVendors,
|
|
1881
|
-
installedVendors: sm.installedVendors,
|
|
1882
|
-
});
|
|
1920
|
+
// Send the fully initialized state to the client.
|
|
1921
|
+
sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
|
|
1883
1922
|
}
|
|
1884
1923
|
|
|
1885
1924
|
async function setModel(session, model) {
|
|
@@ -1993,7 +2032,7 @@ function createSDKBridge(opts) {
|
|
|
1993
2032
|
// Creates a mention session that can be reused across multiple mentions
|
|
1994
2033
|
// within a conversation flow (session continuity).
|
|
1995
2034
|
async function createMentionSession(opts) {
|
|
1996
|
-
// opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
|
|
2035
|
+
// opts: { vendor, linuxUser, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
|
|
1997
2036
|
var abortController = new AbortController();
|
|
1998
2037
|
|
|
1999
2038
|
// Current response callbacks (swapped on each pushMessage)
|
|
@@ -2013,6 +2052,7 @@ function createSDKBridge(opts) {
|
|
|
2013
2052
|
try {
|
|
2014
2053
|
handle = await mentionAdapter.createQuery({
|
|
2015
2054
|
cwd: cwd,
|
|
2055
|
+
linuxUser: opts.linuxUser || undefined,
|
|
2016
2056
|
systemPrompt: opts.claudeMd,
|
|
2017
2057
|
model: opts.model || undefined,
|
|
2018
2058
|
toolServers: opts.includeMcpServers ? (mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined) : undefined,
|
|
@@ -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
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
|
|
5
|
-
var MEMORY_CONTRACT =
|
|
5
|
+
var MEMORY_CONTRACT =
|
|
6
|
+
"The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
|
|
7
|
+
"Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
|
|
8
|
+
"Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
|
|
9
|
+
"Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
|
|
10
|
+
"Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
|
|
11
|
+
"Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
|
|
6
12
|
|
|
7
13
|
function getToolDefs(handlers) {
|
|
8
14
|
return [
|
package/lib/worktree.js
CHANGED
|
@@ -43,9 +43,14 @@ function isWorktree(projectPath) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function isPathInside(parentPath, candidatePath) {
|
|
47
|
+
var relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
|
|
48
|
+
return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
// Scan worktrees for a given project path
|
|
47
|
-
// Returns array of { path, branch, bare, detached,
|
|
48
|
-
//
|
|
52
|
+
// Returns array of { path, branch, bare, detached, external }
|
|
53
|
+
// external = true when Git registered the worktree outside the main project folder
|
|
49
54
|
function scanWorktrees(projectPath) {
|
|
50
55
|
var resolvedParent = path.resolve(projectPath);
|
|
51
56
|
try {
|
|
@@ -63,7 +68,7 @@ function scanWorktrees(projectPath) {
|
|
|
63
68
|
if (wt.bare) continue;
|
|
64
69
|
var resolvedWt = path.resolve(wt.path);
|
|
65
70
|
if (resolvedWt === resolvedParent) continue;
|
|
66
|
-
wt.
|
|
71
|
+
wt.external = !isPathInside(resolvedParent, resolvedWt);
|
|
67
72
|
wt.dirName = path.basename(wt.path);
|
|
68
73
|
results.push(wt);
|
|
69
74
|
}
|
|
@@ -131,4 +136,4 @@ function removeWorktree(projectPath, worktreeDirName) {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
|
|
139
|
+
module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
|
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,238 @@
|
|
|
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
|
+
function trustDefaultWhenModeIsNotExposed(ctx, next) {
|
|
100
|
+
var options = ctx.state && ctx.state.configOptions;
|
|
101
|
+
for (var i = 0; i < (options || []).length; i++) {
|
|
102
|
+
if (options[i].category === "mode" || options[i].id === "mode") return next();
|
|
103
|
+
}
|
|
104
|
+
if (ctx.state && ctx.state.modes) return next();
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
var ACP_AGENT_PROFILES = {
|
|
109
|
+
opencode: {
|
|
110
|
+
vendor: "opencode",
|
|
111
|
+
displayName: "OpenCode",
|
|
112
|
+
binaryName: "opencode",
|
|
113
|
+
overrideName: "OPENCODE_CLI_PATH",
|
|
114
|
+
args: ["acp"],
|
|
115
|
+
defaultModels: ["auto"],
|
|
116
|
+
defaultModel: "auto",
|
|
117
|
+
sessionResume: null,
|
|
118
|
+
fetchModels: fetchOpenCodeModels,
|
|
119
|
+
permissionModeGuaranteed: true,
|
|
120
|
+
prepare: function(ctx) {
|
|
121
|
+
var injected = ctx.initOpts && ctx.initOpts._openCodeAgentNames;
|
|
122
|
+
if (Array.isArray(injected)) {
|
|
123
|
+
ctx.driverState.agentNames = injected.slice();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
return fetchOpenCodeAgentNames(ctx.binaryPath, ctx.cwd, ctx.initOpts && ctx.initOpts.env).then(function(names) {
|
|
127
|
+
ctx.driverState.agentNames = names;
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
buildProcessOptions: function(ctx, base) {
|
|
131
|
+
var existing = (base.env && base.env.OPENCODE_CONFIG_CONTENT) || process.env.OPENCODE_CONFIG_CONTENT;
|
|
132
|
+
var config = {};
|
|
133
|
+
if (existing) config = JSON.parse(existing);
|
|
134
|
+
var agent = Object.assign({}, config.agent || {});
|
|
135
|
+
var names = ["build", "plan"].concat(ctx.driverState.agentNames || []);
|
|
136
|
+
for (var i = 0; i < names.length; i++) {
|
|
137
|
+
agent[names[i]] = Object.assign({}, agent[names[i]] || {}, { permission: "ask" });
|
|
138
|
+
}
|
|
139
|
+
base.env = Object.assign({}, base.env || {}, {
|
|
140
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(Object.assign({}, config, { permission: "ask", agent: agent })),
|
|
141
|
+
});
|
|
142
|
+
return base;
|
|
143
|
+
},
|
|
144
|
+
validateProcessOptions: function(ctx) {
|
|
145
|
+
var injected = ctx.initOpts && ctx.initOpts._openCodeResolvedConfig;
|
|
146
|
+
var resolved = injected
|
|
147
|
+
? Promise.resolve(injected)
|
|
148
|
+
: fetchOpenCodeResolvedConfig(ctx.binaryPath, ctx.cwd, ctx.processOptions.env);
|
|
149
|
+
return resolved.then(function(config) {
|
|
150
|
+
validateOpenCodeConfig(config);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
ensureSafePermissionMode: function() {
|
|
154
|
+
// OpenCode ACP modes select agents, not approval policy. The process
|
|
155
|
+
// configuration above enforces ask at both global and effective-agent levels.
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
kimi: {
|
|
160
|
+
vendor: "kimi",
|
|
161
|
+
displayName: "Kimi Code",
|
|
162
|
+
binaryName: "kimi",
|
|
163
|
+
overrideName: "KIMI_CLI_PATH",
|
|
164
|
+
args: ["acp"],
|
|
165
|
+
defaultModels: ["auto"],
|
|
166
|
+
defaultModel: "auto",
|
|
167
|
+
sessionResume: null,
|
|
168
|
+
ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
|
|
169
|
+
},
|
|
170
|
+
grok: {
|
|
171
|
+
vendor: "grok",
|
|
172
|
+
displayName: "Grok Build",
|
|
173
|
+
binaryName: "grok",
|
|
174
|
+
overrideName: "GROK_CLI_PATH",
|
|
175
|
+
args: ["--no-auto-update", "--permission-mode", "ask", "agent", "stdio"],
|
|
176
|
+
defaultModels: ["auto"],
|
|
177
|
+
defaultModel: "auto",
|
|
178
|
+
sessionResume: null,
|
|
179
|
+
permissionModeGuaranteed: true,
|
|
180
|
+
},
|
|
181
|
+
copilot: {
|
|
182
|
+
vendor: "copilot",
|
|
183
|
+
displayName: "GitHub Copilot CLI",
|
|
184
|
+
binaryName: "copilot",
|
|
185
|
+
overrideName: "COPILOT_CLI_PATH",
|
|
186
|
+
args: ["--acp"],
|
|
187
|
+
defaultModels: ["auto"],
|
|
188
|
+
defaultModel: "auto",
|
|
189
|
+
sessionResume: null,
|
|
190
|
+
ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
|
|
191
|
+
},
|
|
192
|
+
qwen: {
|
|
193
|
+
vendor: "qwen",
|
|
194
|
+
displayName: "Qwen Code",
|
|
195
|
+
binaryName: "qwen",
|
|
196
|
+
overrideName: "QWEN_CLI_PATH",
|
|
197
|
+
args: ["--acp", "--approval-mode", "default"],
|
|
198
|
+
defaultModels: ["auto"],
|
|
199
|
+
defaultModel: "auto",
|
|
200
|
+
sessionResume: null,
|
|
201
|
+
permissionModeGuaranteed: true,
|
|
202
|
+
},
|
|
203
|
+
junie: {
|
|
204
|
+
vendor: "junie",
|
|
205
|
+
displayName: "Junie CLI",
|
|
206
|
+
binaryName: "junie",
|
|
207
|
+
overrideName: "JUNIE_CLI_PATH",
|
|
208
|
+
args: ["--acp", "true"],
|
|
209
|
+
defaultModels: ["auto"],
|
|
210
|
+
defaultModel: "auto",
|
|
211
|
+
sessionResume: null,
|
|
212
|
+
ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function getAcpAgentProfile(vendor) {
|
|
217
|
+
return ACP_AGENT_PROFILES[vendor] || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getAcpAgentDriver(vendor) {
|
|
221
|
+
return getAcpAgentProfile(vendor);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function findAcpAgentPath(profile) {
|
|
225
|
+
if (!profile) return null;
|
|
226
|
+
return findOnPath(profile.binaryName, profile.overrideName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
ACP_AGENT_PROFILES: ACP_AGENT_PROFILES,
|
|
231
|
+
getAcpAgentProfile: getAcpAgentProfile,
|
|
232
|
+
getAcpAgentDriver: getAcpAgentDriver,
|
|
233
|
+
findAcpAgentPath: findAcpAgentPath,
|
|
234
|
+
fetchOpenCodeModels: fetchOpenCodeModels,
|
|
235
|
+
fetchOpenCodeAgentNames: fetchOpenCodeAgentNames,
|
|
236
|
+
fetchOpenCodeResolvedConfig: fetchOpenCodeResolvedConfig,
|
|
237
|
+
validateOpenCodeConfig: validateOpenCodeConfig,
|
|
238
|
+
};
|
|
@@ -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
|
+
};
|