@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.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 (278) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +18 -3
  2. package/dist/agent/agent-definition-registry.js +47 -18
  3. package/dist/agent/agent-definition.d.ts +17 -0
  4. package/dist/agent/agent-definition.js +22 -1
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +114 -19
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +208 -14
  68. package/dist/engine/engine.js +754 -169
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +31 -6
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +4 -0
  171. package/dist/prompt/composer.js +28 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +33 -0
  220. package/dist/settings/manager.js +94 -1
  221. package/dist/settings/personalization.d.ts +19 -0
  222. package/dist/settings/personalization.js +7 -0
  223. package/dist/settings/schema.d.ts +1142 -126
  224. package/dist/settings/schema.js +119 -10
  225. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  226. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  227. package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
  228. package/dist/tool-system/builtin/agent-registry.js +8 -4
  229. package/dist/tool-system/builtin/agent.d.ts +29 -10
  230. package/dist/tool-system/builtin/agent.js +108 -52
  231. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  232. package/dist/tool-system/builtin/arena.js +6 -7
  233. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  234. package/dist/tool-system/builtin/complete-goal.js +45 -0
  235. package/dist/tool-system/builtin/cron.js +53 -9
  236. package/dist/tool-system/builtin/edit.d.ts +2 -1
  237. package/dist/tool-system/builtin/edit.js +6 -1
  238. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  239. package/dist/tool-system/builtin/generate-image.js +146 -0
  240. package/dist/tool-system/builtin/glob.js +7 -0
  241. package/dist/tool-system/builtin/grep.js +6 -0
  242. package/dist/tool-system/builtin/index.d.ts +18 -1
  243. package/dist/tool-system/builtin/index.js +62 -1
  244. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  245. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  246. package/dist/tool-system/builtin/read.d.ts +2 -1
  247. package/dist/tool-system/builtin/read.js +9 -1
  248. package/dist/tool-system/builtin/sleep.js +9 -3
  249. package/dist/tool-system/builtin/task.js +4 -1
  250. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  251. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  252. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  253. package/dist/tool-system/builtin/view-image.js +96 -0
  254. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  255. package/dist/tool-system/builtin/web-search.js +36 -3
  256. package/dist/tool-system/builtin/write.d.ts +2 -1
  257. package/dist/tool-system/builtin/write.js +7 -1
  258. package/dist/tool-system/context.d.ts +12 -0
  259. package/dist/tool-system/executor.d.ts +0 -1
  260. package/dist/tool-system/executor.js +34 -40
  261. package/dist/tool-system/investigation-guard.d.ts +4 -1
  262. package/dist/tool-system/investigation-guard.js +19 -3
  263. package/dist/tool-system/mcp-manager.d.ts +36 -12
  264. package/dist/tool-system/mcp-manager.js +173 -10
  265. package/dist/tool-system/path-policy.d.ts +94 -0
  266. package/dist/tool-system/path-policy.js +279 -0
  267. package/dist/tool-system/permission.js +43 -50
  268. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  269. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  270. package/dist/tool-system/registry.js +12 -1
  271. package/dist/tool-system/validation.d.ts +10 -0
  272. package/dist/tool-system/validation.js +16 -3
  273. package/dist/types.d.ts +90 -15
  274. package/dist/utils/format.js +4 -2
  275. package/dist/utils/lockfile.js +9 -2
  276. package/dist/utils/theme.d.ts +6 -0
  277. package/dist/utils/theme.js +4 -2
  278. package/package.json +2 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * History-image stripping for non-vision models.
3
+ *
4
+ * Why this exists: the vision gate in `Engine.run` only rejects *new* image
5
+ * attachments on the current turn. But a conversation can accumulate image
6
+ * content blocks while a vision-capable model is active, then the user
7
+ * switches to a non-vision model (e.g. DeepSeek) mid-session. Those
8
+ * *historical* image blocks are still in the transcript and get re-serialized
9
+ * on every subsequent request — OpenAI-compat backends 400 with
10
+ * `unknown variant 'image_url', expected 'text'`, which then falls back out
11
+ * of streaming and degrades the turn.
12
+ *
13
+ * Fix: when the active model has `supportsVision: false`, replace each
14
+ * message's image blocks with a single text placeholder before the request
15
+ * is built. The transcript/store is untouched — only the outgoing copy is
16
+ * sanitized — so switching back to a vision model still sees the images.
17
+ *
18
+ * Pure function, no I/O. Tested in `strip-vision.test.ts`.
19
+ */
20
+ import type { Message } from "../types.js";
21
+ /** Text we substitute for an elided image block. Surfaced to the model so it
22
+ * knows an image *was* here (vs silently vanishing the context). */
23
+ export declare const VISION_PLACEHOLDER = "[\u56FE\u7247\u5DF2\u7701\u7565:\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u89C6\u89C9\u8F93\u5165]";
24
+ /**
25
+ * Return a copy of `messages` with image content blocks replaced by a text
26
+ * placeholder, but only when `supportsVision` is false.
27
+ *
28
+ * Identity-preserving: when vision is supported, or no message carries an
29
+ * image, the original array (and untouched message objects) are returned as-is
30
+ * so callers pay nothing on the common path.
31
+ */
32
+ export declare function stripVisionFromHistory(messages: Message[], supportsVision: boolean): Message[];
@@ -0,0 +1,94 @@
1
+ /**
2
+ * History-image stripping for non-vision models.
3
+ *
4
+ * Why this exists: the vision gate in `Engine.run` only rejects *new* image
5
+ * attachments on the current turn. But a conversation can accumulate image
6
+ * content blocks while a vision-capable model is active, then the user
7
+ * switches to a non-vision model (e.g. DeepSeek) mid-session. Those
8
+ * *historical* image blocks are still in the transcript and get re-serialized
9
+ * on every subsequent request — OpenAI-compat backends 400 with
10
+ * `unknown variant 'image_url', expected 'text'`, which then falls back out
11
+ * of streaming and degrades the turn.
12
+ *
13
+ * Fix: when the active model has `supportsVision: false`, replace each
14
+ * message's image blocks with a single text placeholder before the request
15
+ * is built. The transcript/store is untouched — only the outgoing copy is
16
+ * sanitized — so switching back to a vision model still sees the images.
17
+ *
18
+ * Pure function, no I/O. Tested in `strip-vision.test.ts`.
19
+ */
20
+ /** Text we substitute for an elided image block. Surfaced to the model so it
21
+ * knows an image *was* here (vs silently vanishing the context). */
22
+ export const VISION_PLACEHOLDER = "[图片已省略:当前模型不支持视觉输入]";
23
+ /**
24
+ * Return a copy of `messages` with image content blocks replaced by a text
25
+ * placeholder, but only when `supportsVision` is false.
26
+ *
27
+ * Identity-preserving: when vision is supported, or no message carries an
28
+ * image, the original array (and untouched message objects) are returned as-is
29
+ * so callers pay nothing on the common path.
30
+ */
31
+ export function stripVisionFromHistory(messages, supportsVision) {
32
+ if (supportsVision)
33
+ return messages;
34
+ let changed = false;
35
+ const out = messages.map((msg) => {
36
+ if (typeof msg.content === "string")
37
+ return msg;
38
+ // Need to process if there's a top-level image, OR a tool_result whose
39
+ // own content array carries a nested image (view_image-produced screenshots
40
+ // live in `tool_result.content: ContentBlock[]`).
41
+ if (!msg.content.some(needsStrip))
42
+ return msg;
43
+ changed = true;
44
+ const rebuilt = [];
45
+ let placeholderInserted = false;
46
+ for (const block of msg.content) {
47
+ if (block.type === "image") {
48
+ // Collapse runs of images into a single placeholder per message —
49
+ // five screenshots don't need five identical notes.
50
+ if (!placeholderInserted) {
51
+ rebuilt.push({ type: "text", text: VISION_PLACEHOLDER });
52
+ placeholderInserted = true;
53
+ }
54
+ continue;
55
+ }
56
+ if (block.type === "tool_result" &&
57
+ Array.isArray(block.content) &&
58
+ block.content.some((b) => b.type === "image")) {
59
+ rebuilt.push({ ...block, content: stripImageBlocks(block.content) });
60
+ continue;
61
+ }
62
+ rebuilt.push(block);
63
+ }
64
+ return { ...msg, content: rebuilt };
65
+ });
66
+ return changed ? out : messages;
67
+ }
68
+ /** True if this block holds an image we must elide — either it *is* an image,
69
+ * or it's a tool_result whose nested content array contains one. */
70
+ function needsStrip(block) {
71
+ if (block.type === "image")
72
+ return true;
73
+ return (block.type === "tool_result" &&
74
+ Array.isArray(block.content) &&
75
+ block.content.some((b) => b.type === "image"));
76
+ }
77
+ /** Replace image blocks in a ContentBlock[] with a single text placeholder
78
+ * (consecutive/multiple images collapse to one), preserving other blocks.
79
+ * Used for both the message top level and nested tool_result.content. */
80
+ function stripImageBlocks(blocks) {
81
+ const rebuilt = [];
82
+ let placeholderInserted = false;
83
+ for (const block of blocks) {
84
+ if (block.type === "image") {
85
+ if (!placeholderInserted) {
86
+ rebuilt.push({ type: "text", text: VISION_PLACEHOLDER });
87
+ placeholderInserted = true;
88
+ }
89
+ continue;
90
+ }
91
+ rebuilt.push(block);
92
+ }
93
+ return rebuilt;
94
+ }
@@ -20,13 +20,12 @@ export interface CreateMessageOptions {
20
20
  */
21
21
  recordUsage?: boolean;
22
22
  /**
23
- * DeepSeek V4 thinking-mode switch. "enabled" is the endpoint default;
24
- * "disabled" turns reasoning off (faster, cheaper, no `reasoning_content`
25
- * on the reply). Ignored by every provider except DeepSeek V4 — the OpenAI
26
- * SDK forwards unknown top-level fields verbatim, so it's harmless on
27
- * other compatible endpoints.
23
+ * Reasoning/thinking setting for this call. Overrides LLMConfig.reasoning.
24
+ * Rich shape ({mode:"off"|"on"} | {mode:"effort",effort} |
25
+ * {mode:"budget",budgetTokens}) translated to the per-vendor wire form by
26
+ * the client's capability layer.
28
27
  */
29
- thinking?: "enabled" | "disabled";
28
+ reasoning?: import("./reasoning-setting.js").ReasoningSetting;
30
29
  }
31
30
  export interface LLMUsageTracker {
32
31
  records: TokenUsage[];
@@ -25,6 +25,7 @@ import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, readFil
25
25
  import { join, dirname } from "node:path";
26
26
  import { homedir } from "node:os";
27
27
  import { AsyncLocalStorage } from "node:async_hooks";
28
+ import { redactSecrets } from "./sanitize-messages.js";
28
29
  const LEVEL_ORDER = {
29
30
  debug: 0,
30
31
  info: 1,
@@ -246,7 +247,14 @@ class Logger {
246
247
  recordError(msg, data) {
247
248
  if (inMemoryErrors.length >= MAX_IN_MEMORY_ERRORS)
248
249
  inMemoryErrors.shift();
249
- inMemoryErrors.push({ msg, t: new Date().toISOString(), data });
250
+ // Scrub secrets out of msg + data before parking the entry in the in-
251
+ // memory ring. The ring is exposed via diagnostics endpoints, so
252
+ // anything that survives here is reachable by protocol clients.
253
+ inMemoryErrors.push({
254
+ msg: redactSecrets(msg),
255
+ t: new Date().toISOString(),
256
+ data: data ? redactSecrets(data) : undefined,
257
+ });
250
258
  }
251
259
  write(level, msg, data) {
252
260
  if (!this.enabled)
@@ -262,7 +270,13 @@ class Logger {
262
270
  return;
263
271
  }
264
272
  const t = new Date().toISOString();
265
- const entry = { t, l: level, msg, ...this.context };
273
+ // Scrub msg + data before the entry is built. msg is a plain string so
274
+ // only bearer-tokens / URL secret query params can hide there; data is
275
+ // arbitrary so it gets the full recursive walk. Done at the boundary so
276
+ // every level/category sink shares the same redaction rules.
277
+ const safeMsg = redactSecrets(msg);
278
+ const safeData = data ? redactSecrets(data) : undefined;
279
+ const entry = { t, l: level, msg: safeMsg, ...this.context };
266
280
  if (cat && !entry.cat)
267
281
  entry.cat = cat;
268
282
  // Stamp the current process-wide session id unless this logger's
@@ -270,8 +284,8 @@ class Logger {
270
284
  // children created before Engine.run() still pick up the real sid.
271
285
  if (entry.sid === undefined)
272
286
  entry.sid = getCurrentSid();
273
- if (data)
274
- entry.d = data;
287
+ if (safeData)
288
+ entry.d = safeData;
275
289
  const line = JSON.stringify(entry) + "\n";
276
290
  if (this.stderrEcho) {
277
291
  process.stderr.write(line);
@@ -32,6 +32,16 @@ export declare function sanitizeContent(content: Message["content"]): Message["c
32
32
  * path for the overwhelmingly common pure-text case).
33
33
  */
34
34
  export declare function sanitizeMessages(messages: readonly Message[]): Message[];
35
+ /**
36
+ * Recursively walk a value and return a *new* value with secret-looking
37
+ * fields replaced by "[redacted]". Used by the logger (so anything ending up
38
+ * in `entry.d` is scrubbed) and by diagnostics / in-memory error capture.
39
+ *
40
+ * Pure / no I/O. Does not mutate the input — important because the same
41
+ * object may also be on its way to a transcript or UI stream where the
42
+ * unredacted form is correct.
43
+ */
44
+ export declare function redactSecrets<T>(value: T, depth?: number): T;
35
45
  /**
36
46
  * Sanitize a free-form task string before logging.
37
47
  *
@@ -129,6 +129,96 @@ export function sanitizeMessages(messages) {
129
129
  }
130
130
  return touched ? out : messages;
131
131
  }
132
+ // ─────────────────────────────────────────────────────────────────────
133
+ // Secret redaction
134
+ // ─────────────────────────────────────────────────────────────────────
135
+ //
136
+ // Beyond image payloads, log/diagnostics entries can carry API keys,
137
+ // authorization headers, provider tokens, and similar secrets via
138
+ // `data` blobs (settings dumps, provider configs, error responses, …).
139
+ // These walkers redact secrets *in place on a deep clone* so the caller
140
+ // can still log "useful shape, scrubbed values".
141
+ const SECRET_KEY_RE = /(^|[._-])(api[_-]?key|authorization|x[_-]api[_-]key|bearer[_-]?token|access[_-]?token|refresh[_-]?token|session[_-]?token|token|secret|password|client[_-]?secret|cookie)($|[._-])/i;
142
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=~]{8,}/g;
143
+ // URL query parameters that look like credentials. Conservative — only
144
+ // trigger on a small named set so a normal `?id=…&q=…` URL passes through.
145
+ const URL_SECRET_QS_RE = /([?&](?:key|api_key|apikey|access_token|token|auth|sig|signature)=)[^&#\s]+/gi;
146
+ const REDACTED = "[redacted]";
147
+ const MAX_DEPTH = 10;
148
+ function isSecretKey(key) {
149
+ return SECRET_KEY_RE.test(key);
150
+ }
151
+ /** Scrub bare `Bearer <token>` and credential-looking URL query params from a string. */
152
+ function redactSecretsInString(s) {
153
+ let out = s;
154
+ if (BEARER_RE.test(out))
155
+ out = out.replace(BEARER_RE, "Bearer [redacted]");
156
+ if (URL_SECRET_QS_RE.test(out))
157
+ out = out.replace(URL_SECRET_QS_RE, "$1[redacted]");
158
+ return out;
159
+ }
160
+ /**
161
+ * Recursively walk a value and return a *new* value with secret-looking
162
+ * fields replaced by "[redacted]". Used by the logger (so anything ending up
163
+ * in `entry.d` is scrubbed) and by diagnostics / in-memory error capture.
164
+ *
165
+ * Pure / no I/O. Does not mutate the input — important because the same
166
+ * object may also be on its way to a transcript or UI stream where the
167
+ * unredacted form is correct.
168
+ */
169
+ export function redactSecrets(value, depth = 0) {
170
+ if (value === null || value === undefined)
171
+ return value;
172
+ if (depth > MAX_DEPTH)
173
+ return value;
174
+ const t = typeof value;
175
+ if (t === "string")
176
+ return redactSecretsInString(value);
177
+ if (t === "number" || t === "boolean" || t === "bigint" || t === "symbol")
178
+ return value;
179
+ if (t === "function")
180
+ return value;
181
+ if (Array.isArray(value)) {
182
+ return value.map((v) => redactSecrets(v, depth + 1));
183
+ }
184
+ // Error objects: preserve name/message/stack shape but redact within.
185
+ if (value instanceof Error) {
186
+ const clone = {
187
+ name: value.name,
188
+ message: redactSecretsInString(value.message),
189
+ ...(value.stack ? { stack: redactSecretsInString(value.stack) } : {}),
190
+ };
191
+ // Some libraries attach extra properties (`.response`, `.config`, …) —
192
+ // walk them as well.
193
+ for (const key of Object.keys(value)) {
194
+ if (key === "name" || key === "message" || key === "stack")
195
+ continue;
196
+ const v = value[key];
197
+ clone[key] = isSecretKey(key) ? REDACTED : redactSecrets(v, depth + 1);
198
+ }
199
+ return clone;
200
+ }
201
+ if (t === "object") {
202
+ const obj = value;
203
+ const out = {};
204
+ for (const [k, v] of Object.entries(obj)) {
205
+ if (isSecretKey(k)) {
206
+ // Preserve presence (null/undefined stay as-is so consumers can
207
+ // still distinguish "key present" vs "key absent"; non-empty values
208
+ // collapse to [redacted]).
209
+ if (v === null || v === undefined || v === "")
210
+ out[k] = v;
211
+ else
212
+ out[k] = REDACTED;
213
+ }
214
+ else {
215
+ out[k] = redactSecrets(v, depth + 1);
216
+ }
217
+ }
218
+ return out;
219
+ }
220
+ return value;
221
+ }
132
222
  /**
133
223
  * Sanitize a free-form task string before logging.
134
224
  *
@@ -10,7 +10,12 @@ export class LSPClient extends EventEmitter {
10
10
  process;
11
11
  nextId = 1;
12
12
  pending = new Map();
13
- buffer = "";
13
+ // The LSP framing uses byte-counted Content-Length, so we buffer bytes,
14
+ // not characters. Joining chunks as strings (`data.toString()` + string
15
+ // concat) can split a multibyte UTF-8 codepoint across chunks and de-
16
+ // synchronize against Content-Length. Stay in Buffer-land until we've
17
+ // sliced out exactly the right body length.
18
+ buffer = Buffer.alloc(0);
14
19
  contentLength = -1;
15
20
  initialized = false;
16
21
  constructor(command, args = [], cwd) {
@@ -24,7 +29,7 @@ export class LSPClient extends EventEmitter {
24
29
  cwd: this.cwd,
25
30
  stdio: ["pipe", "pipe", "pipe"],
26
31
  });
27
- this.process.stdout?.on("data", (data) => this.handleData(data.toString()));
32
+ this.process.stdout?.on("data", (data) => this.handleData(data));
28
33
  this.process.stderr?.on("data", (_data) => {
29
34
  // LSP servers often log to stderr — ignore
30
35
  });
@@ -116,26 +121,41 @@ export class LSPClient extends EventEmitter {
116
121
  return this.process !== undefined && !this.process.killed;
117
122
  }
118
123
  handleData(data) {
119
- this.buffer += data;
124
+ // Append the new chunk as bytes. Buffer.concat avoids the UTF-8
125
+ // boundary corruption that string concatenation produces — a
126
+ // multibyte codepoint split across two TCP/pipe chunks decodes to
127
+ // U+FFFD, mangles the buffer length count, and breaks framing.
128
+ this.buffer = Buffer.concat([this.buffer, data]);
120
129
  while (true) {
121
130
  if (this.contentLength < 0) {
131
+ // Search for the header terminator in bytes, not chars.
122
132
  const headerEnd = this.buffer.indexOf("\r\n\r\n");
123
133
  if (headerEnd < 0)
124
134
  break;
125
- const header = this.buffer.slice(0, headerEnd);
135
+ // Headers are ASCII per spec, so toString("ascii") is safe and
136
+ // avoids paying for a UTF-8 decode on the framing band.
137
+ const header = this.buffer.slice(0, headerEnd).toString("ascii");
126
138
  const match = header.match(/Content-Length:\s*(\d+)/i);
127
139
  if (!match) {
128
- this.buffer = this.buffer.slice(headerEnd + 4);
140
+ this.buffer = this.buffer.subarray(headerEnd + 4);
129
141
  continue;
130
142
  }
131
143
  this.contentLength = parseInt(match[1], 10);
132
- this.buffer = this.buffer.slice(headerEnd + 4);
144
+ this.buffer = this.buffer.subarray(headerEnd + 4);
133
145
  }
146
+ // Content-Length is a BYTE count — compare against the buffer's
147
+ // byte length, not its character length. Pre-fix this compared
148
+ // string lengths; with multibyte responses (e.g. CJK identifiers,
149
+ // emoji in diagnostics) we'd block waiting for bytes that had
150
+ // already arrived.
134
151
  if (this.buffer.length < this.contentLength)
135
152
  break;
136
- const content = this.buffer.slice(0, this.contentLength);
137
- this.buffer = this.buffer.slice(this.contentLength);
153
+ const bodyBytes = this.buffer.subarray(0, this.contentLength);
154
+ this.buffer = this.buffer.subarray(this.contentLength);
138
155
  this.contentLength = -1;
156
+ // Decode the full body in one shot — at this point we have the
157
+ // exact byte count, so the UTF-8 boundary issue is fully gone.
158
+ const content = bodyBytes.toString("utf8");
139
159
  try {
140
160
  const message = JSON.parse(content);
141
161
  if ("id" in message && this.pending.has(message.id)) {
@@ -6,6 +6,7 @@ import { LSPClient } from "./client.js";
6
6
  import { BUILTIN_LSP_SERVERS } from "./servers.js";
7
7
  import { execSync } from "node:child_process";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { rootUriToPath } from "./root-path.js";
9
10
  export class LSPServerManager {
10
11
  servers = new Map();
11
12
  rootUri;
@@ -49,7 +50,7 @@ export class LSPServerManager {
49
50
  }
50
51
  managed.state = "starting";
51
52
  try {
52
- const client = new LSPClient(managed.config.command, managed.config.args, this.rootUri.replace("file://", ""));
53
+ const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
53
54
  await client.start();
54
55
  await client.initialize(this.rootUri);
55
56
  managed.client = client;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Convert a `file://` root URI to a platform filesystem path.
3
+ *
4
+ * The LSP manager used `rootUri.replace("file://", "")`, which only strips the
5
+ * first occurrence and leaves percent-encoding intact, and on Windows turns
6
+ * `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
7
+ * platform conversion (drive letters, percent-decoding, UNC).
8
+ */
9
+ export declare function rootUriToPath(rootUri: string): string;
@@ -0,0 +1,12 @@
1
+ import { fileURLToPath } from "node:url";
2
+ /**
3
+ * Convert a `file://` root URI to a platform filesystem path.
4
+ *
5
+ * The LSP manager used `rootUri.replace("file://", "")`, which only strips the
6
+ * first occurrence and leaves percent-encoding intact, and on Windows turns
7
+ * `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
8
+ * platform conversion (drive letters, percent-decoding, UNC).
9
+ */
10
+ export function rootUriToPath(rootUri) {
11
+ return fileURLToPath(rootUri);
12
+ }
@@ -4,9 +4,9 @@
4
4
  * (see src/ui/components/OnboardingPrompt.tsx); this module only exposes
5
5
  * pure(-ish) helpers it consumes, so there's a single input stack.
6
6
  */
7
- import { mkdirSync, writeFileSync, readFileSync, existsSync, renameSync } from "node:fs";
7
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, renameSync, rmSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { homedir } from "node:os";
9
+ import { userHome } from "./settings/manager.js";
10
10
  import { getOpenRouterModels } from "./data/openrouter-models.js";
11
11
  import { sanitizeApiKey } from "./llm/api-key-sanitize.js";
12
12
  export const PROVIDERS = [
@@ -257,8 +257,13 @@ export function resolveApiKey(optionsApiKey, settingsApiKey) {
257
257
  if (!p.envKey)
258
258
  continue;
259
259
  const v = process.env[p.envKey];
260
- if (v && v.trim())
261
- return v.trim();
260
+ if (!v)
261
+ continue;
262
+ // Same sanitization boundary as detectEnvKeys — env-sourced keys carry
263
+ // CRLF/BOM/zero-width/control chars; .trim() alone misses interior ones.
264
+ const cleaned = sanitizeApiKey(v).value;
265
+ if (cleaned)
266
+ return cleaned;
262
267
  }
263
268
  return undefined;
264
269
  }
@@ -272,7 +277,7 @@ export function resolveApiKey(optionsApiKey, settingsApiKey) {
272
277
  * then settings.model (legacy top-level). undefined when nothing matches.
273
278
  */
274
279
  export function findSavedKeyForProvider(provider) {
275
- const file = join(homedir(), ".code-shell", "settings.json");
280
+ const file = join(userHome(), ".code-shell", "settings.json");
276
281
  if (!existsSync(file))
277
282
  return undefined;
278
283
  let data;
@@ -304,7 +309,7 @@ export function findSavedKeyForProvider(provider) {
304
309
  * just the provider's first model.
305
310
  */
306
311
  export function loadSavedModelsForProvider(provider) {
307
- const file = join(homedir(), ".code-shell", "settings.json");
312
+ const file = join(userHome(), ".code-shell", "settings.json");
308
313
  if (!existsSync(file))
309
314
  return [];
310
315
  let data;
@@ -334,7 +339,7 @@ export function hasApiKey() {
334
339
  // /logout look like a no-op when the others still held credentials.
335
340
  // Reads ~/.code-shell/ only — ~/.claude/ compat was dropped because Claude
336
341
  // Code's settings schema diverges and merging broke boot.
337
- const p = join(homedir(), ".code-shell", "settings.json");
342
+ const p = join(userHome(), ".code-shell", "settings.json");
338
343
  if (existsSync(p)) {
339
344
  try {
340
345
  const data = JSON.parse(readFileSync(p, "utf-8"));
@@ -501,7 +506,7 @@ export function modelDisplayName(model) {
501
506
  return base.charAt(0).toUpperCase() + base.slice(1);
502
507
  }
503
508
  export function saveSettings(result, providerDef, poolModels) {
504
- const dir = join(homedir(), ".code-shell");
509
+ const dir = join(userHome(), ".code-shell");
505
510
  const file = join(dir, "settings.json");
506
511
  mkdirSync(dir, { recursive: true });
507
512
  let existing = {};
@@ -551,7 +556,7 @@ export function saveSettings(result, providerDef, poolModels) {
551
556
  * Writes atomically via tmp+rename.
552
557
  */
553
558
  export function appendOnboardingResult(opts) {
554
- const dir = join(homedir(), ".code-shell");
559
+ const dir = join(userHome(), ".code-shell");
555
560
  const file = join(dir, "settings.json");
556
561
  mkdirSync(dir, { recursive: true });
557
562
  let existing = {};
@@ -602,13 +607,15 @@ export function appendOnboardingResult(opts) {
602
607
  renameSync(tmp, file);
603
608
  }
604
609
  catch {
605
- // Fallback: best-effort direct write if rename fails (e.g. cross-device).
610
+ // Fallback: best-effort direct write if rename fails (e.g. cross-device),
611
+ // then remove the orphaned temp file the failed rename left behind.
606
612
  writeFileSync(file, JSON.stringify(updated, null, 2) + "\n", "utf-8");
613
+ rmSync(tmp, { force: true });
607
614
  }
608
615
  }
609
616
  // saveArenaSettings removed — replaced by saveArenaSettingsByKeys
610
617
  export function saveArenaSettingsByKeys(keys) {
611
- const dir = join(homedir(), ".code-shell");
618
+ const dir = join(userHome(), ".code-shell");
612
619
  const file = join(dir, "settings.json");
613
620
  let existing = {};
614
621
  if (existsSync(file)) {
@@ -21,9 +21,43 @@ export type GitResult = {
21
21
  ok: false;
22
22
  error: string;
23
23
  };
24
+ /**
25
+ * Force git to never prompt interactively. gitOps runs in headless contexts
26
+ * (the Electron main process, the agent worker) that have no TTY, so a
27
+ * private/auth-required clone or an unknown SSH host key would otherwise make
28
+ * git BLOCK on a credential/host-key prompt — the operation appears to hang
29
+ * until the safeSpawn timeout (or an off-screen askpass dialog). With these
30
+ * set, git fails fast with an auth error instead.
31
+ *
32
+ * - GIT_TERMINAL_PROMPT=0 — git's own username/password prompt becomes an error.
33
+ * - GIT_SSH_COMMAND BatchMode=yes — ssh never asks for a password/passphrase;
34
+ * accept-new auto-trusts a first-seen host key (instead of the y/n prompt)
35
+ * but still rejects a changed key. Not overridden if the caller already
36
+ * provides GIT_SSH_COMMAND (e.g. a custom identity file).
37
+ */
38
+ export declare function nonInteractiveGitEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
39
+ /**
40
+ * Clone a marketplace repo cheaply. These repos can bundle hundreds of plugins
41
+ * (thousands of files), but adding a marketplace only needs its manifest — the
42
+ * individual plugin trees are pulled on demand at install time
43
+ * ({@link gitSparseCheckoutAdd}). So we do a blobless, no-checkout clone and
44
+ * then sparse-check out only the manifest directories.
45
+ *
46
+ * `sparsePaths` defaults to the two manifest locations we support
47
+ * (.claude-plugin and .agents/plugins). On any failure of the sparse setup we
48
+ * fall back to checking out the full tree, so correctness never depends on
49
+ * sparse support being present.
50
+ */
24
51
  export declare function gitClone(url: string, destDir: string, options?: {
25
52
  ref?: string;
53
+ sparsePaths?: string[];
26
54
  }): Promise<GitResult>;
55
+ /**
56
+ * Expand a sparse-checkout to include an additional path (a plugin subdir),
57
+ * materializing its blobs. Best-effort: if the repo isn't sparse this errors
58
+ * harmlessly and the caller proceeds (the files are already present).
59
+ */
60
+ export declare function gitSparseCheckoutAdd(repoDir: string, relPath: string): Promise<GitResult>;
27
61
  export declare function gitRevParseHead(repoDir: string): Promise<GitResult>;
28
62
  export declare function gitFetchAndReset(repoDir: string, ref?: string): Promise<GitResult>;
29
63
  /**
@@ -15,10 +15,31 @@
15
15
  * classification. See docs/architecture/17 for the trust framing.
16
16
  */
17
17
  import { safeSpawn } from "../runtime/safe-spawn.js";
18
+ /**
19
+ * Force git to never prompt interactively. gitOps runs in headless contexts
20
+ * (the Electron main process, the agent worker) that have no TTY, so a
21
+ * private/auth-required clone or an unknown SSH host key would otherwise make
22
+ * git BLOCK on a credential/host-key prompt — the operation appears to hang
23
+ * until the safeSpawn timeout (or an off-screen askpass dialog). With these
24
+ * set, git fails fast with an auth error instead.
25
+ *
26
+ * - GIT_TERMINAL_PROMPT=0 — git's own username/password prompt becomes an error.
27
+ * - GIT_SSH_COMMAND BatchMode=yes — ssh never asks for a password/passphrase;
28
+ * accept-new auto-trusts a first-seen host key (instead of the y/n prompt)
29
+ * but still rejects a changed key. Not overridden if the caller already
30
+ * provides GIT_SSH_COMMAND (e.g. a custom identity file).
31
+ */
32
+ export function nonInteractiveGitEnv(base) {
33
+ return {
34
+ ...base,
35
+ GIT_TERMINAL_PROMPT: "0",
36
+ GIT_SSH_COMMAND: base.GIT_SSH_COMMAND ?? "ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new",
37
+ };
38
+ }
18
39
  async function runGit(args, cwd, timeoutMs = 60_000) {
19
40
  const r = await safeSpawn("git", args, {
20
41
  cwd: cwd ?? process.cwd(),
21
- env: process.env,
42
+ env: nonInteractiveGitEnv(process.env),
22
43
  timeoutMs,
23
44
  });
24
45
  if (r.spawnFailed) {
@@ -35,12 +56,47 @@ async function runGit(args, cwd, timeoutMs = 60_000) {
35
56
  error: `git ${args.join(" ")} exited ${r.exitCode}: ${r.stderr.trim() || "unknown error"}`,
36
57
  };
37
58
  }
59
+ /**
60
+ * Clone a marketplace repo cheaply. These repos can bundle hundreds of plugins
61
+ * (thousands of files), but adding a marketplace only needs its manifest — the
62
+ * individual plugin trees are pulled on demand at install time
63
+ * ({@link gitSparseCheckoutAdd}). So we do a blobless, no-checkout clone and
64
+ * then sparse-check out only the manifest directories.
65
+ *
66
+ * `sparsePaths` defaults to the two manifest locations we support
67
+ * (.claude-plugin and .agents/plugins). On any failure of the sparse setup we
68
+ * fall back to checking out the full tree, so correctness never depends on
69
+ * sparse support being present.
70
+ */
38
71
  export async function gitClone(url, destDir, options) {
39
- const args = ["clone", "--depth", "1"];
72
+ const sparsePaths = options?.sparsePaths ?? [".claude-plugin", ".agents/plugins"];
73
+ const args = ["clone", "--depth", "1", "--filter=blob:none", "--no-checkout"];
40
74
  if (options?.ref)
41
75
  args.push("--branch", options.ref);
42
76
  args.push("--", url, destDir);
43
- return runGit(args);
77
+ const cloned = await runGit(args);
78
+ if (!cloned.ok)
79
+ return cloned;
80
+ // Restrict the working tree to the manifest dirs, then check out. Use
81
+ // non-cone mode so nested paths like ".agents/plugins" match exactly.
82
+ const init = await runGit(["sparse-checkout", "set", "--no-cone", ...sparsePaths], destDir);
83
+ if (!init.ok) {
84
+ // Sparse unsupported/failed — fall back to a normal full checkout so the
85
+ // manifest is still present.
86
+ return runGit(["checkout"], destDir);
87
+ }
88
+ const checkout = await runGit(["checkout"], destDir);
89
+ if (!checkout.ok)
90
+ return checkout;
91
+ return cloned;
92
+ }
93
+ /**
94
+ * Expand a sparse-checkout to include an additional path (a plugin subdir),
95
+ * materializing its blobs. Best-effort: if the repo isn't sparse this errors
96
+ * harmlessly and the caller proceeds (the files are already present).
97
+ */
98
+ export async function gitSparseCheckoutAdd(repoDir, relPath) {
99
+ return runGit(["sparse-checkout", "add", relPath], repoDir);
44
100
  }
45
101
  export async function gitRevParseHead(repoDir) {
46
102
  return runGit(["rev-parse", "HEAD"], repoDir);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Convert one Codex agent TOML into a CC agent Markdown string.
3
+ * Real CC fields: name, description, model, body(=developer_instructions).
4
+ * Every other field is preserved with a `codex_` prefix (v1 inert; see spec §7.1).
5
+ * `mcp_servers` values are rewritten to `<plugin>:<server>` to match the MCP merge key.
6
+ */
7
+ export declare function convertCodexAgentToml(toml: string, sourceName: string, pluginName: string): string;