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/daemon-projects.js
CHANGED
|
@@ -41,10 +41,10 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
|
|
|
41
41
|
if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
|
|
42
42
|
}
|
|
43
43
|
if (alreadyRegistered) continue;
|
|
44
|
-
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName,
|
|
44
|
+
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
|
|
45
45
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
46
46
|
worktreeRegistry[parentSlug].push(wtSlug);
|
|
47
|
-
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.
|
|
47
|
+
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
|
|
48
48
|
}
|
|
49
49
|
daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
|
|
50
50
|
rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
|
|
@@ -79,7 +79,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
|
|
|
79
79
|
if (existingSlugs[ei] === wtSlug) { found = true; break; }
|
|
80
80
|
}
|
|
81
81
|
if (!found) {
|
|
82
|
-
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName,
|
|
82
|
+
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
|
|
83
83
|
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
84
84
|
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
85
85
|
worktreeRegistry[parentSlug].push(wtSlug);
|
package/lib/daemon.js
CHANGED
|
@@ -978,7 +978,7 @@ var relay = createServer({
|
|
|
978
978
|
if (!result.ok) return result;
|
|
979
979
|
// Register the new worktree as ephemeral project
|
|
980
980
|
var wtSlug = parentSlug + "--" + dirName;
|
|
981
|
-
var wtMeta = { parentSlug: parentSlug, branch: branchName,
|
|
981
|
+
var wtMeta = { parentSlug: parentSlug, branch: branchName, external: false };
|
|
982
982
|
relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
|
|
983
983
|
registerWorktreeSlug(parentSlug, wtSlug);
|
|
984
984
|
console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
|
|
@@ -22,6 +22,17 @@ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels) {
|
|
26
|
+
return {
|
|
27
|
+
type: "model_info",
|
|
28
|
+
model: initialModel || "",
|
|
29
|
+
models: initialModels || [],
|
|
30
|
+
vendor: initialVendor,
|
|
31
|
+
availableVendors: sm.availableVendors || [],
|
|
32
|
+
installedVendors: sm.installedVendors || [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
/**
|
|
26
37
|
* Attach connection/disconnection handlers to a project context.
|
|
27
38
|
*
|
|
@@ -116,7 +127,7 @@ function attachConnection(ctx) {
|
|
|
116
127
|
if (!_warmedUp) {
|
|
117
128
|
_warmedUp = true;
|
|
118
129
|
if (typeof warmup === "function") {
|
|
119
|
-
try { warmup(); }
|
|
130
|
+
try { warmup(wsUser && wsUser.linuxUser ? wsUser.linuxUser : null); }
|
|
120
131
|
catch (e) { console.error("[project-connection] warmup failed for " + slug + ":", e && e.message ? e.message : e); }
|
|
121
132
|
}
|
|
122
133
|
}
|
|
@@ -167,9 +178,9 @@ function attachConnection(ctx) {
|
|
|
167
178
|
sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
|
|
168
179
|
}
|
|
169
180
|
var initialModel = (restoredActive && restoredActive.model) || sm.currentModel || "";
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
// Vendor installation state is needed even before a new project has a
|
|
182
|
+
// model. Always send it so reconnects cannot fall back to "Not installed."
|
|
183
|
+
sendTo(ws, buildInitialModelInfo(sm, initialVendor, initialModel, initialModels));
|
|
173
184
|
sendTo(ws, { type: "config_state", model: initialModel, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
174
185
|
sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
|
|
175
186
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
@@ -333,4 +344,8 @@ function attachConnection(ctx) {
|
|
|
333
344
|
};
|
|
334
345
|
}
|
|
335
346
|
|
|
336
|
-
module.exports = {
|
|
347
|
+
module.exports = {
|
|
348
|
+
attachConnection: attachConnection,
|
|
349
|
+
buildInitialModelInfo: buildInitialModelInfo,
|
|
350
|
+
dispatchMessageSafely: dispatchMessageSafely,
|
|
351
|
+
};
|
package/lib/project-debate.js
CHANGED
|
@@ -604,6 +604,7 @@ function attachDebate(ctx) {
|
|
|
604
604
|
var _modMate = matesModule.getMate(mateCtx, debate.moderatorId);
|
|
605
605
|
ctx.sdk.createMentionSession({
|
|
606
606
|
vendor: _modMate ? _modMate.vendor : null,
|
|
607
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
607
608
|
claudeMd: claudeMd,
|
|
608
609
|
initialContext: digests,
|
|
609
610
|
initialMessage: quickBriefPrompt,
|
|
@@ -799,6 +800,7 @@ function attachDebate(ctx) {
|
|
|
799
800
|
var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
|
|
800
801
|
ctx.sdk.createMentionSession({
|
|
801
802
|
vendor: _modMate2 ? _modMate2.vendor : null,
|
|
803
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
802
804
|
claudeMd: claudeMd,
|
|
803
805
|
initialContext: moderatorContext,
|
|
804
806
|
initialMessage: "Begin the debate on: " + debate.topic,
|
|
@@ -994,6 +996,7 @@ function attachDebate(ctx) {
|
|
|
994
996
|
var _panMate = matesModule.getMate(debate.mateCtx, mateId);
|
|
995
997
|
ctx.sdk.createMentionSession({
|
|
996
998
|
vendor: _panMate ? _panMate.vendor : null,
|
|
999
|
+
linuxUser: debate.mateCtx && debate.mateCtx.linuxUser ? debate.mateCtx.linuxUser : undefined,
|
|
997
1000
|
claudeMd: claudeMd,
|
|
998
1001
|
initialContext: panelistContext + historyContext,
|
|
999
1002
|
initialMessage: "The moderator addresses you:\n\n" + moderatorText,
|
|
@@ -1530,6 +1533,7 @@ function attachDebate(ctx) {
|
|
|
1530
1533
|
var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
|
|
1531
1534
|
ctx.sdk.createMentionSession({
|
|
1532
1535
|
vendor: _modMate3 ? _modMate3.vendor : null,
|
|
1536
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
1533
1537
|
claudeMd: claudeMd,
|
|
1534
1538
|
initialContext: moderatorContext,
|
|
1535
1539
|
initialMessage: resumePrompt,
|
|
@@ -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
|
|
|
@@ -179,6 +179,7 @@ function attachMateInteraction(ctx) {
|
|
|
179
179
|
var _digestQueue = [];
|
|
180
180
|
var _digestBusy = false;
|
|
181
181
|
var _digestWorkerTurns = 0;
|
|
182
|
+
var _digestWorkerLinuxUser = null;
|
|
182
183
|
var DIGEST_WORKER_MAX_TURNS = 20;
|
|
183
184
|
|
|
184
185
|
function enqueueDigest(job) {
|
|
@@ -191,6 +192,13 @@ function attachMateInteraction(ctx) {
|
|
|
191
192
|
_digestBusy = true;
|
|
192
193
|
var job = _digestQueue.shift();
|
|
193
194
|
|
|
195
|
+
if (_digestWorker && _digestWorkerLinuxUser !== (job.linuxUser || null)) {
|
|
196
|
+
try { _digestWorker.close(); } catch (e) {}
|
|
197
|
+
_digestWorker = null;
|
|
198
|
+
_digestWorkerTurns = 0;
|
|
199
|
+
_digestWorkerLinuxUser = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
|
|
195
203
|
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
196
204
|
|
|
@@ -315,6 +323,7 @@ function attachMateInteraction(ctx) {
|
|
|
315
323
|
try { _digestWorker.close(); } catch (e) {}
|
|
316
324
|
_digestWorker = null;
|
|
317
325
|
_digestWorkerTurns = 0;
|
|
326
|
+
_digestWorkerLinuxUser = null;
|
|
318
327
|
}
|
|
319
328
|
|
|
320
329
|
var responseText = "";
|
|
@@ -328,12 +337,14 @@ function attachMateInteraction(ctx) {
|
|
|
328
337
|
console.error("[digest-worker] Error:", err);
|
|
329
338
|
_digestWorker = null;
|
|
330
339
|
_digestWorkerTurns = 0;
|
|
340
|
+
_digestWorkerLinuxUser = null;
|
|
331
341
|
if (job.onError) job.onError(err);
|
|
332
342
|
processDigestQueue();
|
|
333
343
|
},
|
|
334
344
|
});
|
|
335
345
|
} else {
|
|
336
346
|
sdk.createMentionSession({
|
|
347
|
+
linuxUser: job.linuxUser || undefined,
|
|
337
348
|
claudeMd: "",
|
|
338
349
|
model: "haiku",
|
|
339
350
|
initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
|
|
@@ -344,10 +355,15 @@ function attachMateInteraction(ctx) {
|
|
|
344
355
|
onError: function (err) {
|
|
345
356
|
console.error("[digest-worker] Create error:", err);
|
|
346
357
|
_digestWorker = null;
|
|
358
|
+
_digestWorkerLinuxUser = null;
|
|
347
359
|
if (job.onError) job.onError(err);
|
|
348
360
|
processDigestQueue();
|
|
349
361
|
},
|
|
350
|
-
}).then(function (ws) {
|
|
362
|
+
}).then(function (ws) {
|
|
363
|
+
_digestWorker = ws;
|
|
364
|
+
_digestWorkerTurns = 1;
|
|
365
|
+
_digestWorkerLinuxUser = job.linuxUser || null;
|
|
366
|
+
}).catch(function (err) {
|
|
351
367
|
if (job.onError) job.onError(err || new Error("digest worker creation failed"));
|
|
352
368
|
processDigestQueue();
|
|
353
369
|
});
|
|
@@ -378,6 +394,7 @@ function attachMateInteraction(ctx) {
|
|
|
378
394
|
|
|
379
395
|
enqueueDigest({
|
|
380
396
|
mateCtx: mateCtx,
|
|
397
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
381
398
|
mateId: mateId,
|
|
382
399
|
type: "mention",
|
|
383
400
|
conversationContent: conversationContent,
|
|
@@ -473,6 +490,7 @@ function attachMateInteraction(ctx) {
|
|
|
473
490
|
|
|
474
491
|
enqueueDigest({
|
|
475
492
|
mateCtx: mateCtx,
|
|
493
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
476
494
|
mateId: mateId,
|
|
477
495
|
type: "dm",
|
|
478
496
|
priorSummary: priorSummary || "",
|
|
@@ -716,6 +734,7 @@ function attachMateInteraction(ctx) {
|
|
|
716
734
|
// Create new persistent mention session
|
|
717
735
|
sdk.createMentionSession({
|
|
718
736
|
vendor: mate.vendor || null,
|
|
737
|
+
linuxUser: getLinuxUserForSession(session) || undefined,
|
|
719
738
|
claudeMd: claudeMd,
|
|
720
739
|
initialContext: mentionContext,
|
|
721
740
|
initialMessage: mentionFullInput,
|
|
@@ -887,6 +906,7 @@ function attachMateInteraction(ctx) {
|
|
|
887
906
|
].join("\n");
|
|
888
907
|
|
|
889
908
|
sdk.createMentionSession({
|
|
909
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
890
910
|
claudeMd: "",
|
|
891
911
|
model: "haiku",
|
|
892
912
|
initialContext: synthesisContext,
|
package/lib/project-memory.js
CHANGED
|
@@ -339,6 +339,7 @@ function attachMemory(ctx) {
|
|
|
339
339
|
var gateText = "";
|
|
340
340
|
var _gateSession = null;
|
|
341
341
|
sdk.createMentionSession({
|
|
342
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
342
343
|
claudeMd: "",
|
|
343
344
|
model: "haiku",
|
|
344
345
|
initialContext: gateContext,
|
|
@@ -448,6 +449,7 @@ function attachMemory(ctx) {
|
|
|
448
449
|
var updateText = "";
|
|
449
450
|
var _updateSession = null;
|
|
450
451
|
sdk.createMentionSession({
|
|
452
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
451
453
|
claudeMd: "",
|
|
452
454
|
model: "haiku",
|
|
453
455
|
initialContext: updateContext,
|
|
@@ -553,6 +555,7 @@ function attachMemory(ctx) {
|
|
|
553
555
|
var initText = "";
|
|
554
556
|
var _initSession = null;
|
|
555
557
|
sdk.createMentionSession({
|
|
558
|
+
linuxUser: mateCtx.linuxUser || undefined,
|
|
556
559
|
claudeMd: "",
|
|
557
560
|
model: "haiku",
|
|
558
561
|
initialContext: initContext,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
var { spawn } = require("child_process");
|
|
2
|
+
var { buildUserEnv } = require("./build-user-env");
|
|
3
|
+
var { wrapSpawnAsUser } = require("./os-users");
|
|
4
|
+
|
|
5
|
+
var MAX_COMMAND_LENGTH = 16 * 1024;
|
|
6
|
+
var MAX_OUTPUT_LENGTH = 64 * 1024;
|
|
7
|
+
var COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
8
|
+
|
|
9
|
+
function stripTerminalCodes(value) {
|
|
10
|
+
return String(value || "")
|
|
11
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
12
|
+
.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "")
|
|
13
|
+
.replace(/\r/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function attachShellCommand(ctx) {
|
|
17
|
+
var runningBySocket = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function sendResult(ws, data) {
|
|
20
|
+
ctx.sendTo(ws, Object.assign({ type: "shell_command_result" }, data));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handleShellCommand(ws, msg) {
|
|
24
|
+
if (msg.type !== "shell_command") return false;
|
|
25
|
+
|
|
26
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 100) : "";
|
|
27
|
+
var command = typeof msg.command === "string" ? msg.command.trim() : "";
|
|
28
|
+
if (!requestId || !command) {
|
|
29
|
+
sendResult(ws, { requestId: requestId, error: "Enter a command to run." });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (command.length > MAX_COMMAND_LENGTH) {
|
|
33
|
+
sendResult(ws, { requestId: requestId, error: "Command is too long." });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ws._clayUser) {
|
|
38
|
+
var permissions = ctx.usersModule.getEffectivePermissions(ws._clayUser, ctx.osUsers);
|
|
39
|
+
if (!permissions.terminal) {
|
|
40
|
+
sendResult(ws, { requestId: requestId, error: "Terminal access is not permitted." });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (runningBySocket.get(ws)) {
|
|
45
|
+
sendResult(ws, { requestId: requestId, error: "Another shell command is still running." });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var session = ctx.getSessionForWs(ws);
|
|
50
|
+
if (!session) {
|
|
51
|
+
sendResult(ws, { requestId: requestId, error: "No active session." });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var osUserInfo = ctx.getOsUserInfoForWs(ws);
|
|
56
|
+
var shell = (osUserInfo && osUserInfo.shell)
|
|
57
|
+
|| process.env.SHELL
|
|
58
|
+
|| (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/sh");
|
|
59
|
+
var args = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command];
|
|
60
|
+
var spawnOptions = {
|
|
61
|
+
cwd: ctx.cwd,
|
|
62
|
+
env: buildUserEnv(osUserInfo),
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
};
|
|
65
|
+
if (osUserInfo) {
|
|
66
|
+
spawnOptions.uid = osUserInfo.uid;
|
|
67
|
+
spawnOptions.gid = osUserInfo.gid;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var wrapped = wrapSpawnAsUser(shell, args, spawnOptions);
|
|
71
|
+
var child;
|
|
72
|
+
try {
|
|
73
|
+
child = spawn(wrapped.command, wrapped.args, wrapped.options);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
sendResult(ws, { requestId: requestId, error: e.message || "Failed to start command." });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runningBySocket.set(ws, child);
|
|
80
|
+
var chunks = [];
|
|
81
|
+
var outputLength = 0;
|
|
82
|
+
var truncated = false;
|
|
83
|
+
var timedOut = false;
|
|
84
|
+
var finished = false;
|
|
85
|
+
|
|
86
|
+
function collect(data) {
|
|
87
|
+
if (outputLength >= MAX_OUTPUT_LENGTH) {
|
|
88
|
+
truncated = true;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
var text = data.toString("utf8");
|
|
92
|
+
var remaining = MAX_OUTPUT_LENGTH - outputLength;
|
|
93
|
+
if (text.length > remaining) {
|
|
94
|
+
text = text.slice(0, remaining);
|
|
95
|
+
truncated = true;
|
|
96
|
+
}
|
|
97
|
+
chunks.push(text);
|
|
98
|
+
outputLength += text.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
child.stdout.on("data", collect);
|
|
102
|
+
child.stderr.on("data", collect);
|
|
103
|
+
|
|
104
|
+
var timer = setTimeout(function () {
|
|
105
|
+
timedOut = true;
|
|
106
|
+
try { child.kill("SIGTERM"); } catch (e) {}
|
|
107
|
+
setTimeout(function () {
|
|
108
|
+
if (!finished) {
|
|
109
|
+
try { child.kill("SIGKILL"); } catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
}, 1000).unref();
|
|
112
|
+
}, COMMAND_TIMEOUT_MS);
|
|
113
|
+
timer.unref();
|
|
114
|
+
|
|
115
|
+
child.on("error", function (error) {
|
|
116
|
+
if (finished) return;
|
|
117
|
+
finished = true;
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
runningBySocket.delete(ws);
|
|
120
|
+
sendResult(ws, { requestId: requestId, command: command, error: error.message || "Command failed." });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
child.on("close", function (code, signal) {
|
|
124
|
+
if (finished) return;
|
|
125
|
+
finished = true;
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
runningBySocket.delete(ws);
|
|
128
|
+
|
|
129
|
+
var output = stripTerminalCodes(chunks.join(""));
|
|
130
|
+
if (truncated) output += "\n… output truncated at 64 KB";
|
|
131
|
+
if (timedOut) output += (output ? "\n" : "") + "Command timed out after 30 seconds.";
|
|
132
|
+
var exitCode = typeof code === "number" ? code : null;
|
|
133
|
+
var context = [
|
|
134
|
+
"[Shell command executed by the user]",
|
|
135
|
+
"$ " + command,
|
|
136
|
+
output || "(no output)",
|
|
137
|
+
"[Exit code: " + (exitCode == null ? (signal || "unknown") : exitCode) + "]",
|
|
138
|
+
].join("\n");
|
|
139
|
+
if (!session.pendingShellContexts) session.pendingShellContexts = [];
|
|
140
|
+
session.pendingShellContexts.push(context);
|
|
141
|
+
|
|
142
|
+
sendResult(ws, {
|
|
143
|
+
requestId: requestId,
|
|
144
|
+
sessionId: session.localId,
|
|
145
|
+
command: command,
|
|
146
|
+
output: output,
|
|
147
|
+
exitCode: exitCode,
|
|
148
|
+
signal: signal || null,
|
|
149
|
+
timedOut: timedOut,
|
|
150
|
+
truncated: truncated,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { handleShellCommand: handleShellCommand };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { attachShellCommand: attachShellCommand };
|
|
@@ -374,6 +374,16 @@ function attachUserMessage(ctx) {
|
|
|
374
374
|
fullText = mentionPrefix + "\n\n" + fullText;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// Inject one-shot shell results captured from the composer command mode.
|
|
378
|
+
if (session.pendingShellContexts && session.pendingShellContexts.length > 0) {
|
|
379
|
+
var shellPrefix = session.pendingShellContexts.join("\n\n");
|
|
380
|
+
session.pendingShellContexts = [];
|
|
381
|
+
fullText = shellPrefix + "\n\n" + fullText;
|
|
382
|
+
}
|
|
383
|
+
if (msg.shellCommandResponse) {
|
|
384
|
+
fullText += "\n\n[Respond to the shell command result above now.]";
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
// Inject active terminal context sources (delta only: send new output since last message)
|
|
378
388
|
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
379
389
|
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
package/lib/project.js
CHANGED
|
@@ -28,6 +28,7 @@ var { attachKnowledge } = require("./project-knowledge");
|
|
|
28
28
|
var { attachFilesystem } = require("./project-filesystem");
|
|
29
29
|
var { attachSessions } = require("./project-sessions");
|
|
30
30
|
var { attachUserMessage } = require("./project-user-message");
|
|
31
|
+
var { attachShellCommand } = require("./project-shell-command");
|
|
31
32
|
var { attachConnection } = require("./project-connection");
|
|
32
33
|
var { attachMcp } = require("./project-mcp");
|
|
33
34
|
var { createLocalMcp } = require("./mcp-local");
|
|
@@ -162,7 +163,7 @@ function createProjectContext(opts) {
|
|
|
162
163
|
var updateChannel = opts.updateChannel || "stable";
|
|
163
164
|
var osUsers = opts.osUsers || false;
|
|
164
165
|
var projectOwnerId = opts.projectOwnerId || null;
|
|
165
|
-
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch,
|
|
166
|
+
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, external }
|
|
166
167
|
var isMate = opts.isMate || false;
|
|
167
168
|
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
168
169
|
var serverPort = opts.port || 2633;
|
|
@@ -834,6 +835,7 @@ function createProjectContext(opts) {
|
|
|
834
835
|
getNotificationsModule: function () { return _notifications; },
|
|
835
836
|
mateDisplayName: opts.mateDisplayName || "",
|
|
836
837
|
isMate: isMate,
|
|
838
|
+
osUsers: osUsers,
|
|
837
839
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
838
840
|
mcpServers: getLocalMcpServers,
|
|
839
841
|
getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
|
|
@@ -1077,6 +1079,7 @@ function createProjectContext(opts) {
|
|
|
1077
1079
|
try {
|
|
1078
1080
|
var readyResult = await vendorAdapter.init({
|
|
1079
1081
|
cwd: cwd,
|
|
1082
|
+
linuxUser: modelLinuxUser || undefined,
|
|
1080
1083
|
clayPort: serverPort,
|
|
1081
1084
|
clayTls: serverTls,
|
|
1082
1085
|
clayAuthToken: serverAuthToken,
|
|
@@ -1176,6 +1179,9 @@ function createProjectContext(opts) {
|
|
|
1176
1179
|
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
1177
1180
|
if (_filesystem.handleFilesystemMessage(ws, msg)) return;
|
|
1178
1181
|
|
|
1182
|
+
// --- Shell command context ---
|
|
1183
|
+
if (_shellCommand.handleShellCommand(ws, msg)) return;
|
|
1184
|
+
|
|
1179
1185
|
// --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
|
|
1180
1186
|
if (_userMessage.handleUserMessage(ws, msg)) return;
|
|
1181
1187
|
}
|
|
@@ -1424,6 +1430,15 @@ function createProjectContext(opts) {
|
|
|
1424
1430
|
_email: _email,
|
|
1425
1431
|
});
|
|
1426
1432
|
|
|
1433
|
+
var _shellCommand = attachShellCommand({
|
|
1434
|
+
cwd: cwd,
|
|
1435
|
+
osUsers: osUsers,
|
|
1436
|
+
usersModule: usersModule,
|
|
1437
|
+
sendTo: sendTo,
|
|
1438
|
+
getSessionForWs: getSessionForWs,
|
|
1439
|
+
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1427
1442
|
// --- Filesystem handler (delegated to project-filesystem.js) ---
|
|
1428
1443
|
var _filesystem = attachFilesystem({
|
|
1429
1444
|
cwd: cwd,
|
|
@@ -1673,8 +1688,8 @@ function createProjectContext(opts) {
|
|
|
1673
1688
|
getProject: function () { return project; },
|
|
1674
1689
|
// Exposed so the first websocket connection can lazily warm up the
|
|
1675
1690
|
// adapters for this project (see project-connection handleConnection).
|
|
1676
|
-
warmup: function () {
|
|
1677
|
-
sdk.warmup();
|
|
1691
|
+
warmup: function (linuxUser) {
|
|
1692
|
+
sdk.warmup(osUsers ? linuxUser : null);
|
|
1678
1693
|
sdk.startIdleReaper();
|
|
1679
1694
|
if (!osUsers && !sessionTitleMigrationScheduled) {
|
|
1680
1695
|
sessionTitleMigrationScheduled = true;
|
|
@@ -1800,7 +1815,7 @@ function createProjectContext(opts) {
|
|
|
1800
1815
|
status.isWorktree = true;
|
|
1801
1816
|
status.parentSlug = worktreeMeta.parentSlug;
|
|
1802
1817
|
status.branch = worktreeMeta.branch;
|
|
1803
|
-
status.
|
|
1818
|
+
status.worktreeExternal = worktreeMeta.external === true;
|
|
1804
1819
|
}
|
|
1805
1820
|
if (usersModule.isMultiUser()) {
|
|
1806
1821
|
var seen = {};
|
|
Binary file
|