clay-server 2.47.0-beta.6 → 3.0.0-beta.2

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 (46) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-session-pair.js +312 -0
  4. package/lib/project-session-spawn.js +8 -4
  5. package/lib/project-sessions.js +108 -10
  6. package/lib/project-user-message.js +18 -8
  7. package/lib/project.js +33 -0
  8. package/lib/public/app.js +20 -0
  9. package/lib/public/css/menus.css +83 -1
  10. package/lib/public/css/messages.css +14 -0
  11. package/lib/public/css/pane.css +420 -0
  12. package/lib/public/css/sidebar.css +40 -0
  13. package/lib/public/css/sticky-notes.css +2 -0
  14. package/lib/public/index.html +4 -0
  15. package/lib/public/modules/app-connection.js +6 -0
  16. package/lib/public/modules/app-favicon.js +5 -0
  17. package/lib/public/modules/app-header.js +101 -6
  18. package/lib/public/modules/app-messages.js +59 -6
  19. package/lib/public/modules/app-panels.js +49 -27
  20. package/lib/public/modules/app-rendering.js +11 -4
  21. package/lib/public/modules/pane-bridge.js +41 -0
  22. package/lib/public/modules/pane-session.js +18 -0
  23. package/lib/public/modules/sidebar-sessions.js +175 -1
  24. package/lib/public/modules/sidebar.js +2 -0
  25. package/lib/public/modules/split-group-helpers.js +18 -0
  26. package/lib/public/modules/split-pair-ui.js +324 -0
  27. package/lib/public/modules/split-view.js +499 -0
  28. package/lib/public/modules/sticky-notes.js +32 -91
  29. package/lib/public/style.css +1 -0
  30. package/lib/sdk-bridge.js +111 -20
  31. package/lib/sdk-message-processor.js +2 -2
  32. package/lib/server.js +24 -4
  33. package/lib/session-hygiene.js +100 -0
  34. package/lib/session-pair-mcp-server.js +28 -0
  35. package/lib/session-split-groups.js +298 -0
  36. package/lib/sessions.js +163 -34
  37. package/lib/ws-request.js +14 -0
  38. package/lib/ws-schema.js +13 -0
  39. package/lib/yoke/adapters/claude.js +32 -0
  40. package/lib/yoke/adapters/codex.js +100 -77
  41. package/lib/yoke/adapters/kiro.js +34 -35
  42. package/lib/yoke/codex-app-server.js +56 -6
  43. package/lib/yoke/index.js +1 -0
  44. package/lib/yoke/skill-discovery.js +196 -0
  45. package/lib/yoke/vendor-registry.js +34 -0
  46. package/package.json +2 -2
@@ -0,0 +1,14 @@
1
+ var url = require("url");
2
+
3
+ function parseWsRequestUrl(requestUrl) {
4
+ var parsed = url.parse(requestUrl || "/", true);
5
+ var paneSession = Number(parsed.query.session);
6
+ if (!Number.isSafeInteger(paneSession) || paneSession < 1) paneSession = null;
7
+ return {
8
+ path: parsed.pathname || "/",
9
+ pane: parsed.query.pane === "1",
10
+ paneSession: paneSession,
11
+ };
12
+ }
13
+
14
+ module.exports = { parseWsRequestUrl: parseWsRequestUrl };
package/lib/ws-schema.js CHANGED
@@ -32,6 +32,12 @@ var schema = {
32
32
  "fork_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Fork a session from a given message UUID" },
33
33
  "input_sync": { direction: "c2s", handler: "lib/project-sessions.js", description: "Sync the current input field text to other clients" },
34
34
  "tui_transcript_request": { direction: "c2s", handler: "lib/project-sessions.js", description: "Ask for the assistant text index of a Claude TUI session (for hover-to-grab)" },
35
+ "split_group_create": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Create a persistent two-session split group" },
36
+ "split_group_rename": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Rename a split group" },
37
+ "split_group_dissolve": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Separate a split group without deleting its sessions" },
38
+ "split_group_set_pair": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Assign Driver/Worker roles on a split group (driverId null clears them)" },
39
+ "pair_session_options": { direction: "c2s", handler: "lib/project-session-pair.js", description: "Request vendors/models for the pair-session dialog (response reuses the same type)" },
40
+ "pair_session_create": { direction: "c2s", handler: "lib/project-session-pair.js", description: "Create a Driver/Worker session pair as a split group" },
35
41
 
36
42
  "session_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of sessions for the sidebar" },
37
43
  "session_switched": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Confirmation that active session changed" },
@@ -44,6 +50,10 @@ var schema = {
44
50
  "fork_complete": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fork succeeded, includes new session ID" },
45
51
  "input_sync_broadcast": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Broadcast input text from another client" },
46
52
  "tui_transcript_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Assistant text index for a Claude TUI session (full replace; sent on request and after each new assistant message)" },
53
+ "split_groups": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full persistent split-group list for the current user" },
54
+ "split_group_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of a split-group mutation" },
55
+ "pair_session_created": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of pair_session_create; carries the new group" },
56
+ "split_delegation": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A pair delegation started or ended (drives the split-view flow indicator)" },
47
57
 
48
58
  // -----------------------------------------------------------------------
49
59
  // History replay
@@ -164,6 +174,9 @@ var schema = {
164
174
  "set_server_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default model" },
165
175
  "set_project_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default model" },
166
176
  "set_permission_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set permission mode for the current session" },
177
+ "set_session_full_access": { direction: "c2s", handler: "lib/project-sessions.js", description: "Toggle Clay-managed permission skipping for a GUI session" },
178
+ "session_full_access_changed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A session's full-access state changed" },
179
+ "session_full_access_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of toggling session full access" },
167
180
  "reload_skills": { direction: "c2s", handler: "lib/project-sessions.js", description: "Hot-reload skills/MCP/agents on the active query without a restart" },
168
181
  "set_mcp_permission_mode_override": { direction: "c2s", handler: "lib/project-sessions.js", description: "Override permission mode (default/auto/null) for one MCP server" },
169
182
  "set_server_default_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default permission mode" },
@@ -11,6 +11,7 @@ var net = require("net");
11
11
  var crypto = require("crypto");
12
12
  var { spawn } = require("child_process");
13
13
  var { resolveOsUserInfo } = require("../../os-users");
14
+ var skillDiscovery = require("../skill-discovery");
14
15
 
15
16
  // SDK-specific knowledge stays in the adapter (not the relay): the host dialog
16
17
  // kinds this client can render, declared to the SDK when an onUserDialog
@@ -18,6 +19,13 @@ var { resolveOsUserInfo } = require("../../os-users");
18
19
  // emitted. Keep in sync with the client's generic user-dialog renderer.
19
20
  var SUPPORTED_DIALOG_KINDS = ["refusal_fallback_prompt"];
20
21
 
22
+ function getSharedSkillPlugins(homeDir, existingPlugins) {
23
+ var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
24
+ var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(homeDir ? { homeDir: homeDir } : null);
25
+ if (bridgePath) plugins.push({ type: "local", path: bridgePath, skipMcpDiscovery: true });
26
+ return plugins;
27
+ }
28
+
21
29
  // --- SDK loading ---
22
30
  // Async loader (ESM dynamic import, same pattern as current project.js getSDK)
23
31
  var _sdkPromise = null;
@@ -1166,6 +1174,7 @@ function createClaudeAdapter(opts) {
1166
1174
  abortController: ac,
1167
1175
  settings: { disableAllHooks: true },
1168
1176
  };
1177
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
1169
1178
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1170
1179
 
1171
1180
  if (initOpts && initOpts.dangerouslySkipPermissions) {
@@ -1181,6 +1190,7 @@ function createClaudeAdapter(opts) {
1181
1190
  fastModeState: null,
1182
1191
  capabilities: {
1183
1192
  effort: true,
1193
+ midSessionModelSwitch: true,
1184
1194
  fork: true,
1185
1195
  rollback: false,
1186
1196
  sessionListing: true,
@@ -1193,6 +1203,7 @@ function createClaudeAdapter(opts) {
1193
1203
  elicitation: true,
1194
1204
  fileCheckpointing: true,
1195
1205
  contextCompacting: true,
1206
+ skillSharing: true,
1196
1207
  toolPolicy: ["ask", "allow-all"],
1197
1208
  },
1198
1209
  };
@@ -1328,6 +1339,12 @@ function createClaudeAdapter(opts) {
1328
1339
 
1329
1340
  // YOKE standard options -> SDK options
1330
1341
  if (queryOpts.systemPrompt) sdkOptions.systemPrompt = queryOpts.systemPrompt;
1342
+ // Additive variant: keep the full Claude Code system prompt and append.
1343
+ // A plain-string systemPrompt REPLACES the whole prompt, so pair
1344
+ // presets and other add-ons must come through here instead.
1345
+ if (!queryOpts.systemPrompt && queryOpts.appendSystemPrompt) {
1346
+ sdkOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
1347
+ }
1331
1348
  if (queryOpts.model) sdkOptions.model = queryOpts.model;
1332
1349
  if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
1333
1350
  if (queryOpts.toolServers) sdkOptions.mcpServers = queryOpts.toolServers;
@@ -1358,6 +1375,7 @@ function createClaudeAdapter(opts) {
1358
1375
  if (co.allowDangerouslySkipPermissions) sdkOptions.allowDangerouslySkipPermissions = true;
1359
1376
  if (co.resumeSessionAt) sdkOptions.resumeSessionAt = co.resumeSessionAt;
1360
1377
  if (co.settings) sdkOptions.settings = co.settings;
1378
+ sdkOptions.plugins = getSharedSkillPlugins(co.originalHome, co.plugins);
1361
1379
 
1362
1380
  var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
1363
1381
  return createQueryHandle(rawQuery, mq, ac);
@@ -1510,6 +1528,12 @@ function createClaudeAdapter(opts) {
1510
1528
  if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
1511
1529
  if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
1512
1530
  if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
1531
+ // Same additive mapping as the in-process path: never pass a plain
1532
+ // string (it would replace the whole Claude Code system prompt).
1533
+ if (queryOpts.appendSystemPrompt) {
1534
+ queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
1535
+ }
1536
+ queryOptions.plugins = getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins);
1513
1537
 
1514
1538
  if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
1515
1539
  if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
@@ -1562,6 +1586,7 @@ function createClaudeAdapter(opts) {
1562
1586
  fastModeState: r.fastModeState || null,
1563
1587
  capabilities: {
1564
1588
  effort: true,
1589
+ midSessionModelSwitch: true,
1565
1590
  fork: true,
1566
1591
  rollback: false,
1567
1592
  sessionListing: true,
@@ -1574,6 +1599,7 @@ function createClaudeAdapter(opts) {
1574
1599
  elicitation: true,
1575
1600
  fileCheckpointing: true,
1576
1601
  contextCompacting: true,
1602
+ skillSharing: true,
1577
1603
  toolPolicy: ["ask", "allow-all"],
1578
1604
  },
1579
1605
  });
@@ -1624,6 +1650,7 @@ function createClaudeAdapter(opts) {
1624
1650
  abortController: ac,
1625
1651
  settings: { disableAllHooks: true },
1626
1652
  };
1653
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
1627
1654
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1628
1655
 
1629
1656
  if (initOpts && initOpts.dangerouslySkipPermissions) {
@@ -1639,6 +1666,7 @@ function createClaudeAdapter(opts) {
1639
1666
  fastModeState: null,
1640
1667
  capabilities: {
1641
1668
  effort: true,
1669
+ midSessionModelSwitch: true,
1642
1670
  fork: true,
1643
1671
  rollback: false,
1644
1672
  sessionListing: true,
@@ -1651,6 +1679,7 @@ function createClaudeAdapter(opts) {
1651
1679
  elicitation: true,
1652
1680
  fileCheckpointing: true,
1653
1681
  contextCompacting: true,
1682
+ skillSharing: true,
1654
1683
  toolPolicy: ["ask", "allow-all"],
1655
1684
  },
1656
1685
  };
@@ -1703,6 +1732,7 @@ function createClaudeAdapter(opts) {
1703
1732
  }
1704
1733
 
1705
1734
  var warmupOptions = { cwd: workerCwd, settingSources: ["user", "project", "local"], settings: { disableAllHooks: true } };
1735
+ warmupOptions.plugins = getSharedSkillPlugins(initOpts && initOpts.originalHome);
1706
1736
  if (_claudeBinaryPath) warmupOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1707
1737
  if (initOpts && initOpts.dangerouslySkipPermissions) {
1708
1738
  warmupOptions.permissionMode = "bypassPermissions";
@@ -1724,6 +1754,7 @@ function createClaudeAdapter(opts) {
1724
1754
  fastModeState: r.fastModeState || null,
1725
1755
  capabilities: {
1726
1756
  effort: true,
1757
+ midSessionModelSwitch: true,
1727
1758
  fork: true,
1728
1759
  rollback: false,
1729
1760
  sessionListing: true,
@@ -1736,6 +1767,7 @@ function createClaudeAdapter(opts) {
1736
1767
  elicitation: true,
1737
1768
  fileCheckpointing: true,
1738
1769
  contextCompacting: true,
1770
+ skillSharing: true,
1739
1771
  toolPolicy: ["ask", "allow-all"],
1740
1772
  },
1741
1773
  });
@@ -6,55 +6,7 @@
6
6
  var path = require("path");
7
7
  var fs = require("fs");
8
8
  var { CodexAppServer } = require("../codex-app-server");
9
-
10
- // --- Claude skill discovery ---
11
- // Finds Claude skills in ~/.claude/skills/ and <cwd>/.claude/skills/
12
- // so Codex can recognize $<skill-name> in user input.
13
- function discoverClaudeSkills(cwd) {
14
- var skills = {};
15
- var REAL_HOME;
16
- try { REAL_HOME = require("../../config").REAL_HOME; } catch (e) { REAL_HOME = require("os").homedir(); }
17
- var dirs = [
18
- path.join(REAL_HOME, ".claude", "skills"),
19
- path.join(cwd || "", ".claude", "skills"),
20
- ];
21
- for (var d = 0; d < dirs.length; d++) {
22
- var base = dirs[d];
23
- if (!base) continue;
24
- var entries;
25
- try { entries = fs.readdirSync(base, { withFileTypes: true }); } catch (e) { continue; }
26
- for (var i = 0; i < entries.length; i++) {
27
- var entry = entries[i];
28
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
29
- var skillMd = path.join(base, entry.name, "SKILL.md");
30
- try {
31
- fs.accessSync(skillMd, fs.constants.R_OK);
32
- // project skills override global skills
33
- skills[entry.name] = skillMd;
34
- } catch (e) {}
35
- }
36
- }
37
- return skills;
38
- }
39
-
40
- // Parse user text for $<skill-name> references.
41
- // Returns { text, skills: [{ name, path }] }
42
- function parseSkillRefs(text, availableSkills) {
43
- if (typeof text !== "string") return { text: text, skills: [] };
44
- var skills = [];
45
- var seen = {};
46
- var re = /\$([a-zA-Z0-9_-]+)/g;
47
- var match;
48
- while ((match = re.exec(text)) !== null) {
49
- var name = match[1];
50
- if (seen[name]) continue;
51
- if (availableSkills[name]) {
52
- seen[name] = true;
53
- skills.push({ name: name, path: availableSkills[name] });
54
- }
55
- }
56
- return { text: text, skills: skills };
57
- }
9
+ var skillDiscovery = require("../skill-discovery");
58
10
 
59
11
  // --- Event flattening ---
60
12
  // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
@@ -193,11 +145,14 @@ function flattenEvent(notification, state) {
193
145
  events.push({ yokeType: "interrupted" });
194
146
  }
195
147
  var inputTokens = state.lastInputTokens || (usage ? (usage.input_tokens || 0) + (usage.cached_input_tokens || 0) : 0);
196
- var outputTokens = usage ? (usage.output_tokens || 0) : 0;
197
- var cachedTokens = usage ? (usage.cached_input_tokens || 0) : 0;
148
+ var outputTokens = (usage ? (usage.output_tokens || 0) : 0) || state.lastOutputTokens || 0;
149
+ var cachedTokens = (usage ? (usage.cached_input_tokens || 0) : 0) || state.lastCachedTokens || 0;
198
150
  var hasTokenData = inputTokens > 0 || outputTokens > 0;
199
151
  var resultModelUsage = {};
200
- resultModelUsage[state.model] = { contextWindow: null };
152
+ // Codex reports the real window in thread/tokenUsage/updated. Dropping it
153
+ // made the client fall back to its model-name table, which has no entry
154
+ // for codex model ids and defaulted to Claude's 200k denominator.
155
+ resultModelUsage[state.model] = { contextWindow: state.modelContextWindow || null };
201
156
  var assistantUuid = generateUuid();
202
157
  events.push({
203
158
  yokeType: "result",
@@ -216,6 +171,8 @@ function flattenEvent(notification, state) {
216
171
  lastStreamInputTokens: state.lastInputTokens || null,
217
172
  });
218
173
  state.lastInputTokens = null;
174
+ state.lastCachedTokens = null;
175
+ state.lastOutputTokens = null;
219
176
  return events;
220
177
  }
221
178
 
@@ -300,6 +257,41 @@ function flattenEvent(notification, state) {
300
257
  return events;
301
258
  }
302
259
 
260
+ // Reasoning deltas: streamed summarized thinking. The reasoning ITEM often
261
+ // carries only encrypted content with no readable text; the human-readable
262
+ // summary arrives through these delta notifications. Without handling them
263
+ // Codex sessions show no thinking at all.
264
+ if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
265
+ var rDeltaItemId = params.itemId || params.id;
266
+ if (rDeltaItemId && !state.thinkingBlocks[rDeltaItemId]) {
267
+ state.blockCounter++;
268
+ state.thinkingBlocks[rDeltaItemId] = "blk_" + state.blockCounter;
269
+ events.push({ yokeType: "thinking_start", blockId: state.thinkingBlocks[rDeltaItemId] });
270
+ }
271
+ if (params.delta) {
272
+ events.push({
273
+ yokeType: "thinking_delta",
274
+ blockId: rDeltaItemId ? state.thinkingBlocks[rDeltaItemId] : ("blk_" + state.blockCounter),
275
+ text: params.delta,
276
+ });
277
+ // Track streamed length so item/completed doesn't re-send the full text
278
+ if (rDeltaItemId) {
279
+ state.thinkingLengths[rDeltaItemId] = (state.thinkingLengths[rDeltaItemId] || 0) + params.delta.length;
280
+ }
281
+ }
282
+ return events;
283
+ }
284
+
285
+ // Separator between reasoning summary sections
286
+ if (method === "item/reasoning/summaryPartAdded") {
287
+ var rPartItemId = params.itemId || params.id;
288
+ if (rPartItemId && state.thinkingBlocks[rPartItemId]) {
289
+ events.push({ yokeType: "thinking_delta", blockId: state.thinkingBlocks[rPartItemId], text: "\n\n" });
290
+ state.thinkingLengths[rPartItemId] = (state.thinkingLengths[rPartItemId] || 0) + 2;
291
+ }
292
+ return events;
293
+ }
294
+
303
295
  if (method === "item/plan/delta") {
304
296
  var planDeltaItemId = params.itemId || params.id;
305
297
  var nextPlanText = (state.planTexts[planDeltaItemId] || "") + (params.delta || "");
@@ -553,11 +545,23 @@ function flattenEvent(notification, state) {
553
545
  }
554
546
  }
555
547
 
556
- // Token usage update - track input tokens for context bar
548
+ // Token usage update - track input tokens for context bar.
549
+ //
550
+ // ThreadTokenUsage carries { lastTurn, total, modelContextWindow }. The
551
+ // context gauge must use the LAST turn, not the running total: each turn's
552
+ // input already contains the whole conversation, so summing turns
553
+ // overstates occupancy (a 5k + 8k thread reads as 13k of an 8k context).
554
+ // `total` is only a fallback for servers that omit lastTurn.
557
555
  if (method === "thread/tokenUsage/updated") {
558
556
  var tu = params.tokenUsage;
559
- if (tu && tu.total) {
560
- state.lastInputTokens = tu.total.inputTokens || 0;
557
+ if (tu) {
558
+ var turnUsage = tu.lastTurn || tu.total;
559
+ if (turnUsage) {
560
+ state.lastInputTokens = (turnUsage.inputTokens || 0) + (turnUsage.cachedInputTokens || 0);
561
+ state.lastCachedTokens = turnUsage.cachedInputTokens || 0;
562
+ state.lastOutputTokens = turnUsage.outputTokens || 0;
563
+ }
564
+ if (tu.modelContextWindow) state.modelContextWindow = tu.modelContextWindow;
561
565
  }
562
566
  return events;
563
567
  }
@@ -604,6 +608,9 @@ function createEventState(model) {
604
608
  turnStarted: false,
605
609
  lastUsage: null,
606
610
  lastInputTokens: null,
611
+ lastCachedTokens: null,
612
+ lastOutputTokens: null,
613
+ modelContextWindow: null,
607
614
  done: false,
608
615
  aborted: false,
609
616
  loopStarted: false,
@@ -622,7 +629,7 @@ function createEventState(model) {
622
629
 
623
630
  function createCodexQueryHandle(appServer, queryOpts) {
624
631
  var abortController = queryOpts.abortController;
625
- var systemPrompt = queryOpts.systemPrompt || "";
632
+ var systemPrompt = queryOpts.systemPrompt || queryOpts.appendSystemPrompt || "";
626
633
  var canUseTool = queryOpts.canUseTool || null;
627
634
  var onElicitation = queryOpts.onElicitation || null;
628
635
  var onFinished = queryOpts.onFinished || null;
@@ -677,6 +684,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
677
684
  var messageQueue = [];
678
685
  var messageWaiting = null;
679
686
  var messageQueueEnded = false;
687
+ var handlerEntry = null;
680
688
 
681
689
  function pushMessageToQueue(msg) {
682
690
  if (messageQueueEnded) return;
@@ -863,8 +871,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
863
871
  }
864
872
 
865
873
  try {
866
- // Set event handler on app-server
867
- appServer.eventHandler = handleServerEvent;
874
+ handlerEntry = appServer.addHandler(handleServerEvent);
875
+ if (queryOpts.resumeSessionId) {
876
+ handlerEntry.threadId = queryOpts.resumeSessionId;
877
+ }
868
878
 
869
879
  // Start or resume thread
870
880
  var threadParams = {
@@ -896,6 +906,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
896
906
 
897
907
  if (threadResult && threadResult.thread) {
898
908
  state.threadId = threadResult.thread.id;
909
+ handlerEntry.threadId = state.threadId;
899
910
  }
900
911
 
901
912
  while (!isCancelled()) {
@@ -919,23 +930,24 @@ function createCodexQueryHandle(appServer, queryOpts) {
919
930
  input = currentMessage;
920
931
  }
921
932
 
922
- // Detect $<skill-name> references (Claude skills) and inject skill input items
923
- var availableSkills = discoverClaudeSkills(queryOpts.cwd);
933
+ // Explicit references use Codex's native skill input item regardless of
934
+ // which vendor owns the SKILL.md file.
935
+ var availableSkills = skillDiscovery.discoverSkills(queryOpts.cwd);
924
936
  var skillItemsToInject = [];
925
937
  var injected = {};
926
938
  for (var ii = 0; ii < input.length; ii++) {
927
939
  if (input[ii].type === "text" && input[ii].text) {
928
- var parsed = parseSkillRefs(input[ii].text, availableSkills);
929
- for (var si = 0; si < parsed.skills.length; si++) {
930
- if (!injected[parsed.skills[si].name]) {
931
- injected[parsed.skills[si].name] = true;
932
- skillItemsToInject.push({ type: "skill", name: parsed.skills[si].name, path: parsed.skills[si].path });
940
+ var referencedSkills = skillDiscovery.findSkillReferences(input[ii].text, availableSkills);
941
+ for (var si = 0; si < referencedSkills.length; si++) {
942
+ if (!injected[referencedSkills[si].name]) {
943
+ injected[referencedSkills[si].name] = true;
944
+ skillItemsToInject.push({ type: "skill", name: referencedSkills[si].name, path: referencedSkills[si].path });
933
945
  }
934
946
  }
935
947
  }
936
948
  }
937
949
  if (skillItemsToInject.length > 0) {
938
- console.log("[yoke/codex] injecting Claude skills:", skillItemsToInject.map(function(s) { return s.name; }).join(", "));
950
+ console.log("[yoke/codex] injecting shared skills:", skillItemsToInject.map(function(s) { return s.name; }).join(", "));
939
951
  input = input.concat(skillItemsToInject);
940
952
  }
941
953
 
@@ -964,6 +976,11 @@ function createCodexQueryHandle(appServer, queryOpts) {
964
976
  ? { yokeType: "auth_required", vendor: "codex" }
965
977
  : { yokeType: "error", text: loopErrMsg });
966
978
  }
979
+ } finally {
980
+ if (handlerEntry) {
981
+ appServer.removeHandler(handlerEntry);
982
+ handlerEntry = null;
983
+ }
967
984
  }
968
985
 
969
986
  state.done = true;
@@ -1143,6 +1160,7 @@ function createCodexAdapter(opts) {
1143
1160
  fastModeState: null,
1144
1161
  capabilities: {
1145
1162
  effort: true,
1163
+ midSessionModelSwitch: false,
1146
1164
  fork: true,
1147
1165
  rollback: true,
1148
1166
  sessionListing: false,
@@ -1155,6 +1173,7 @@ function createCodexAdapter(opts) {
1155
1173
  elicitation: true,
1156
1174
  fileCheckpointing: false,
1157
1175
  contextCompacting: false,
1176
+ skillSharing: true,
1158
1177
  toolPolicy: ["ask", "allow-all"],
1159
1178
  },
1160
1179
  };
@@ -1358,17 +1377,21 @@ function createCodexAdapter(opts) {
1358
1377
 
1359
1378
  _cachedModels = CODEX_MODELS.slice();
1360
1379
 
1361
- // Discover skills: built-in Codex skills + Claude skills
1380
+ // Register foreign vendor roots with Codex so automatic skill
1381
+ // triggering and skills/list use the same merged inventory.
1362
1382
  var skillNames = [];
1363
1383
  try {
1364
- var REAL_HOME;
1365
- try { REAL_HOME = require("../../config").REAL_HOME; } catch (e) { REAL_HOME = require("os").homedir(); }
1366
- var claudeSkillsDir = require("path").join(REAL_HOME, ".claude", "skills");
1367
- var extraRoots = _cwd ? [{ cwd: _cwd, extraUserRoots: [claudeSkillsDir] }] : [];
1384
+ var roots = skillDiscovery.getSkillRoots(_cwd);
1385
+ var extraRootPaths = [];
1386
+ for (var ri = 0; ri < roots.length; ri++) {
1387
+ if (roots[ri].source !== "codex-user" && path.isAbsolute(roots[ri].path)) extraRootPaths.push(roots[ri].path);
1388
+ }
1389
+ await _appServer.send("skills/extraRoots/set", { extraRoots: extraRootPaths }, 10000).catch(function(e) {
1390
+ console.error("[codex] skills/extraRoots/set failed:", e.message);
1391
+ });
1368
1392
  var skillsResult = await _appServer.send("skills/list", {
1369
1393
  cwds: _cwd ? [_cwd] : [],
1370
1394
  forceReload: true,
1371
- perCwdExtraUserRoots: extraRoots,
1372
1395
  }, 10000).catch(function(e) {
1373
1396
  console.error("[codex] skills/list failed:", e.message);
1374
1397
  return null;
@@ -1385,11 +1408,11 @@ function createCodexAdapter(opts) {
1385
1408
  }
1386
1409
  }
1387
1410
  }
1388
- // Also discover Claude skills directly as fallback
1389
- var claudeSkills = discoverClaudeSkills(_cwd);
1390
- var claudeSkillNames = Object.keys(claudeSkills);
1391
- for (var csn = 0; csn < claudeSkillNames.length; csn++) {
1392
- if (skillNames.indexOf(claudeSkillNames[csn]) === -1) skillNames.push(claudeSkillNames[csn]);
1411
+ // Direct discovery remains the fallback for older Codex app-server
1412
+ // versions and keeps the GUI inventory vendor-neutral.
1413
+ var sharedSkills = skillDiscovery.discoverSkills(_cwd);
1414
+ for (var csn = 0; csn < sharedSkills.length; csn++) {
1415
+ if (skillNames.indexOf(sharedSkills[csn].name) === -1) skillNames.push(sharedSkills[csn].name);
1393
1416
  }
1394
1417
  console.log("[codex] Discovered skills:", skillNames.length, "(" + skillNames.slice(0, 5).join(", ") + (skillNames.length > 5 ? "..." : "") + ")");
1395
1418
  } catch (e) {
@@ -12,40 +12,33 @@
12
12
  // session/request_permission requests for tool approvals.
13
13
  // session/cancel (notif) -> interrupt the active turn
14
14
 
15
- var path = require("path");
16
- var fs = require("fs");
17
15
  var { execFile } = require("child_process");
18
16
  var { KiroAcpServer, findKiroPath } = require("../kiro-acp-server");
17
+ var skillDiscovery = require("../skill-discovery");
19
18
  var { KIRO_DEFAULTS } = require("../../kiro-defaults");
20
19
 
21
- // --- Claude skill discovery ---
22
- // Kiro exposes its own agents/skills, but Clay users share $<skill-name>
23
- // references from ~/.claude/skills. We mirror the Codex adapter so the same
24
- // references resolve regardless of runtime.
25
- function discoverClaudeSkills(cwd) {
26
- var skills = {};
27
- var REAL_HOME;
28
- try { REAL_HOME = require("../../config").REAL_HOME; } catch (e) { REAL_HOME = require("os").homedir(); }
29
- var dirs = [
30
- path.join(REAL_HOME, ".claude", "skills"),
31
- path.join(cwd || "", ".claude", "skills"),
32
- ];
33
- for (var d = 0; d < dirs.length; d++) {
34
- var base = dirs[d];
35
- if (!base) continue;
36
- var entries;
37
- try { entries = fs.readdirSync(base, { withFileTypes: true }); } catch (e) { continue; }
38
- for (var i = 0; i < entries.length; i++) {
39
- var entry = entries[i];
40
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
41
- var skillMd = path.join(base, entry.name, "SKILL.md");
42
- try {
43
- fs.accessSync(skillMd, fs.constants.R_OK);
44
- skills[entry.name] = skillMd;
45
- } catch (e) {}
20
+ function injectSharedSkillContent(message, skills) {
21
+ var text = "";
22
+ if (typeof message === "string") text = message;
23
+ else if (Array.isArray(message)) {
24
+ for (var i = 0; i < message.length; i++) {
25
+ if (message[i] && message[i].type === "text") text += "\n" + (message[i].text || "");
26
+ }
27
+ }
28
+ var content = skillDiscovery.readReferencedSkills(text, skills, 32768);
29
+ if (!content) return message;
30
+ var appendix = "\n\nUse the following shared skill instructions for this turn:\n" + content;
31
+ if (typeof message === "string") return message + appendix;
32
+ if (!Array.isArray(message)) return message;
33
+ var cloned = message.slice();
34
+ for (var ci = 0; ci < cloned.length; ci++) {
35
+ if (cloned[ci] && cloned[ci].type === "text") {
36
+ cloned[ci] = Object.assign({}, cloned[ci], { text: (cloned[ci].text || "") + appendix });
37
+ return cloned;
46
38
  }
47
39
  }
48
- return skills;
40
+ cloned.unshift({ type: "text", text: appendix.trim() });
41
+ return cloned;
49
42
  }
50
43
 
51
44
  var _uuidCounter = 0;
@@ -404,7 +397,7 @@ function createEventState(opts) {
404
397
 
405
398
  function createKiroQueryHandle(acp, queryOpts) {
406
399
  var abortController = queryOpts.abortController;
407
- var systemPrompt = queryOpts.systemPrompt || "";
400
+ var systemPrompt = queryOpts.systemPrompt || queryOpts.appendSystemPrompt || "";
408
401
  var canUseTool = queryOpts.canUseTool || null;
409
402
  var onFinished = queryOpts.onFinished || null;
410
403
 
@@ -686,9 +679,10 @@ function createKiroQueryHandle(acp, queryOpts) {
686
679
  while (!isCancelled()) {
687
680
  resetTurnState();
688
681
 
689
- var input = typeof currentMessage === "string"
690
- ? [{ type: "text", text: currentMessage }]
691
- : currentMessage;
682
+ var messageWithSkills = injectSharedSkillContent(currentMessage, queryOpts.skills || []);
683
+ var input = typeof messageWithSkills === "string"
684
+ ? [{ type: "text", text: messageWithSkills }]
685
+ : messageWithSkills;
692
686
 
693
687
  // Emit turn_start so the UI records a user turn boundary.
694
688
  pushEvent({ yokeType: "turn_start", uuid: generateUuid(), messageType: "user" });
@@ -869,6 +863,7 @@ function createKiroAdapter(opts) {
869
863
  fastModeState: null,
870
864
  capabilities: {
871
865
  effort: false,
866
+ midSessionModelSwitch: false,
872
867
  fork: false,
873
868
  rollback: false,
874
869
  sessionListing: false,
@@ -881,6 +876,7 @@ function createKiroAdapter(opts) {
881
876
  elicitation: false,
882
877
  fileCheckpointing: false,
883
878
  contextCompacting: true,
879
+ skillSharing: true,
884
880
  toolPolicy: ["ask"],
885
881
  },
886
882
  };
@@ -1011,8 +1007,7 @@ function createKiroAdapter(opts) {
1011
1007
  _defaultModel = "auto";
1012
1008
  }
1013
1009
 
1014
- // Discover Claude skills so $<skill-name> references resolve.
1015
- var skillNames = Object.keys(discoverClaudeSkills(_cwd));
1010
+ var skillNames = skillDiscovery.discoverSkills(_cwd).map(function(skill) { return skill.name; });
1016
1011
  console.log("[yoke/kiro] ACP initialized, models: " + _cachedModels.length + ", skills: " + skillNames.length);
1017
1012
 
1018
1013
  updateLastActiveAt();
@@ -1065,6 +1060,8 @@ function createKiroAdapter(opts) {
1065
1060
  var model = queryOpts.model || _defaultModel || "auto";
1066
1061
  var ac = queryOpts.abortController || new AbortController();
1067
1062
  var kiroOpts = (queryOpts.adapterOptions && queryOpts.adapterOptions.KIRO) || {};
1063
+ var sharedSkills = skillDiscovery.discoverSkills(queryOpts.cwd || _cwd);
1064
+ var skillIndex = skillDiscovery.buildSkillIndex(sharedSkills);
1068
1065
 
1069
1066
  var activeEntry = { abort: function() { try { ac.abort(); } catch (e) {} } };
1070
1067
 
@@ -1074,7 +1071,8 @@ function createKiroAdapter(opts) {
1074
1071
  contextWindow: _modelContextWindows[model] || null,
1075
1072
  mode: kiroOpts.mode || null,
1076
1073
  cwd: queryOpts.cwd || _cwd,
1077
- systemPrompt: queryOpts.systemPrompt || "",
1074
+ systemPrompt: (queryOpts.systemPrompt || "") + (skillIndex ? "\n\n" + skillIndex : ""),
1075
+ skills: sharedSkills,
1078
1076
  abortController: ac,
1079
1077
  canUseTool: queryOpts.canUseTool || null,
1080
1078
  resumeSessionId: queryOpts.resumeSessionId || null,
@@ -1165,6 +1163,7 @@ module.exports = {
1165
1163
  contractTestKit: {
1166
1164
  createEventState: createEventState,
1167
1165
  createQueryHandle: createKiroQueryHandle,
1166
+ injectSharedSkillContent: injectSharedSkillContent,
1168
1167
  normalizeEvent: flattenUpdate,
1169
1168
  },
1170
1169
  };