clay-server 4.1.0-beta.16 → 4.1.0-beta.18

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.
@@ -4,9 +4,8 @@
4
4
  // the canvas over the conversation; this pane lists them, splits them into Open
5
5
  // and Closed, and is where a note is completed or brought back.
6
6
  //
7
- // Completion here is Close, never delete. A closed note leaves the active
8
- // canvas and moves to the Closed tab, where it stays readable and reversible.
9
- // There is deliberately no destructive control on this surface at all.
7
+ // Completion here is Close. Permanent deletion is available only for closed
8
+ // notes and always requires an explicit, accessible confirmation.
10
9
 
11
10
  import { store } from './store.js';
12
11
  import { iconHtml, refreshIcons } from './icons.js';
@@ -19,6 +18,10 @@ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } fr
19
18
  var panel = null;
20
19
  var gridEl = null;
21
20
  var tabsEl = null;
21
+ var deleteDialog = null;
22
+ var deleteTrigger = null;
23
+ var deleteRequestCounter = 0;
24
+ var deleteRequestTimer = null;
22
25
 
23
26
  // Registered by app.js so this module never imports the other right-pane tools
24
27
  // and cannot form an import cycle with them.
@@ -63,11 +66,15 @@ function ensurePanel() {
63
66
  '</div>' +
64
67
  '</header>' +
65
68
  '<div id="notes-browser-tabs" class="notes-browser-tabs" role="tablist" aria-label="Sticky Notes state"></div>' +
66
- '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>';
69
+ '<div id="notes-browser-grid" class="notes-browser-grid" role="list"></div>' +
70
+ '<div id="notes-delete-dialog" class="notes-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="notes-delete-heading">' +
71
+ '<div class="notes-delete-dialog-card"><h2 id="notes-delete-heading">Delete this note permanently?</h2><p>This permanently removes the note. It cannot be restored.</p><p class="notes-delete-dialog-title"></p><p class="notes-delete-dialog-status" role="status"></p><div class="notes-delete-dialog-actions"><button type="button" data-action="cancel">Cancel</button><button type="button" class="danger" data-action="delete">Delete permanently</button></div></div>' +
72
+ '</div>';
67
73
  panels.appendChild(panel);
68
74
 
69
75
  gridEl = panel.querySelector("#notes-browser-grid");
70
76
  tabsEl = panel.querySelector("#notes-browser-tabs");
77
+ deleteDialog = panel.querySelector("#notes-delete-dialog");
71
78
 
72
79
  panel.querySelector("#notes-browser-close").addEventListener("click", closeNotesBrowser);
73
80
  panel.querySelector("#notes-browser-wide").addEventListener("click", function () {
@@ -76,6 +83,29 @@ function ensurePanel() {
76
83
  panel.querySelector("#notes-browser-fullscreen").addEventListener("click", function () {
77
84
  applyWindowState(store.get('notesBrowserWide'), !store.get('notesBrowserFullscreen'));
78
85
  });
86
+ deleteDialog.querySelector('[data-action="cancel"]').addEventListener("click", closeDeleteDialog);
87
+ deleteDialog.querySelector('[data-action="delete"]').addEventListener("click", confirmDelete);
88
+ deleteDialog.addEventListener("click", function (event) { if (event.target === deleteDialog) closeDeleteDialog(); });
89
+ deleteDialog.addEventListener("keydown", function (event) {
90
+ if (event.key === "Escape") {
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ closeDeleteDialog(true);
94
+ return;
95
+ }
96
+ if (event.key !== "Tab") return;
97
+ var focusable = deleteDialog.querySelectorAll("button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex='-1'])");
98
+ if (!focusable.length) return;
99
+ var first = focusable[0];
100
+ var last = focusable[focusable.length - 1];
101
+ if (event.shiftKey && document.activeElement === first) {
102
+ event.preventDefault();
103
+ last.focus();
104
+ } else if (!event.shiftKey && document.activeElement === last) {
105
+ event.preventDefault();
106
+ first.focus();
107
+ }
108
+ });
79
109
  refreshIcons();
80
110
  }
81
111
 
@@ -166,7 +196,8 @@ function buildCard(data) {
166
196
  title.textContent = getTitle(data.text) || "Untitled";
167
197
  header.appendChild(title);
168
198
 
169
- // Exactly one lifecycle action per card, and it is never destructive.
199
+ // Closing and reopening are lifecycle actions; permanent deletion is offered
200
+ // separately below only because this card is known to be closed.
170
201
  var action = document.createElement("button");
171
202
  action.type = "button";
172
203
  action.className = "notes-browser-card-action";
@@ -200,6 +231,14 @@ function buildCard(data) {
200
231
  card.appendChild(body);
201
232
 
202
233
  if (closed) {
234
+ var deleteAction = document.createElement("button");
235
+ deleteAction.type = "button";
236
+ deleteAction.className = "notes-browser-card-action notes-browser-card-delete";
237
+ deleteAction.innerHTML = iconHtml("trash-2") + "<span>Delete permanently</span>";
238
+ deleteAction.title = "Permanently delete this closed note";
239
+ deleteAction.setAttribute("aria-label", "Delete permanently: " + title.textContent);
240
+ deleteAction.addEventListener("click", function (e) { e.stopPropagation(); openDeleteDialog(data, deleteAction); });
241
+ header.appendChild(deleteAction);
203
242
  var meta = document.createElement("p");
204
243
  meta.className = "notes-browser-card-meta";
205
244
  meta.textContent = closedMeta(data);
@@ -224,6 +263,77 @@ function buildCard(data) {
224
263
  return card;
225
264
  }
226
265
 
266
+ function nextDeleteRequestId() {
267
+ deleteRequestCounter++;
268
+ return "delete-note-" + Date.now() + "-" + deleteRequestCounter;
269
+ }
270
+
271
+ function accountKey() {
272
+ return store.get('myUserId') ? String(store.get('myUserId')) : "single";
273
+ }
274
+
275
+ function openDeleteDialog(data, trigger) {
276
+ if (!deleteDialog || !data || !isClosed(data)) return;
277
+ deleteTrigger = trigger || null;
278
+ store.set({ notesDeletePending: { noteId: data.id, requestId: null, projectSlug: store.get('currentSlug') || null, accountKey: accountKey() } });
279
+ deleteDialog.querySelector(".notes-delete-dialog-title").textContent = getTitle(data.text) || "Untitled";
280
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "";
281
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
282
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
283
+ deleteDialog.classList.remove("hidden");
284
+ deleteDialog.querySelector('[data-action="cancel"]').focus();
285
+ }
286
+
287
+ function closeDeleteDialog(force) {
288
+ var pending = store.get('notesDeletePending');
289
+ if (!force && pending && pending.requestId) return;
290
+ if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
291
+ if (deleteDialog) deleteDialog.classList.add("hidden");
292
+ store.set({ notesDeletePending: null });
293
+ if (deleteTrigger && deleteTrigger.isConnected) deleteTrigger.focus();
294
+ deleteTrigger = null;
295
+ }
296
+
297
+ function confirmDelete() {
298
+ var pending = store.get('notesDeletePending');
299
+ if (!pending || !deleteDialog) return;
300
+ if (!store.get('connected')) {
301
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Sticky Notes are unavailable while disconnected.";
302
+ return;
303
+ }
304
+ var requestId = nextDeleteRequestId();
305
+ store.set({ notesDeletePending: Object.assign({}, pending, { requestId: requestId }) });
306
+ deleteDialog.querySelector('[data-action="delete"]').disabled = true;
307
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = true;
308
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "Deleting permanently...";
309
+ send({ type: "note_delete_permanently", id: pending.noteId, requestId: requestId });
310
+ deleteRequestTimer = setTimeout(function () {
311
+ var active = store.get('notesDeletePending');
312
+ if (!deleteDialog || !active || active.requestId !== requestId) return;
313
+ store.set({ notesDeletePending: Object.assign({}, active, { requestId: null }) });
314
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
315
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
316
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = "The delete request timed out. Try again.";
317
+ deleteRequestTimer = null;
318
+ }, 10000);
319
+ }
320
+
321
+ export function handleNoteDeleteResult(msg) {
322
+ var pending = store.get('notesDeletePending');
323
+ if (!deleteDialog || !msg || msg.projectSlug !== store.get('currentSlug') ||
324
+ msg.requestId !== (pending && pending.requestId) || !pending || msg.noteId !== pending.noteId ||
325
+ msg.projectSlug !== pending.projectSlug || pending.accountKey !== accountKey()) return;
326
+ if (deleteRequestTimer) { clearTimeout(deleteRequestTimer); deleteRequestTimer = null; }
327
+ if (msg.ok) {
328
+ closeDeleteDialog(true);
329
+ return;
330
+ }
331
+ store.set({ notesDeletePending: Object.assign({}, pending, { requestId: null }) });
332
+ deleteDialog.querySelector('[data-action="delete"]').disabled = false;
333
+ deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
334
+ deleteDialog.querySelector(".notes-delete-dialog-status").textContent = msg.error || "The note could not be deleted.";
335
+ }
336
+
227
337
  function emptyState(tab) {
228
338
  var empty = document.createElement("div");
229
339
  empty.className = "notes-browser-empty";
@@ -242,6 +352,11 @@ function emptyState(tab) {
242
352
  export function renderNotesBrowser() {
243
353
  if (!gridEl) return;
244
354
  var all = listNoteData();
355
+ var pending = store.get('notesDeletePending');
356
+ if (pending) {
357
+ var pendingCurrent = all.filter(function (item) { return item && item.id === pending.noteId; })[0];
358
+ if (!pendingCurrent || !isClosed(pendingCurrent)) closeDeleteDialog(true);
359
+ }
245
360
  var open = openNotes(all);
246
361
  var closed = closedNotes(all);
247
362
  renderTabs(open.length, closed.length);
@@ -284,6 +399,9 @@ export function openNotesBrowser() {
284
399
  }
285
400
 
286
401
  export function closeNotesBrowser() {
402
+ // Reset destructive UI state even when the panel was already closed. Right
403
+ // workbench/project transitions can race with panel visibility updates.
404
+ closeDeleteDialog(true);
287
405
  releaseRightWorkbench("notes-browser");
288
406
  if (!store.get('notesBrowserOpen')) return;
289
407
  if (panel) panel.classList.add("hidden");
@@ -304,10 +422,13 @@ export function initNotesBrowser() {
304
422
  registerRightWorkbench("notes-browser", closeNotesBrowser);
305
423
  document.addEventListener("keydown", function (e) {
306
424
  if (e.key !== "Escape") return;
425
+ if (deleteDialog && !deleteDialog.classList.contains("hidden")) return;
307
426
  if (!store.get('notesBrowserOpen')) return;
308
427
  closeNotesBrowser();
309
428
  });
310
429
  store.subscribe(function (state, previous) {
430
+ if (state.connected !== previous.connected || state.myUserId !== previous.myUserId ||
431
+ state.isMultiUserMode !== previous.isMultiUserMode) closeDeleteDialog(true);
311
432
  if (state.currentSlug === previous.currentSlug) return;
312
433
  // A different project starts from a closed, bounded, Open-tab pane.
313
434
  closeNotesBrowser();
@@ -424,13 +424,12 @@ export function handleNoteWritten(msg) {
424
424
  // an older daemon on the other end of the socket still must not corrupt state.
425
425
  export function handleNoteDeleted(msg) {
426
426
  var entry = notes.get(msg.id);
427
- if (!entry) return;
428
- entry.el.remove();
427
+ if (entry) entry.el.remove();
428
+ clearNoteTimers(msg.id);
429
429
  notes.delete(msg.id);
430
430
  updateBadge();
431
431
  updateSidebarBadge();
432
432
 
433
- clearNoteTimers(msg.id);
434
433
  refreshBrowser();
435
434
  }
436
435
 
package/lib/sdk-bridge.js CHANGED
@@ -7,6 +7,7 @@ var usersModule = require("./users");
7
7
  var { getCodexConfig } = require("./codex-defaults");
8
8
  var { getKiroConfig } = require("./kiro-defaults");
9
9
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
10
+ var skillDiscovery = require("./yoke/skill-discovery");
10
11
  var { isSafeBashSegment } = require("./safe-bash-commands");
11
12
  var { createMessageQueue } = require("./sdk-message-queue");
12
13
  var { attachMessageProcessor } = require("./sdk-message-processor");
@@ -162,6 +163,22 @@ function createSDKBridge(opts) {
162
163
  }];
163
164
  }
164
165
 
166
+ function getCodexSessionMcpServer(session, queryGeneration) {
167
+ if (!session || !Number.isInteger(queryGeneration)) return null;
168
+ var bridgeArgs = [
169
+ path.join(__dirname, "yoke", "mcp-bridge-server.js"),
170
+ "--port", String(clayPort),
171
+ "--slug", slug,
172
+ "--session", String(session.localId),
173
+ "--query-generation", String(queryGeneration),
174
+ "--session-only",
175
+ ];
176
+ if (clayTls) bridgeArgs.push("--tls");
177
+ var bridgeEnv = {};
178
+ if (clayAuthToken) bridgeEnv.CLAY_AUTH_TOKEN = clayAuthToken;
179
+ return { name: "clay-session-tools", command: process.execPath, args: bridgeArgs, env: bridgeEnv };
180
+ }
181
+
165
182
  function getFreshAuthState(force) {
166
183
  var now = Date.now();
167
184
  if (!force && _cachedFreshAuthState && now - _cachedFreshAuthAt < 15000) {
@@ -338,7 +355,7 @@ function createSDKBridge(opts) {
338
355
  }
339
356
 
340
357
  // --- Skill discovery (extracted to sdk-skill-discovery.js) ---
341
- var skills = attachSkillDiscovery({ cwd: cwd });
358
+ var skills = attachSkillDiscovery({ cwd: cwd, getOptions: function () { return { env: getRuntimeEnv() }; } });
342
359
  var discoverSkillDirs = skills.discoverSkillDirs;
343
360
  var mergeSkills = skills.mergeSkills;
344
361
 
@@ -351,6 +368,19 @@ function createSDKBridge(opts) {
351
368
  return owner && owner.linuxUser ? owner.linuxUser : null;
352
369
  }
353
370
 
371
+ function getSessionSkillOptions(session) {
372
+ var runtimeEnv = getRuntimeEnv(session || {});
373
+ if (!osUsers) return { env: runtimeEnv };
374
+ var linuxUser = getSessionLinuxUser(session);
375
+ if (!linuxUser) return { disable: true };
376
+ try {
377
+ var info = require("./os-users").resolveOsUserInfo(linuxUser);
378
+ return Object.assign({}, skillDiscovery.optionsForOsUser(info), { env: runtimeEnv });
379
+ } catch (e) {
380
+ return { disable: true };
381
+ }
382
+ }
383
+
354
384
  var titleGenerator = sessionTitleGenerator.attachSessionTitleGenerator({
355
385
  sm: sm,
356
386
  cwd: cwd,
@@ -372,6 +402,7 @@ function createSDKBridge(opts) {
372
402
  adapter: adapter,
373
403
  saveImageFile: opts.saveImageFile,
374
404
  getSessionLinuxUser: getSessionLinuxUser,
405
+ getSessionSkillOptions: getSessionSkillOptions,
375
406
  onProcessingChanged: onProcessingChanged,
376
407
  onTurnDone: onTurnDone,
377
408
  onAutoTitle: function (session) { titleGenerator.generate(session); },
@@ -1004,7 +1035,8 @@ function createSDKBridge(opts) {
1004
1035
  && Number(session._sdkQueryGeneration || 0) === codexCatalogCandidate.generation
1005
1036
  && session.cliSessionId === msg.sessionId
1006
1037
  && (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session)) {
1007
- session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
1038
+ if (codexCatalogCandidate.version) session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
1039
+ if (codexCatalogCandidate.issuesVersion) session.codexIssuesToolCatalogVersion = codexCatalogCandidate.issuesVersion;
1008
1040
  sm.saveSessionFile(session);
1009
1041
  codexCatalogCandidate = null;
1010
1042
  }
@@ -1574,7 +1606,11 @@ function createSDKBridge(opts) {
1574
1606
  if (linuxUser) {
1575
1607
  claudeOpts.linuxUser = linuxUser;
1576
1608
  claudeOpts.singleTurn = !!session.singleTurn;
1577
- claudeOpts.originalHome = require("./config").REAL_HOME || null;
1609
+ try {
1610
+ claudeOpts.originalHome = require("./os-users").resolveOsUserInfo(linuxUser).home;
1611
+ } catch (e) {
1612
+ claudeOpts.originalHome = null;
1613
+ }
1578
1614
  claudeOpts.projectPath = session.cwd || null;
1579
1615
  claudeOpts._perfT0 = t0;
1580
1616
  // Pass previous worker state for reuse
@@ -1616,6 +1652,17 @@ function createSDKBridge(opts) {
1616
1652
  // delivered by this provider.
1617
1653
  var sessionToolDefs = getSessionToolDefs(session) || [];
1618
1654
  var queryGeneration = Number(session._sdkQueryGeneration || 0);
1655
+ var hasIssueCatalog = false;
1656
+ for (var issueToolIndex = 0; issueToolIndex < sessionToolDefs.length; issueToolIndex++) {
1657
+ if (sessionToolDefs[issueToolIndex].name === "create_issue") {
1658
+ hasIssueCatalog = true;
1659
+ break;
1660
+ }
1661
+ }
1662
+ var recoverCodexIssueTools = sessionAdapter.vendor === "codex"
1663
+ && !!session.cliSessionId
1664
+ && hasIssueCatalog
1665
+ && Number(session.codexIssuesToolCatalogVersion || 0) < 1;
1619
1666
  function queryContextAvailable() {
1620
1667
  if (Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
1621
1668
  if (sm && sm.sessions && typeof sm.sessions.get === "function" && sm.sessions.get(session.localId) !== session) return false;
@@ -1750,6 +1797,7 @@ function createSDKBridge(opts) {
1750
1797
  cwd: cwd,
1751
1798
  linuxUser: linuxUser || undefined,
1752
1799
  env: sessionRuntimeEnv,
1800
+ skillOptions: getSessionSkillOptions(session),
1753
1801
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1754
1802
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1755
1803
  // would strip the driver session down to the two-sentence pair preset.
@@ -1771,6 +1819,9 @@ function createSDKBridge(opts) {
1771
1819
  toolServers: mergedMcpServers,
1772
1820
  toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1773
1821
  dynamicTools: sessionToolDefs,
1822
+ // Codex persists dynamic tools with a thread. A stale authorized Driver
1823
+ // gets this exact-query bridge; its handlers still reauthorize live.
1824
+ sessionMcpServer: recoverCodexIssueTools ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
1774
1825
  callDynamicTool: function (toolName, args) {
1775
1826
  var toolDef = findSessionToolDef(toolName);
1776
1827
  if (toolDef && typeof toolDef.handler === "function") {
@@ -1957,6 +2008,12 @@ function createSDKBridge(opts) {
1957
2008
  if (completeInterviewCatalog || requiredCatalogTools.report_scheduled_task_outcome) {
1958
2009
  codexCatalogCandidate = { generation: queryGeneration, version: yoke.sessionTools.CODEX_CATALOG_VERSION };
1959
2010
  }
2011
+ if (hasIssueCatalog) {
2012
+ codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, {
2013
+ generation: queryGeneration,
2014
+ issuesVersion: 1,
2015
+ });
2016
+ }
1960
2017
  }
1961
2018
  session.streamPromise = processQueryStream(session, codexCatalogCandidate).catch(function(err) {
1962
2019
  });
@@ -2199,22 +2256,9 @@ function createSDKBridge(opts) {
2199
2256
  slug: slug,
2200
2257
  });
2201
2258
 
2202
- var fsSkills = discoverSkillDirs();
2203
- sm.skillNames = mergeSkills(result.skills, fsSkills);
2204
- if (result.slashCommands) {
2205
- var seen = new Set();
2206
- var combined = [];
2207
- var all = result.slashCommands.concat(Array.from(sm.skillNames));
2208
- for (var k = 0; k < all.length; k++) {
2209
- if (!seen.has(all[k])) {
2210
- seen.add(all[k]);
2211
- combined.push(all[k]);
2212
- }
2213
- }
2214
- sm.slashCommands = combined;
2215
- sm.setSlashCommandsForVendor(defaultVendor, combined);
2216
- send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
2217
- }
2259
+ // Skill and slash-command catalogs are session-owned. The warmup has
2260
+ // no authenticated session target, so never publish its personal
2261
+ // inventory through the project-wide session manager or broadcast.
2218
2262
  if (result.defaultModel) {
2219
2263
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2220
2264
  sm.defaultModelByVendor[defaultVendor] = sm.defaultModelByVendor[defaultVendor] || result.defaultModel;
@@ -28,6 +28,7 @@ function attachMessageProcessor(ctx) {
28
28
  var mergeSkills = ctx.mergeSkills;
29
29
  var saveImageFile = ctx.saveImageFile;
30
30
  var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
31
+ var getSessionSkillOptions = ctx.getSessionSkillOptions || function () { return {}; };
31
32
 
32
33
  function getMateIdForNotification() {
33
34
  if (!isMate) return null;
@@ -208,21 +209,26 @@ function attachMessageProcessor(ctx) {
208
209
  session.activeBackgroundTasks = [];
209
210
  sendToSession(session, { type: "active_background_tasks", tasks: [] });
210
211
  if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
211
- var fsSkills = discoverSkillDirs();
212
- sm.skillNames = mergeSkills(parsed.skills, fsSkills);
212
+ var fsSkills = discoverSkillDirs(getSessionSkillOptions(session));
213
+ session.skillNames = Array.from(mergeSkills(parsed.skills, fsSkills));
213
214
  if (parsed.slashCommands) {
214
215
  // Union: SDK slash_commands + merged skills (deduplicated)
216
+ session.vendorSlashCommands = parsed.slashCommands.filter(function (name) {
217
+ return session.skillNames.indexOf(name) === -1;
218
+ });
215
219
  var seen = new Set();
216
220
  var combined = [];
217
- var all = parsed.slashCommands.concat(Array.from(sm.skillNames));
221
+ var all = session.vendorSlashCommands.concat(session.skillNames);
218
222
  for (var k = 0; k < all.length; k++) {
219
223
  if (!seen.has(all[k])) {
220
224
  seen.add(all[k]);
221
225
  combined.push(all[k]);
222
226
  }
223
227
  }
224
- sm.slashCommands = combined;
225
- send({ type: "slash_commands", commands: sm.slashCommands });
228
+ session.slashCommands = combined;
229
+ session.slashCommandsByVendor = session.slashCommandsByVendor || {};
230
+ session.slashCommandsByVendor[session.vendor || (adapter && adapter.vendor) || "claude"] = combined;
231
+ sendToSession(session, { type: "slash_commands", commands: combined, vendor: session.vendor || (adapter && adapter.vendor) || "claude" });
226
232
  }
227
233
  if (parsed.model) {
228
234
  var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
@@ -899,15 +905,20 @@ function attachMessageProcessor(ctx) {
899
905
  // init) and push a full replacement to the client.
900
906
  var cmdSeen = new Set();
901
907
  var cmdCombined = [];
902
- var cmdAll = (parsed.commandNames || []).concat(Array.from(sm.skillNames || []));
908
+ session.vendorSlashCommands = (parsed.commandNames || []).filter(function (name) {
909
+ return (session.skillNames || []).indexOf(name) === -1;
910
+ });
911
+ var cmdAll = session.vendorSlashCommands.concat(session.skillNames || []);
903
912
  for (var ci = 0; ci < cmdAll.length; ci++) {
904
913
  if (!cmdSeen.has(cmdAll[ci])) {
905
914
  cmdSeen.add(cmdAll[ci]);
906
915
  cmdCombined.push(cmdAll[ci]);
907
916
  }
908
917
  }
909
- sm.slashCommands = cmdCombined;
910
- send({ type: "slash_commands", commands: sm.slashCommands });
918
+ session.slashCommands = cmdCombined;
919
+ session.slashCommandsByVendor = session.slashCommandsByVendor || {};
920
+ session.slashCommandsByVendor[session.vendor || (adapter && adapter.vendor) || "claude"] = cmdCombined;
921
+ sendToSession(session, { type: "slash_commands", commands: cmdCombined, vendor: session.vendor || (adapter && adapter.vendor) || "claude" });
911
922
 
912
923
  } else if (parsed.yokeType === "worker_shutting_down") {
913
924
  // Surface non-routine teardown reasons; "host_exit" is the normal close.
@@ -1,5 +1,4 @@
1
- var fs = require("fs");
2
- var path = require("path");
1
+ var skillDiscovery = require("./yoke/skill-discovery");
3
2
 
4
3
  // Split shell command on operators (&&, ||, ;, |) while respecting quotes
5
4
  // and parentheses. Returns array of command segments.
@@ -79,34 +78,13 @@ function splitShellSegments(cmd) {
79
78
 
80
79
  function attachSkillDiscovery(ctx) {
81
80
  var cwd = ctx.cwd;
81
+ var getOptions = ctx.getOptions || function () { return {}; };
82
82
 
83
- function discoverSkillDirs() {
83
+ function discoverSkillDirs(options) {
84
+ var discovered = skillDiscovery.discoverSkills(cwd, options || getOptions());
84
85
  var skills = {};
85
- var dirs = [
86
- path.join(require("./config").REAL_HOME, ".claude", "skills"),
87
- path.join(cwd, ".claude", "skills"),
88
- ];
89
- for (var d = 0; d < dirs.length; d++) {
90
- var base = dirs[d];
91
- var entries;
92
- try {
93
- entries = fs.readdirSync(base, { withFileTypes: true });
94
- } catch (e) {
95
- continue; // directory doesn't exist
96
- }
97
- for (var i = 0; i < entries.length; i++) {
98
- var entry = entries[i];
99
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
100
- var skillDir = path.join(base, entry.name);
101
- var skillMd = path.join(skillDir, "SKILL.md");
102
- try {
103
- fs.accessSync(skillMd, fs.constants.R_OK);
104
- // project skills override global skills with same name
105
- skills[entry.name] = skillDir;
106
- } catch (e) {
107
- // no SKILL.md, skip
108
- }
109
- }
86
+ for (var i = 0; i < discovered.length; i++) {
87
+ skills[discovered[i].name] = discovered[i].path.replace(/\/SKILL\.md$/, "");
110
88
  }
111
89
  return skills;
112
90
  }
package/lib/sessions.js CHANGED
@@ -24,6 +24,7 @@ function createSessionManager(opts) {
24
24
  var sessionViewedListeners = [];
25
25
  var onSessionRenamed = opts.onSessionRenamed || function () {};
26
26
  var onSessionIdentityAssigned = opts.onSessionIdentityAssigned || function () {};
27
+ var onSessionHydrate = opts.onSessionHydrate || null;
27
28
 
28
29
  // --- Multi-session state ---
29
30
  var nextLocalId = 1;
@@ -162,6 +163,7 @@ function createSessionManager(opts) {
162
163
  if (session.ownerId) metaObj.ownerId = session.ownerId;
163
164
  if (session.vendor) metaObj.vendor = session.vendor;
164
165
  if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
166
+ if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
165
167
  if (session.model) metaObj.model = session.model;
166
168
  if (session.effort) metaObj.effort = session.effort;
167
169
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -317,6 +319,7 @@ function createSessionManager(opts) {
317
319
  };
318
320
  if (m.vendor) session.vendor = m.vendor;
319
321
  session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
322
+ session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
320
323
  session.model = m.model || deriveSessionModel(loaded[i].history);
321
324
  session.effort = m.effort || null;
322
325
  if (m.loop) session.loop = m.loop;
@@ -797,6 +800,7 @@ function createSessionManager(opts) {
797
800
  function switchSession(localId, targetWs, transform) {
798
801
  var session = sessions.get(localId);
799
802
  if (!session) return;
803
+ if (typeof onSessionHydrate === "function") onSessionHydrate(session);
800
804
 
801
805
  // Pane sockets (split-view iframes) pin their session via switch_session
802
806
  // but must not move the project-global active pointer: reloads and
@@ -824,7 +828,9 @@ function createSessionManager(opts) {
824
828
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
825
829
  _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
826
830
  // Send vendor-specific slash commands
827
- var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
831
+ var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
832
+ || session.slashCommands
833
+ || [];
828
834
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
829
835
  broadcastSessionList();
830
836
  replayHistory(session, undefined, targetWs, transform);
@@ -1288,6 +1294,7 @@ function createSessionManager(opts) {
1288
1294
  }
1289
1295
 
1290
1296
  return {
1297
+ setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
1291
1298
  get activeSessionId() { return activeSessionId; },
1292
1299
  get nextLocalId() { return nextLocalId; },
1293
1300
  get slashCommands() { return slashCommands; },
@@ -44,6 +44,7 @@ function createAcpAdapter(vendor, opts) {
44
44
  var defaultModel = driver.defaultModel || cachedModels[0] || "auto";
45
45
  var activeHandles = [];
46
46
  var lastActiveAt = Date.now();
47
+ var skillEnv = opts.env || process.env;
47
48
 
48
49
  function context(extra) {
49
50
  return Object.assign({
@@ -96,7 +97,7 @@ function createAcpAdapter(vendor, opts) {
96
97
  }
97
98
 
98
99
  function readyResult() {
99
- var skills = skillDiscovery.discoverSkills(cwd).map(function(skill) { return skill.name; });
100
+ var skills = skillDiscovery.discoverSkills(cwd, { env: skillEnv }).map(function(skill) { return skill.name; });
100
101
  var result = {
101
102
  models: cachedModels.slice(),
102
103
  defaultModel: defaultModel,
@@ -220,7 +221,7 @@ function createAcpAdapter(vendor, opts) {
220
221
  if (!initialized || !acp || !acp.started) await adapter.init(queryOpts);
221
222
  if (shuttingDown) throw new Error(driver.displayName + " adapter is shutting down");
222
223
  var controller = queryOpts.abortController || new AbortController();
223
- var sharedSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd || cwd);
224
+ var sharedSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd || cwd, queryOpts.skillOptions || { env: queryOpts.env || skillEnv });
224
225
  var skillIndex = skillDiscovery.buildSkillIndex(sharedSkills);
225
226
  var acpOptions = (queryOpts.adapterOptions && queryOpts.adapterOptions.ACP) || {};
226
227
  var handle = null;
@@ -353,12 +353,12 @@ function createAntigravityAdapter(opts) {
353
353
 
354
354
  var adapter = {
355
355
  vendor: "antigravity",
356
- init: async function() {
356
+ init: async function(initOpts) {
357
357
  if (!binaryPath) binaryPath = findBinary();
358
358
  if (!binaryPath) throw new Error("Antigravity CLI binary not found: agy");
359
359
  var fetched = opts._fetchModels ? await opts._fetchModels(binaryPath, cwd) : await fetchModels(binaryPath, cwd);
360
360
  if (fetched && fetched.length) cachedModels = fetched;
361
- var skills = skillDiscovery.discoverSkills(cwd).map(function(skill) { return skill.name; });
361
+ var skills = skillDiscovery.discoverSkills(cwd, { env: (initOpts && initOpts.env) || null }).map(function(skill) { return skill.name; });
362
362
  return {
363
363
  models: cachedModels.slice(),
364
364
  defaultModel: cachedModels[0] || "auto",