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.
@@ -67,10 +67,28 @@ function applySystemPromptSnapshot(systemPrompt, enabled) {
67
67
  return systemPrompt;
68
68
  }
69
69
 
70
- function getSharedSkillPlugins(homeDir, existingPlugins) {
70
+ function getSharedSkillPlugins(homeDir, existingPlugins, cwd, env) {
71
71
  var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
72
- var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(homeDir ? { homeDir: homeDir } : null);
72
+ // undefined means normal in-process Claude and may use the configured
73
+ // daemon home; null means a failed mapped-user resolution and must fail
74
+ // closed without constructing any personal bridges.
75
+ if (homeDir === null) return plugins;
76
+ var effectiveHome = homeDir === undefined ? require("../../config").REAL_HOME || os.homedir() : homeDir;
77
+ var rootOptions = { homeDir: effectiveHome, env: env, scopedHome: !!homeDir };
78
+ var roots = skillDiscovery.getSkillRoots(cwd, rootOptions);
79
+ var codexRoot = roots.filter(function (root) { return root.source === "codex-user"; })[0];
80
+ var codexOpts = codexRoot ? { homeDir: effectiveHome, source: codexRoot.path, name: "codex" } : null;
81
+ if (codexRoot && codexRoot.path !== path.join(effectiveHome, ".codex", "skills")) {
82
+ codexOpts.name = "codex-" + crypto.createHash("sha256").update(codexRoot.path).digest("hex").slice(0, 12);
83
+ }
84
+ var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(codexOpts);
73
85
  if (bridgePath) plugins.push({ type: "local", path: bridgePath, skipMcpDiscovery: true });
86
+ for (var i = 0; i < roots.length; i++) {
87
+ if (roots[i].source === "codex-user" || roots[i].source === "claude-user" || roots[i].source === "claude-project") continue;
88
+ var rootId = crypto.createHash("sha256").update(roots[i].source + "\0" + path.resolve(roots[i].path)).digest("hex").slice(0, 12);
89
+ var sharedPath = skillDiscovery.ensureClaudeCodexPlugin({ homeDir: effectiveHome, source: roots[i].path, name: "shared-" + roots[i].source + "-" + rootId });
90
+ if (sharedPath) plugins.push({ type: "local", path: sharedPath, skipMcpDiscovery: true });
91
+ }
74
92
  return plugins;
75
93
  }
76
94
 
@@ -99,7 +117,7 @@ function buildWorkerQueryOptions(queryOpts, claudeOpts, workerCwd) {
99
117
  queryOpts.systemPromptSnapshot
100
118
  );
101
119
  }
102
- queryOptions.plugins = queryOpts.skipSkills ? (claudeOpts.plugins || []) : getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins);
120
+ queryOptions.plugins = queryOpts.skipSkills ? (claudeOpts.plugins || []) : getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins, workerCwd, queryOpts.env);
103
121
  if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
104
122
  if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
105
123
  queryOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
@@ -1423,7 +1441,7 @@ function createClaudeAdapter(opts) {
1423
1441
  abortController: ac,
1424
1442
  settings: { disableAllHooks: true },
1425
1443
  };
1426
- warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
1444
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome, null, initOpts && initOpts.cwd, initOpts && initOpts.env);
1427
1445
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1428
1446
 
1429
1447
  if (initOpts && initOpts.dangerouslySkipPermissions) {
@@ -1637,7 +1655,7 @@ function createClaudeAdapter(opts) {
1637
1655
  if (co.allowDangerouslySkipPermissions) sdkOptions.allowDangerouslySkipPermissions = true;
1638
1656
  if (co.resumeSessionAt) sdkOptions.resumeSessionAt = co.resumeSessionAt;
1639
1657
  if (co.settings) sdkOptions.settings = co.settings;
1640
- sdkOptions.plugins = queryOpts.skipSkills ? (co.plugins || []) : getSharedSkillPlugins(co.originalHome, co.plugins);
1658
+ sdkOptions.plugins = queryOpts.skipSkills ? (co.plugins || []) : getSharedSkillPlugins(co.originalHome, co.plugins, queryOpts.cwd || _cwd, queryOpts.env);
1641
1659
 
1642
1660
  claudeModelSwitch.attach(sdkOptions, queryOpts.onModelSwitch);
1643
1661
  var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
@@ -1897,7 +1915,7 @@ function createClaudeAdapter(opts) {
1897
1915
  abortController: ac,
1898
1916
  settings: { disableAllHooks: true },
1899
1917
  };
1900
- warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
1918
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome, null, initOpts && initOpts.cwd, initOpts && initOpts.env);
1901
1919
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1902
1920
 
1903
1921
  if (initOpts && initOpts.dangerouslySkipPermissions) {
@@ -1979,7 +1997,7 @@ function createClaudeAdapter(opts) {
1979
1997
  }
1980
1998
 
1981
1999
  var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"], settings: { disableAllHooks: true } };
1982
- warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
2000
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome, null, initOpts && initOpts.cwd, initOpts && initOpts.env);
1983
2001
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1984
2002
  if (initOpts && initOpts.dangerouslySkipPermissions) {
1985
2003
  warmupOptions.permissionMode = "bypassPermissions";
@@ -29,7 +29,7 @@ var CODEX_REASONING_DEFAULTS = {
29
29
  model_supports_reasoning_summaries: true,
30
30
  };
31
31
  var DEFAULT_CODEX_MODEL = "gpt-6-astra";
32
- var CODEX_QUERY_BOUND_TOOL_CAPABILITY = { mode: "dynamic", resume: "persisted-catalog-only" };
32
+ var CODEX_QUERY_BOUND_TOOL_CAPABILITY = { mode: "dynamic", resume: "session-mcp-recovery" };
33
33
  var CODEX_NOTE_TOOLS = {
34
34
  list_notes: true,
35
35
  write_note: true,
@@ -1163,16 +1163,25 @@ function createCodexQueryHandle(appServer, queryOpts) {
1163
1163
 
1164
1164
  var threadResult;
1165
1165
  if (queryOpts.resumeSessionId) {
1166
- // ThreadResumeParams in the installed app-server schema has no
1167
- // dynamicTools field. New tools therefore require a new Driver/Worker
1168
- // thread; resumed threads can only refresh handlers for persisted tools.
1169
- threadResult = await appServer.send("thread/resume", {
1166
+ // ThreadResumeParams has no dynamicTools field, but its supported
1167
+ // config override can start an exact-query MCP bridge without making
1168
+ // a replacement thread or changing the saved conversation history.
1169
+ var resumeParams = {
1170
1170
  threadId: queryOpts.resumeSessionId,
1171
1171
  model: threadParams.model,
1172
1172
  sandbox: threadParams.sandbox,
1173
1173
  approvalPolicy: threadParams.approvalPolicy,
1174
1174
  cwd: threadParams.cwd,
1175
- }, 60000);
1175
+ };
1176
+ if (queryOpts.sessionMcpServer && !queryOpts.isolatedRecovery) {
1177
+ resumeParams.config = { mcp_servers: {} };
1178
+ resumeParams.config.mcp_servers[queryOpts.sessionMcpServer.name] = {
1179
+ command: queryOpts.sessionMcpServer.command,
1180
+ args: queryOpts.sessionMcpServer.args || [],
1181
+ env: queryOpts.sessionMcpServer.env || {},
1182
+ };
1183
+ }
1184
+ threadResult = await appServer.send("thread/resume", resumeParams, 60000);
1176
1185
  } else {
1177
1186
  threadResult = await appServer.send("thread/start", threadParams, 60000);
1178
1187
  }
@@ -1213,7 +1222,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
1213
1222
 
1214
1223
  // Explicit references use Codex's native skill input item regardless of
1215
1224
  // which vendor owns the SKILL.md file.
1216
- var availableSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd);
1225
+ var availableSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd, queryOpts.skillOptions || { env: queryOpts.env });
1217
1226
  var skillItemsToInject = [];
1218
1227
  var injected = {};
1219
1228
  for (var ii = 0; ii < input.length; ii++) {
@@ -1427,11 +1436,25 @@ function createCodexAdapter(opts) {
1427
1436
  for (var i = 0; i < listeners.length; i++) listeners[i]();
1428
1437
  }
1429
1438
  var _initPromise = null;
1439
+ var _initializing = false;
1430
1440
  var _shutdownPromise = null;
1431
1441
  var _refCount = 0;
1432
1442
  var _lastActiveAt = Date.now();
1433
1443
  var _shuttingDown = false;
1434
1444
  var _activeQueries = [];
1445
+ var _recoveryHandoff = null;
1446
+ var _recoveryRuntimes = [];
1447
+ var _recoveryCleanup = Promise.resolve();
1448
+ var _recoveryCleanupTasks = [];
1449
+
1450
+ function scopedSkillOptions(env) {
1451
+ if (!_runtimeLinuxUser) return { env: env };
1452
+ try {
1453
+ return Object.assign(skillDiscovery.optionsForOsUser(_resolveOsUserInfo(_runtimeLinuxUser)), { env: env });
1454
+ } catch (e) {
1455
+ return { disable: true };
1456
+ }
1457
+ }
1435
1458
 
1436
1459
  function withoutLinuxUser(callOpts) {
1437
1460
  var cleaned = Object.assign({}, callOpts || {});
@@ -1498,6 +1521,95 @@ function createCodexAdapter(opts) {
1498
1521
  updateLastActiveAt();
1499
1522
  }
1500
1523
 
1524
+ function waitForRecoveryHandoff() {
1525
+ return _recoveryHandoff || Promise.resolve();
1526
+ }
1527
+
1528
+ function retainRecoveryRuntime(runtime) {
1529
+ _recoveryRuntimes.push(runtime);
1530
+ }
1531
+
1532
+ function removeRecoveryRuntime(runtime) {
1533
+ var next = [];
1534
+ for (var i = 0; i < _recoveryRuntimes.length; i++) {
1535
+ if (_recoveryRuntimes[i] !== runtime) next.push(_recoveryRuntimes[i]);
1536
+ }
1537
+ _recoveryRuntimes = next;
1538
+ }
1539
+
1540
+ function cleanupRecoveryRuntime(runtime) {
1541
+ for (var i = 0; i < _recoveryCleanupTasks.length; i++) {
1542
+ if (_recoveryCleanupTasks[i].runtime === runtime) return _recoveryCleanupTasks[i].promise;
1543
+ }
1544
+ var task = { runtime: runtime, promise: null };
1545
+ _recoveryCleanupTasks.push(task);
1546
+ var priorCleanup = _recoveryCleanup;
1547
+ task.promise = Promise.resolve(priorCleanup).catch(function() {
1548
+ return null;
1549
+ }).then(function() {
1550
+ return runtime.shutdown();
1551
+ }).catch(function(error) {
1552
+ console.error("[yoke/codex] Isolated recovery runtime shutdown failed:", error.message || error);
1553
+ }).then(function() {
1554
+ removeRecoveryRuntime(runtime);
1555
+ var remainingTasks = [];
1556
+ for (var ti = 0; ti < _recoveryCleanupTasks.length; ti++) {
1557
+ if (_recoveryCleanupTasks[ti] !== task) remainingTasks.push(_recoveryCleanupTasks[ti]);
1558
+ }
1559
+ _recoveryCleanupTasks = remainingTasks;
1560
+ });
1561
+ _recoveryCleanup = task.promise;
1562
+ return task.promise;
1563
+ }
1564
+
1565
+ function shutdownRecoveryRuntimes() {
1566
+ var runtimes = _recoveryRuntimes.slice();
1567
+ if (!runtimes.length) return _recoveryCleanup;
1568
+ return Promise.all(runtimes.map(function(runtime) {
1569
+ return cleanupRecoveryRuntime(runtime);
1570
+ })).then(function() {
1571
+ _recoveryRuntimes = [];
1572
+ });
1573
+ }
1574
+
1575
+ async function createIsolatedRecoveryQuery(queryOpts) {
1576
+ var releaseHandoff;
1577
+ var priorHandoff = _recoveryHandoff;
1578
+ var myHandoff = new Promise(function(resolve) { releaseHandoff = resolve; });
1579
+ _recoveryHandoff = myHandoff;
1580
+ await (priorHandoff || Promise.resolve());
1581
+ try {
1582
+ await _recoveryCleanup;
1583
+ if (_refCount > 0 || _initializing) {
1584
+ throw new Error("Codex resumed-tool recovery is waiting for an active shared runtime; retry after its current queries finish.");
1585
+ }
1586
+ if (_appServer) {
1587
+ var stopped = await adapter.shutdownIfIdle(0);
1588
+ if (!stopped) throw new Error("Codex resumed-tool recovery could not safely release the shared runtime; retry after its current queries finish.");
1589
+ }
1590
+ var recoveryRuntime = createCodexAdapter(Object.assign({}, _defaultInitOpts, {
1591
+ recoveryRuntime: true,
1592
+ recoverySessionMcpServer: queryOpts.sessionMcpServer,
1593
+ }));
1594
+ retainRecoveryRuntime(recoveryRuntime);
1595
+ var recoveryOpts = Object.assign({}, queryOpts, {
1596
+ isolatedRecovery: true,
1597
+ onRuntimeFinished: function() {
1598
+ return cleanupRecoveryRuntime(recoveryRuntime);
1599
+ },
1600
+ });
1601
+ try {
1602
+ return await recoveryRuntime.createQuery(recoveryOpts);
1603
+ } catch (error) {
1604
+ await cleanupRecoveryRuntime(recoveryRuntime);
1605
+ throw error;
1606
+ }
1607
+ } finally {
1608
+ releaseHandoff();
1609
+ if (_recoveryHandoff === myHandoff) _recoveryHandoff = null;
1610
+ }
1611
+ }
1612
+
1501
1613
  function buildReadyResponse(skillNames) {
1502
1614
  return {
1503
1615
  models: _cachedModels,
@@ -1530,6 +1642,7 @@ function createCodexAdapter(opts) {
1530
1642
  function clearRuntimeState() {
1531
1643
  _appServer = null;
1532
1644
  _initPromise = null;
1645
+ _initializing = false;
1533
1646
  _cachedModels = [];
1534
1647
  _refCount = 0;
1535
1648
  _activeQueries = [];
@@ -1584,7 +1697,7 @@ function createCodexAdapter(opts) {
1584
1697
  var deadline = Date.now() + 5000;
1585
1698
  var shouldAbort = !!force;
1586
1699
 
1587
- if (_initPromise) {
1700
+ if (_initializing && _initPromise) {
1588
1701
  try {
1589
1702
  await Promise.race([
1590
1703
  _initPromise.catch(function() { return null; }),
@@ -1649,9 +1762,10 @@ function createCodexAdapter(opts) {
1649
1762
  }
1650
1763
 
1651
1764
  // Deduplicate concurrent init calls
1652
- if (_initPromise) return _initPromise;
1765
+ if (_initializing && _initPromise) return _initPromise;
1653
1766
 
1654
- _initPromise = (async function() {
1767
+ _initializing = true;
1768
+ var initPromise = (async function() {
1655
1769
  var serverOpts = { cwd: _cwd, env: effectiveInitOpts.env || null };
1656
1770
  if (_runtimeLinuxUser) {
1657
1771
  serverOpts.osUserInfo = _resolveOsUserInfo(_runtimeLinuxUser);
@@ -1671,7 +1785,7 @@ function createCodexAdapter(opts) {
1671
1785
 
1672
1786
  // Track 1: Read local MCP server definitions from ~/.clay/mcp.json
1673
1787
  // and inject into Codex config so Codex manages them natively.
1674
- var mcpServerConfig = {};
1788
+ var mcpServerConfig = Object.assign({}, serverOpts.config.mcp_servers || {});
1675
1789
  try {
1676
1790
  var mcpLocal = require("../../mcp-local");
1677
1791
  var localMcpServers = mcpLocal.readMergedServers();
@@ -1711,6 +1825,17 @@ function createCodexAdapter(opts) {
1711
1825
  console.error("[codex] Failed to configure clay-tools bridge:", e.message);
1712
1826
  }
1713
1827
 
1828
+ // Recovery uses a dedicated app-server so the query-local bridge can
1829
+ // coexist with every server already configured for this runtime.
1830
+ var recoveryMcpServer = effectiveInitOpts.recoverySessionMcpServer;
1831
+ if (recoveryMcpServer && recoveryMcpServer.name && recoveryMcpServer.command) {
1832
+ mcpServerConfig[recoveryMcpServer.name] = {
1833
+ command: recoveryMcpServer.command,
1834
+ args: recoveryMcpServer.args || [],
1835
+ env: recoveryMcpServer.env || {},
1836
+ };
1837
+ }
1838
+
1714
1839
  if (Object.keys(mcpServerConfig).length > 0) {
1715
1840
  serverOpts.config = Object.assign({}, serverOpts.config || {}, {
1716
1841
  mcp_servers: mcpServerConfig,
@@ -1749,7 +1874,8 @@ function createCodexAdapter(opts) {
1749
1874
  // triggering and skills/list use the same merged inventory.
1750
1875
  var skillNames = [];
1751
1876
  try {
1752
- var roots = skillDiscovery.getSkillRoots(_cwd);
1877
+ var skillOptions = scopedSkillOptions(effectiveInitOpts.env);
1878
+ var roots = skillOptions.disable ? [] : skillDiscovery.getSkillRoots(_cwd, skillOptions);
1753
1879
  var extraRootPaths = [];
1754
1880
  for (var ri = 0; ri < roots.length; ri++) {
1755
1881
  if (roots[ri].source !== "codex-user" && path.isAbsolute(roots[ri].path)) extraRootPaths.push(roots[ri].path);
@@ -1778,7 +1904,7 @@ function createCodexAdapter(opts) {
1778
1904
  }
1779
1905
  // Direct discovery remains the fallback for older Codex app-server
1780
1906
  // versions and keeps the GUI inventory vendor-neutral.
1781
- var sharedSkills = skillDiscovery.discoverSkills(_cwd);
1907
+ var sharedSkills = skillDiscovery.discoverSkills(_cwd, skillOptions);
1782
1908
  for (var csn = 0; csn < sharedSkills.length; csn++) {
1783
1909
  if (skillNames.indexOf(sharedSkills[csn].name) === -1) skillNames.push(sharedSkills[csn].name);
1784
1910
  }
@@ -1792,13 +1918,20 @@ function createCodexAdapter(opts) {
1792
1918
  throw createShutdownError();
1793
1919
  }
1794
1920
 
1795
- _initPromise = null;
1796
1921
  updateLastActiveAt();
1797
1922
 
1798
1923
  return buildReadyResponse(skillNames);
1799
1924
  })();
1800
1925
 
1801
- return _initPromise;
1926
+ _initPromise = initPromise;
1927
+ initPromise.then(function() {
1928
+ _initializing = false;
1929
+ if (_initPromise === initPromise) _initPromise = null;
1930
+ }, function() {
1931
+ _initializing = false;
1932
+ if (_initPromise === initPromise) _initPromise = null;
1933
+ });
1934
+ return initPromise;
1802
1935
  },
1803
1936
 
1804
1937
  supportedModels: function() {
@@ -1827,6 +1960,10 @@ function createCodexAdapter(opts) {
1827
1960
  if (_shuttingDown) {
1828
1961
  throw createShutdownError();
1829
1962
  }
1963
+ if (queryOpts && queryOpts.sessionMcpServer && queryOpts.resumeSessionId && !queryOpts.isolatedRecovery) {
1964
+ return createIsolatedRecoveryQuery(queryOpts);
1965
+ }
1966
+ await waitForRecoveryHandoff();
1830
1967
 
1831
1968
  if (!_appServer || !_appServer.started) {
1832
1969
  await adapter.init(queryOpts || {});
@@ -1865,8 +2002,11 @@ function createCodexAdapter(opts) {
1865
2002
  onElicitation: queryOpts.userInputMode === "fallback" ? null : (queryOpts.onElicitation || null),
1866
2003
  onUserInputRequest: queryOpts.userInputMode === "fallback" ? null : (queryOpts.onUserInputRequest || null),
1867
2004
  resumeSessionId: queryOpts.resumeSessionId || null,
2005
+ sessionMcpServer: queryOpts.sessionMcpServer || null,
2006
+ isolatedRecovery: queryOpts.isolatedRecovery === true,
1868
2007
  registerBackgroundTaskReset: registerBackgroundTaskReset,
1869
2008
  ephemeral: queryOpts.ephemeral === true,
2009
+ skillOptions: scopedSkillOptions(queryOpts.env),
1870
2010
  };
1871
2011
 
1872
2012
  // Reasoning effort
@@ -1899,6 +2039,7 @@ function createCodexAdapter(opts) {
1899
2039
  handleOpts.onFinished = function() {
1900
2040
  removeActiveQuery(activeEntry);
1901
2041
  decrementRefCount();
2042
+ if (typeof queryOpts.onRuntimeFinished === "function") queryOpts.onRuntimeFinished();
1902
2043
  };
1903
2044
  handle = createCodexQueryHandle(_appServer, handleOpts);
1904
2045
  } catch (e) {
@@ -2013,6 +2154,7 @@ function createCodexAdapter(opts) {
2013
2154
  return Promise.all([
2014
2155
  beginShutdown(true),
2015
2156
  shutdownUserRuntimes("shutdown"),
2157
+ shutdownRecoveryRuntimes(),
2016
2158
  ]).then(function() { return true; });
2017
2159
  },
2018
2160
 
@@ -2040,7 +2182,7 @@ function createCodexAdapter(opts) {
2040
2182
  });
2041
2183
  }
2042
2184
  if (_shuttingDown || _shutdownPromise) return Promise.resolve(false);
2043
- if (_initPromise) return Promise.resolve(false);
2185
+ if (_initializing) return Promise.resolve(false);
2044
2186
  if (!_appServer) return Promise.resolve(false);
2045
2187
  if (_refCount > 0) return Promise.resolve(false);
2046
2188
  if (Date.now() - _lastActiveAt < (idleMs || 0)) return Promise.resolve(false);
@@ -18,7 +18,7 @@ var INITIALIZE_TIMEOUT_MS = require("../interface").INITIALIZE_TIMEOUT_MS;
18
18
  var skillDiscovery = require("../skill-discovery");
19
19
  var { KIRO_DEFAULTS } = require("../../kiro-defaults");
20
20
 
21
- function injectSharedSkillContent(message, skills) {
21
+ function injectSharedSkillContent(message, skills, readOpts) {
22
22
  var text = "";
23
23
  if (typeof message === "string") text = message;
24
24
  else if (Array.isArray(message)) {
@@ -26,7 +26,7 @@ function injectSharedSkillContent(message, skills) {
26
26
  if (message[i] && message[i].type === "text") text += "\n" + (message[i].text || "");
27
27
  }
28
28
  }
29
- var content = skillDiscovery.readReferencedSkills(text, skills, 32768);
29
+ var content = skillDiscovery.readReferencedSkills(text, skills, 32768, readOpts);
30
30
  if (!content) return message;
31
31
  var appendix = "\n\nUse the following shared skill instructions for this turn:\n" + content;
32
32
  if (typeof message === "string") return message + appendix;
@@ -682,7 +682,7 @@ function createKiroQueryHandle(acp, queryOpts) {
682
682
  while (!isCancelled()) {
683
683
  resetTurnState();
684
684
 
685
- var messageWithSkills = injectSharedSkillContent(currentMessage, queryOpts.skills || []);
685
+ var messageWithSkills = injectSharedSkillContent(currentMessage, queryOpts.skills || [], queryOpts.skillOptions);
686
686
  var input = typeof messageWithSkills === "string"
687
687
  ? [{ type: "text", text: messageWithSkills }]
688
688
  : messageWithSkills;
@@ -1012,7 +1012,7 @@ function createKiroAdapter(opts) {
1012
1012
  _defaultModel = "auto";
1013
1013
  }
1014
1014
 
1015
- var skillNames = skillDiscovery.discoverSkills(_cwd).map(function(skill) { return skill.name; });
1015
+ var skillNames = skillDiscovery.discoverSkills(_cwd, { env: (initOpts && initOpts.env) || null }).map(function(skill) { return skill.name; });
1016
1016
  console.log("[yoke/kiro] ACP initialized, models: " + _cachedModels.length + ", skills: " + skillNames.length);
1017
1017
 
1018
1018
  updateLastActiveAt();
@@ -1065,7 +1065,7 @@ function createKiroAdapter(opts) {
1065
1065
  var model = queryOpts.model || _defaultModel || "auto";
1066
1066
  var ac = queryOpts.abortController || new AbortController();
1067
1067
  var kiroOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.KIRO) || {};
1068
- var sharedSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd || _cwd);
1068
+ var sharedSkills = queryOpts.skipSkills ? [] : skillDiscovery.discoverSkills(queryOpts.cwd || _cwd, queryOpts.skillOptions || { env: queryOpts.env || null });
1069
1069
  var skillIndex = skillDiscovery.buildSkillIndex(sharedSkills);
1070
1070
 
1071
1071
  var activeEntry = { abort: function() { try { ac.abort(); } catch (e) {} } };
@@ -1079,6 +1079,7 @@ function createKiroAdapter(opts) {
1079
1079
  systemPrompt: queryOpts.systemPrompt || "",
1080
1080
  appendSystemPrompt: [queryOpts.appendSystemPrompt, skillIndex].filter(function (part) { return !!part; }).join("\n\n"),
1081
1081
  skills: sharedSkills,
1082
+ skillOptions: queryOpts.skillOptions || { env: queryOpts.env || null },
1082
1083
  abortController: ac,
1083
1084
  canUseTool: queryOpts.canUseTool || null,
1084
1085
  resumeSessionId: queryOpts.resumeSessionId || null,
@@ -0,0 +1,45 @@
1
+ // Session-owned shared skill catalog.
2
+ // ----------------------------------
3
+ // Keeps personal skill names out of project-wide session-manager state while
4
+ // retaining vendor commands separately for hydration and session switching.
5
+
6
+ var skillDiscovery = require("./skill-discovery");
7
+
8
+ function createSessionSkillCatalog(opts) {
9
+ opts = opts || {};
10
+ var cwd = opts.cwd;
11
+ var osUsers = !!opts.osUsers;
12
+ var getLinuxUser = opts.getLinuxUser || function () { return null; };
13
+ var resolveUser = opts.resolveUser || require("../os-users").resolveOsUserInfo;
14
+
15
+ function optionsFor(session) {
16
+ if (!osUsers) return opts.homeDir ? { homeDir: opts.homeDir, env: opts.env } : {};
17
+ var linuxUser = getLinuxUser(session);
18
+ if (!linuxUser) return { disable: true };
19
+ try {
20
+ return skillDiscovery.optionsForOsUser(resolveUser(linuxUser));
21
+ } catch (e) {
22
+ return { disable: true };
23
+ }
24
+ }
25
+
26
+ function hydrate(session) {
27
+ if (!session) return [];
28
+ var discovered = skillDiscovery.discoverSkills(cwd, optionsFor(session));
29
+ var names = discovered.map(function (skill) { return skill.name; });
30
+ session.skillNames = names;
31
+ var vendorCommands = Array.isArray(session.vendorSlashCommands) ? session.vendorSlashCommands : [];
32
+ var combined = vendorCommands.slice();
33
+ for (var i = 0; i < names.length; i++) {
34
+ if (combined.indexOf(names[i]) === -1) combined.push(names[i]);
35
+ }
36
+ session.slashCommands = combined;
37
+ session.slashCommandsByVendor = session.slashCommandsByVendor || {};
38
+ session.slashCommandsByVendor[session.vendor || "claude"] = combined;
39
+ return combined;
40
+ }
41
+
42
+ return { hydrate: hydrate, optionsFor: optionsFor };
43
+ }
44
+
45
+ module.exports = { createSessionSkillCatalog: createSessionSkillCatalog };
@@ -11,6 +11,18 @@ function defaultHomeDir() {
11
11
  try { return require("../config").REAL_HOME; } catch (e) { return os.homedir(); }
12
12
  }
13
13
 
14
+ function optionsForOsUser(userInfo, fsAsUser) {
15
+ if (!userInfo || typeof userInfo.home !== "string" || !userInfo.home) return { disable: true };
16
+ fsAsUser = fsAsUser || require("../os-users").fsAsUser;
17
+ return {
18
+ homeDir: userInfo.home,
19
+ scopedHome: true,
20
+ readDir: function (dir) { return fsAsUser("list", { dir: dir }, userInfo).map(function (entry) { return { name: entry.name, isDir: entry.isDir }; }); },
21
+ readFile: function (file) { return fsAsUser("read", { file: file, readContent: true }, userInfo).content; },
22
+ realpath: function (file) { return fsAsUser("realpath", { file: file }, userInfo).path; },
23
+ };
24
+ }
25
+
14
26
  function unquote(value) {
15
27
  var text = String(value || "").trim();
16
28
  if ((text[0] === '"' && text[text.length - 1] === '"') ||
@@ -20,61 +32,83 @@ function unquote(value) {
20
32
  return text;
21
33
  }
22
34
 
23
- function readMetadata(skillPath, fallbackName) {
35
+ function readMetadata(skillPath, fallbackName, opts) {
24
36
  var content;
25
- try { content = fs.readFileSync(skillPath, "utf8"); } catch (e) { return null; }
37
+ try { content = opts && opts.readFile ? opts.readFile(skillPath) : fs.readFileSync(skillPath, "utf8"); } catch (e) { return null; }
26
38
  var name = fallbackName;
27
39
  var description = "";
40
+ var version = "";
28
41
  var frontmatter = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/);
29
42
  if (frontmatter) {
30
43
  var nameMatch = frontmatter[1].match(/^name:\s*(.+)$/m);
31
44
  var descriptionMatch = frontmatter[1].match(/^description:\s*(.+)$/m);
32
45
  if (nameMatch) name = unquote(nameMatch[1]) || fallbackName;
33
46
  if (descriptionMatch) description = unquote(descriptionMatch[1]);
47
+ var versionMatch = frontmatter[1].match(/^version:\s*(.+)$/m);
48
+ if (versionMatch) version = unquote(versionMatch[1]);
34
49
  }
35
50
  if (!description) {
36
51
  var heading = content.match(/^#\s+(.+)$/m);
37
52
  if (heading) description = heading[1].trim();
38
53
  }
39
- return { name: name, description: description };
54
+ return { name: name, description: description, version: version };
40
55
  }
41
56
 
42
- function scanRoot(root, source, merged) {
57
+ function scanRoot(root, source, merged, opts) {
43
58
  var entries;
44
- try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch (e) { return; }
59
+ try { entries = opts && opts.readDir ? opts.readDir(root) : fs.readdirSync(root, { withFileTypes: true }); } catch (e) { return; }
45
60
  entries.sort(function(a, b) { return a.name.localeCompare(b.name); });
46
61
  for (var i = 0; i < entries.length; i++) {
47
62
  var entry = entries[i];
48
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
63
+ if ((!opts || !opts.readDir) && (entry.isDir === false || (typeof entry.isDirectory === "function" && !entry.isDirectory() && !entry.isSymbolicLink()))) continue;
49
64
  var skillPath = path.join(root, entry.name, "SKILL.md");
50
- var metadata = readMetadata(skillPath, entry.name);
65
+ var realSkillPath;
66
+ try { realSkillPath = opts && opts.realpath ? opts.realpath(skillPath) : fs.realpathSync(skillPath); } catch (e) { continue; }
67
+ var metadata = readMetadata(realSkillPath, entry.name, opts);
51
68
  if (!metadata) continue;
52
- merged[metadata.name] = {
69
+ var key = opts && opts.includeDuplicates ? source + "\0" + metadata.name : metadata.name;
70
+ merged[key] = {
53
71
  name: metadata.name,
54
- path: skillPath,
72
+ path: realSkillPath,
55
73
  description: metadata.description,
74
+ version: metadata.version,
56
75
  source: source,
57
76
  };
58
77
  }
59
78
  }
60
79
 
61
80
  function getSkillRoots(cwd, opts) {
62
- var homeDir = (opts && opts.homeDir) || defaultHomeDir();
81
+ opts = opts || {};
82
+ var env = opts.env || (opts.homeDir ? {} : process.env);
83
+ if (opts.scopedHome) {
84
+ env = Object.assign({}, env);
85
+ delete env.CODEX_HOME;
86
+ delete env.CLAUDE_CONFIG_DIR;
87
+ }
88
+ var homeDir = opts.homeDir || env.HOME || defaultHomeDir();
89
+ var codexHome = env.CODEX_HOME ? path.resolve(homeDir, env.CODEX_HOME) : path.join(homeDir, ".codex");
90
+ var claudeConfigDir = env.CLAUDE_CONFIG_DIR ? path.resolve(homeDir, env.CLAUDE_CONFIG_DIR) : path.join(homeDir, ".claude");
63
91
  var roots = [
64
- { path: path.join(homeDir, ".codex", "skills"), source: "codex-user" },
65
- { path: path.join(homeDir, ".claude", "skills"), source: "claude-user" },
92
+ { path: path.join(homeDir, ".agents", "skills"), source: "agents-user" },
93
+ { path: path.join(codexHome, "skills"), source: "codex-user" },
94
+ { path: path.join(codexHome, "skills", ".system"), source: "codex-system-user" },
95
+ { path: path.join(claudeConfigDir, "skills"), source: "claude-user" },
66
96
  ];
67
97
  if (cwd) {
68
98
  roots.push({ path: path.join(cwd, "skills"), source: "project" });
99
+ roots.push({ path: path.join(cwd, ".agents", "skills"), source: "agents-project" });
100
+ roots.push({ path: path.join(cwd, ".codex", "skills"), source: "codex-project" });
101
+ roots.push({ path: path.join(cwd, ".codex", "skills", ".system"), source: "codex-system-project" });
69
102
  roots.push({ path: path.join(cwd, ".claude", "skills"), source: "claude-project" });
70
103
  }
71
104
  return roots;
72
105
  }
73
106
 
74
107
  function discoverSkills(cwd, opts) {
108
+ if (opts && opts.disable) return [];
75
109
  var merged = {};
76
110
  var roots = getSkillRoots(cwd, opts);
77
- for (var i = 0; i < roots.length; i++) scanRoot(roots[i].path, roots[i].source, merged);
111
+ for (var i = 0; i < roots.length; i++) scanRoot(roots[i].path, roots[i].source, merged, opts);
78
112
  return Object.keys(merged).sort().map(function(name) { return merged[name]; });
79
113
  }
80
114
 
@@ -112,13 +146,13 @@ function buildSkillIndex(skills) {
112
146
  return lines.join("\n");
113
147
  }
114
148
 
115
- function readReferencedSkills(text, skills, maxChars) {
149
+ function readReferencedSkills(text, skills, maxChars, readOpts) {
116
150
  var references = findSkillReferences(text, skills);
117
151
  var remaining = maxChars || 32768;
118
152
  var blocks = [];
119
153
  for (var i = 0; i < references.length && remaining > 0; i++) {
120
154
  var content;
121
- try { content = fs.readFileSync(references[i].path, "utf8"); } catch (e) { continue; }
155
+ try { content = readOpts && readOpts.readFile ? readOpts.readFile(references[i].path) : fs.readFileSync(references[i].path, "utf8"); } catch (e) { continue; }
122
156
  var header = "<shared-skill name=\"" + references[i].name + "\" source=\"" + references[i].source + "\">\n";
123
157
  var footer = "\n</shared-skill>";
124
158
  var available = Math.max(0, remaining - header.length - footer.length);
@@ -142,18 +176,20 @@ function lstatOrNull(target) {
142
176
  }
143
177
 
144
178
  function ensureClaudeCodexPlugin(opts) {
145
- var homeDir = (opts && opts.homeDir) || defaultHomeDir();
146
- var source = path.join(homeDir, ".codex", "skills");
179
+ opts = opts || {};
180
+ var homeDir = opts.homeDir || defaultHomeDir();
181
+ var source = opts.source || path.join(homeDir, ".codex", "skills");
182
+ var bridgeName = opts.name || "codex";
147
183
  try {
148
184
  if (!fs.statSync(source).isDirectory()) return null;
149
185
  } catch (e) { return null; }
150
186
 
151
- var pluginDir = path.join(homeDir, ".clay", "skill-bridge", "codex");
187
+ var pluginDir = path.join(homeDir, ".clay", "skill-bridge", bridgeName);
152
188
  var bridgeRoot = path.dirname(pluginDir);
153
189
  var manifestDir = path.join(pluginDir, ".claude-plugin");
154
190
  var manifestPath = path.join(manifestDir, "plugin.json");
155
191
  var skillsLink = path.join(pluginDir, "skills");
156
- var manifest = JSON.stringify({ name: "codex", version: "1.0.0", description: "Codex skills shared by Clay" }, null, 2) + "\n";
192
+ var manifest = JSON.stringify({ name: bridgeName, version: "1.0.0", description: "Shared skills provided by Clay" }, null, 2) + "\n";
157
193
 
158
194
  try {
159
195
  fs.mkdirSync(path.join(homeDir, ".clay"), { recursive: true });
@@ -168,7 +204,10 @@ function ensureClaudeCodexPlugin(opts) {
168
204
  if (!manifestDirStat) fs.mkdirSync(manifestDir);
169
205
  var manifestStat = lstatOrNull(manifestPath);
170
206
  if (manifestStat) {
171
- if (!manifestStat.isFile() || fs.readFileSync(manifestPath, "utf8") !== manifest) return null;
207
+ if (!manifestStat.isFile()) return null;
208
+ var existingManifest = fs.readFileSync(manifestPath, "utf8");
209
+ var legacyManifest = JSON.stringify({ name: "codex", version: "1.0.0", description: "Codex skills shared by Clay" }, null, 2) + "\n";
210
+ if (existingManifest !== manifest && existingManifest !== legacyManifest) return null;
172
211
  } else {
173
212
  fs.writeFileSync(manifestPath, manifest, { mode: 0o600, flag: "wx" });
174
213
  }
@@ -192,5 +231,6 @@ module.exports = {
192
231
  findSkillReferences: findSkillReferences,
193
232
  getSkillRoots: getSkillRoots,
194
233
  indexSkills: indexSkills,
234
+ optionsForOsUser: optionsForOsUser,
195
235
  readReferencedSkills: readReferencedSkills,
196
236
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0-beta.16",
3
+ "version": "4.1.0-beta.18",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"