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.
Files changed (77) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +1 -1
  3. package/lib/project-connection.js +20 -5
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-file-watch.js +77 -16
  6. package/lib/project-mate-interaction.js +21 -1
  7. package/lib/project-memory.js +3 -0
  8. package/lib/project-shell-command.js +160 -0
  9. package/lib/project-user-message.js +10 -0
  10. package/lib/project.js +19 -4
  11. package/lib/public/antigravity-avatar.png +0 -0
  12. package/lib/public/app.js +28 -0
  13. package/lib/public/copilot-avatar.svg +5 -0
  14. package/lib/public/css/command-palette.css +22 -2
  15. package/lib/public/css/icon-strip.css +29 -13
  16. package/lib/public/css/input.css +79 -0
  17. package/lib/public/css/mates.css +7 -0
  18. package/lib/public/css/menus.css +48 -14
  19. package/lib/public/css/messages.css +9 -0
  20. package/lib/public/css/pane.css +4 -0
  21. package/lib/public/css/pwa-mobile.css +17 -0
  22. package/lib/public/css/title-bar.css +19 -0
  23. package/lib/public/grok-avatar.svg +4 -0
  24. package/lib/public/index.html +43 -4
  25. package/lib/public/junie-avatar.svg +11 -0
  26. package/lib/public/kimi-avatar.svg +4 -0
  27. package/lib/public/modules/app-header.js +4 -0
  28. package/lib/public/modules/app-messages.js +28 -5
  29. package/lib/public/modules/app-panels.js +37 -5
  30. package/lib/public/modules/app-projects.js +3 -1
  31. package/lib/public/modules/app-rendering.js +22 -1
  32. package/lib/public/modules/input.js +58 -28
  33. package/lib/public/modules/mate-sidebar.js +17 -3
  34. package/lib/public/modules/notifications.js +7 -1
  35. package/lib/public/modules/pane-bridge.js +11 -0
  36. package/lib/public/modules/pane-links.js +23 -0
  37. package/lib/public/modules/project-switcher.js +7 -6
  38. package/lib/public/modules/shell-command.js +148 -0
  39. package/lib/public/modules/sidebar-mates.js +7 -1
  40. package/lib/public/modules/sidebar-mobile.js +2 -1
  41. package/lib/public/modules/sidebar-projects.js +34 -43
  42. package/lib/public/modules/sidebar-sessions.js +6 -6
  43. package/lib/public/modules/split-pair-ui.js +3 -2
  44. package/lib/public/modules/tools.js +12 -1
  45. package/lib/public/modules/vendor-priority.js +14 -0
  46. package/lib/public/modules/vendor-selection.js +20 -0
  47. package/lib/public/modules/worktree-location.js +17 -0
  48. package/lib/public/opencode-avatar.svg +4 -0
  49. package/lib/public/qwen-avatar.svg +4 -0
  50. package/lib/public/style.css +3 -2
  51. package/lib/sdk-bridge.js +59 -19
  52. package/lib/sdk-message-processor.js +10 -1
  53. package/lib/session-notes-mcp-server.js +7 -1
  54. package/lib/worktree.js +9 -4
  55. package/lib/ws-schema.js +3 -0
  56. package/lib/yoke/acp-agent-profiles.js +238 -0
  57. package/lib/yoke/acp-driver-runtime.js +50 -0
  58. package/lib/yoke/acp-event-normalizer.js +179 -0
  59. package/lib/yoke/acp-process-manager.js +264 -0
  60. package/lib/yoke/acp-query-handle.js +487 -0
  61. package/lib/yoke/adapters/acp.js +317 -0
  62. package/lib/yoke/adapters/antigravity.js +417 -0
  63. package/lib/yoke/adapters/codex.js +98 -3
  64. package/lib/yoke/adapters/copilot.js +7 -0
  65. package/lib/yoke/adapters/grok.js +7 -0
  66. package/lib/yoke/adapters/junie.js +7 -0
  67. package/lib/yoke/adapters/kimi.js +7 -0
  68. package/lib/yoke/adapters/kiro.js +4 -4
  69. package/lib/yoke/adapters/opencode.js +7 -0
  70. package/lib/yoke/adapters/qwen.js +7 -0
  71. package/lib/yoke/codex-app-server.js +25 -8
  72. package/lib/yoke/index.js +81 -13
  73. package/lib/yoke/instructions.js +3 -0
  74. package/lib/yoke/interface.js +2 -0
  75. package/lib/yoke/kiro-acp-server.js +30 -276
  76. package/lib/yoke/vendor-registry.js +77 -0
  77. package/package.json +1 -1
@@ -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, accessible: wt.accessible };
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.accessible ? "(accessible)" : "(inaccessible)");
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, accessible: wt.accessible };
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, accessible: true };
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
- if (initialModel) {
171
- sendTo(ws, { type: "model_info", model: initialModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
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 = { attachConnection: attachConnection, dispatchMessageSafely: dispatchMessageSafely };
347
+ module.exports = {
348
+ attachConnection: attachConnection,
349
+ buildInitialModelInfo: buildInitialModelInfo,
350
+ dispatchMessageSafely: dispatchMessageSafely,
351
+ };
@@ -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
- var latest = fileWatchers.get(key);
67
- if (!latest || latest.relPath !== relPath) return;
68
- try {
69
- var stat = fs.statSync(absPath);
70
- var ext = path.extname(absPath).toLowerCase();
71
- if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
72
- var content = fs.readFileSync(absPath, "utf8");
73
- sendFileChanged(client, { type: "fs_file_changed", path: relPath, content: content, size: stat.size });
74
- } catch (e) {
75
- // Atomic saves can briefly remove the destination path between
76
- // rename events. Keep the parent watcher alive for the next event.
77
- if (e.code !== "ENOENT") closeFileWatch(key);
78
- }
108
+ publishFileChanged(key, client, relPath, absPath);
79
109
  }, 200);
80
110
  });
81
- fileWatchers.set(key, { watcher: watcher, relPath: relPath, debounce: null });
111
+ var resolveReady = null;
112
+ var ready = new Promise(function (resolve) { resolveReady = resolve; });
113
+ var entry = {
114
+ watcher: watcher,
115
+ relPath: relPath,
116
+ debounce: null,
117
+ reconcile: null,
118
+ pollTimer: null,
119
+ ready: ready,
120
+ resolveReady: resolveReady,
121
+ hasSnapshot: !!initialSnapshot,
122
+ content: initialSnapshot ? initialSnapshot.content : null,
123
+ size: initialSnapshot ? initialSnapshot.size : null,
124
+ };
125
+ fileWatchers.set(key, entry);
126
+ // Directory events are the low-latency path, but macOS can coalesce or
127
+ // drop them under load. Periodic content reconciliation is the source of
128
+ // truth and also survives atomic replacements with identical metadata.
129
+ entry.pollTimer = setInterval(function () {
130
+ publishFileChanged(key, client, relPath, absPath);
131
+ }, 1000);
132
+ // fs.watch has no readiness event. Reconcile once on the next event-loop
133
+ // turn so a change between the initial read and native watcher activation
134
+ // cannot leave the browser showing stale content.
135
+ entry.reconcile = setImmediate(function () {
136
+ var active = fileWatchers.get(key);
137
+ if (active) active.reconcile = null;
138
+ publishFileChanged(key, client, relPath, absPath);
139
+ if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
140
+ });
82
141
  watcher.on("error", function () { closeFileWatch(key); });
142
+ return ready;
83
143
  } catch (e) {
84
144
  closeFileWatch(key);
145
+ return Promise.resolve(false);
85
146
  }
86
147
  }
87
148
 
@@ -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) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
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,
@@ -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, accessible }
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.worktreeAccessible = worktreeMeta.accessible;
1818
+ status.worktreeExternal = worktreeMeta.external === true;
1804
1819
  }
1805
1820
  if (usersModule.isMultiUser()) {
1806
1821
  var seen = {};