@cjhyy/code-shell-core 0.5.0-rc.0 → 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 (283) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +29 -0
  2. package/dist/agent/agent-definition-registry.js +74 -0
  3. package/dist/agent/agent-definition.d.ts +37 -0
  4. package/dist/agent/agent-definition.js +59 -0
  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 +118 -18
  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 +256 -16
  68. package/dist/engine/engine.js +856 -200
  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 +32 -7
  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 +10 -0
  171. package/dist/prompt/composer.js +29 -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 +45 -1
  220. package/dist/settings/manager.js +121 -17
  221. package/dist/settings/manager.test.d.ts +1 -0
  222. package/dist/settings/manager.test.js +73 -0
  223. package/dist/settings/personalization.d.ts +19 -0
  224. package/dist/settings/personalization.js +7 -0
  225. package/dist/settings/schema.d.ts +1163 -126
  226. package/dist/settings/schema.js +126 -10
  227. package/dist/skills/scanner.d.ts +13 -6
  228. package/dist/skills/scanner.js +23 -4
  229. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  230. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  231. package/dist/tool-system/builtin/agent-registry.d.ts +15 -0
  232. package/dist/tool-system/builtin/agent-registry.js +14 -4
  233. package/dist/tool-system/builtin/agent.d.ts +55 -0
  234. package/dist/tool-system/builtin/agent.js +165 -17
  235. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  236. package/dist/tool-system/builtin/arena.js +6 -7
  237. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  238. package/dist/tool-system/builtin/complete-goal.js +45 -0
  239. package/dist/tool-system/builtin/cron.js +53 -9
  240. package/dist/tool-system/builtin/edit.d.ts +2 -1
  241. package/dist/tool-system/builtin/edit.js +6 -1
  242. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  243. package/dist/tool-system/builtin/generate-image.js +146 -0
  244. package/dist/tool-system/builtin/glob.js +7 -0
  245. package/dist/tool-system/builtin/grep.js +6 -0
  246. package/dist/tool-system/builtin/index.d.ts +18 -1
  247. package/dist/tool-system/builtin/index.js +62 -1
  248. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  249. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  250. package/dist/tool-system/builtin/read.d.ts +2 -1
  251. package/dist/tool-system/builtin/read.js +9 -1
  252. package/dist/tool-system/builtin/skill.js +23 -5
  253. package/dist/tool-system/builtin/sleep.js +9 -3
  254. package/dist/tool-system/builtin/task.js +4 -1
  255. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  256. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  257. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  258. package/dist/tool-system/builtin/view-image.js +96 -0
  259. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  260. package/dist/tool-system/builtin/web-search.js +36 -3
  261. package/dist/tool-system/builtin/write.d.ts +2 -1
  262. package/dist/tool-system/builtin/write.js +7 -1
  263. package/dist/tool-system/context.d.ts +43 -0
  264. package/dist/tool-system/executor.d.ts +0 -1
  265. package/dist/tool-system/executor.js +34 -40
  266. package/dist/tool-system/investigation-guard.d.ts +4 -1
  267. package/dist/tool-system/investigation-guard.js +19 -3
  268. package/dist/tool-system/mcp-manager.d.ts +36 -12
  269. package/dist/tool-system/mcp-manager.js +173 -10
  270. package/dist/tool-system/path-policy.d.ts +94 -0
  271. package/dist/tool-system/path-policy.js +279 -0
  272. package/dist/tool-system/permission.js +43 -50
  273. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  274. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  275. package/dist/tool-system/registry.js +12 -1
  276. package/dist/tool-system/validation.d.ts +10 -0
  277. package/dist/tool-system/validation.js +16 -3
  278. package/dist/types.d.ts +90 -15
  279. package/dist/utils/format.js +4 -2
  280. package/dist/utils/lockfile.js +9 -2
  281. package/dist/utils/theme.d.ts +6 -0
  282. package/dist/utils/theme.js +4 -2
  283. package/package.json +2 -1
@@ -4,10 +4,11 @@
4
4
  * source types (path / git / github / git-subdir), no dependencies, no
5
5
  * sparse-checkout, no npm/pip.
6
6
  */
7
- import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, } from "node:fs";
8
- import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { existsSync, realpathSync, rmSync } from "node:fs";
8
+ import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
9
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
9
10
  import { homedir, tmpdir } from "node:os";
10
- import { gitClone, gitRevParseHead, githubRepoToCloneUrl } from "./gitOps.js";
11
+ import { gitClone, gitRevParseHead, gitSparseCheckoutAdd, githubRepoToCloneUrl, } from "./gitOps.js";
11
12
  import { loadMarketplace } from "./marketplaceManager.js";
12
13
  import { readKnownMarketplaces } from "./knownMarketplaces.js";
13
14
  import { appendInstallEntry, pluginInstallKey, readInstalledPlugins, removeInstallEntries, } from "./installedPlugins.js";
@@ -21,6 +22,47 @@ function pluginCacheRoot() {
21
22
  function pluginCacheDir(marketplace, plugin, version) {
22
23
  return join(pluginCacheRoot(), marketplace, plugin, version);
23
24
  }
25
+ /**
26
+ * Resolve a candidate `installPath` to a realpath and verify it lives strictly
27
+ * underneath the plugin cache root. Returns the resolved path on success, or
28
+ * null when the path is missing, escapes the cache, equals the cache root
29
+ * itself, or is otherwise unsafe to remove.
30
+ *
31
+ * Exported so tests can exercise the safety predicate directly without
32
+ * arranging a full uninstall — the contract this defends (no rmSync outside
33
+ * the plugin cache) is the entire point of T2 / Workstream A2.
34
+ */
35
+ export function resolveSafePluginPath(installPath, cacheRoot) {
36
+ if (typeof installPath !== "string" || installPath.length === 0)
37
+ return null;
38
+ // Realpath the cache root once. We require it to exist; if the cache root
39
+ // itself can't be resolved, refuse all deletions rather than fall back to
40
+ // a string-only check.
41
+ let safeCacheRoot;
42
+ try {
43
+ safeCacheRoot = realpathSync(cacheRoot);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ // Realpath the target. A missing target (dangling symlink, already removed,
49
+ // typo in installed_plugins.json) returns null — caller will skip silently.
50
+ let resolvedTarget;
51
+ try {
52
+ resolvedTarget = realpathSync(installPath);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ // Strict containment: must start with cacheRoot + separator. Equal-to-root
58
+ // is explicitly rejected so a tampered entry can't wipe the whole cache.
59
+ const rootWithSep = safeCacheRoot.endsWith(sep) ? safeCacheRoot : safeCacheRoot + sep;
60
+ if (resolvedTarget === safeCacheRoot)
61
+ return null;
62
+ if (!resolvedTarget.startsWith(rootWithSep))
63
+ return null;
64
+ return resolvedTarget;
65
+ }
24
66
  async function materializePath(marketplaceInstallLocation, relativePath, cacheTarget) {
25
67
  const src = isAbsolute(relativePath)
26
68
  ? relativePath
@@ -28,16 +70,16 @@ async function materializePath(marketplaceInstallLocation, relativePath, cacheTa
28
70
  if (!existsSync(src)) {
29
71
  return { ok: false, error: `plugin source path "${relativePath}" not found in marketplace` };
30
72
  }
31
- mkdirSync(dirname(cacheTarget), { recursive: true });
73
+ await mkdir(dirname(cacheTarget), { recursive: true });
32
74
  if (existsSync(cacheTarget))
33
- rmSync(cacheTarget, { recursive: true, force: true });
34
- cpSync(src, cacheTarget, { recursive: true });
75
+ await rm(cacheTarget, { recursive: true, force: true });
76
+ await cp(src, cacheTarget, { recursive: true });
35
77
  return { ok: true, sha: undefined };
36
78
  }
37
79
  async function materializeGit(url, ref, cacheTarget) {
38
- mkdirSync(dirname(cacheTarget), { recursive: true });
80
+ await mkdir(dirname(cacheTarget), { recursive: true });
39
81
  if (existsSync(cacheTarget))
40
- rmSync(cacheTarget, { recursive: true, force: true });
82
+ await rm(cacheTarget, { recursive: true, force: true });
41
83
  const clone = await gitClone(url, cacheTarget, ref ? { ref } : undefined);
42
84
  if (!clone.ok)
43
85
  return { ok: false, error: clone.error };
@@ -48,7 +90,7 @@ async function materializeGit(url, ref, cacheTarget) {
48
90
  }
49
91
  async function materializeGitSubdir(url, subPath, ref, cacheTarget) {
50
92
  // Clone to a tempdir, then copy the subdir over.
51
- const tmp = mkdtempSync(join(tmpdir(), "plugin-clone-"));
93
+ const tmp = await mkdtemp(join(tmpdir(), "plugin-clone-"));
52
94
  try {
53
95
  const clone = await gitClone(url, tmp + "/repo", ref ? { ref } : undefined);
54
96
  if (!clone.ok)
@@ -63,14 +105,14 @@ async function materializeGitSubdir(url, subPath, ref, cacheTarget) {
63
105
  error: `git-subdir path "${subPath}" not found in cloned repository`,
64
106
  };
65
107
  }
66
- mkdirSync(dirname(cacheTarget), { recursive: true });
108
+ await mkdir(dirname(cacheTarget), { recursive: true });
67
109
  if (existsSync(cacheTarget))
68
- rmSync(cacheTarget, { recursive: true, force: true });
69
- cpSync(src, cacheTarget, { recursive: true });
110
+ await rm(cacheTarget, { recursive: true, force: true });
111
+ await cp(src, cacheTarget, { recursive: true });
70
112
  return { ok: true, sha: head.stdout };
71
113
  }
72
114
  finally {
73
- rmSync(tmp, { recursive: true, force: true });
115
+ await rm(tmp, { recursive: true, force: true });
74
116
  }
75
117
  }
76
118
  function shortSha(sha) {
@@ -78,20 +120,49 @@ function shortSha(sha) {
78
120
  return "unknown";
79
121
  return sha.slice(0, 12);
80
122
  }
123
+ /**
124
+ * Compare a marketplace-declared SHA against the cloned HEAD. The
125
+ * declaration is allowed to be a short prefix (>= 7 chars, standard git
126
+ * abbreviation length); for stricter integrity guarantees marketplaces
127
+ * should pin the full 40-char hash. Case-insensitive.
128
+ *
129
+ * Exported so a unit test can pin the policy without spinning up a real
130
+ * clone.
131
+ */
132
+ export function shaMatches(declared, actual) {
133
+ if (typeof declared !== "string" || typeof actual !== "string")
134
+ return false;
135
+ const d = declared.trim().toLowerCase();
136
+ const a = actual.trim().toLowerCase();
137
+ if (d.length < 7)
138
+ return false; // refuse 6-or-less; too collision-prone
139
+ if (d.length > 40)
140
+ return false; // not a valid SHA
141
+ return a.startsWith(d);
142
+ }
81
143
  async function materialize(source, marketplaceInstallLocation, marketplace, plugin) {
82
144
  // First pass: install into a placeholder dir, then rename to the SHA dir
83
145
  // once we know it. For path sources there's no SHA — use "local".
84
146
  const placeholder = pluginCacheDir(marketplace, plugin, "_pending_");
85
147
  if (typeof source === "string") {
148
+ // The marketplace was cloned sparse (only the manifest dirs are on disk),
149
+ // so a local-path plugin's tree may not be checked out yet. Expand the
150
+ // sparse-checkout to include it before reading. Best-effort: on a
151
+ // non-sparse (full) clone this errors harmlessly and the files are already
152
+ // present, so we ignore the result and let materializePath report a real
153
+ // missing-path error if the subdir truly isn't there.
154
+ if (!isAbsolute(source)) {
155
+ await gitSparseCheckoutAdd(marketplaceInstallLocation, source);
156
+ }
86
157
  const r = await materializePath(marketplaceInstallLocation, source, placeholder);
87
158
  if (!r.ok)
88
159
  return r;
89
160
  const finalDir = pluginCacheDir(marketplace, plugin, "local");
90
161
  if (existsSync(finalDir))
91
- rmSync(finalDir, { recursive: true, force: true });
92
- mkdirSync(dirname(finalDir), { recursive: true });
93
- cpSync(placeholder, finalDir, { recursive: true });
94
- rmSync(placeholder, { recursive: true, force: true });
162
+ await rm(finalDir, { recursive: true, force: true });
163
+ await mkdir(dirname(finalDir), { recursive: true });
164
+ await cp(placeholder, finalDir, { recursive: true });
165
+ await rm(placeholder, { recursive: true, force: true });
95
166
  return { ok: true, cacheDir: finalDir, version: "local", sha: undefined };
96
167
  }
97
168
  if (source.source === "git" || source.source === "github") {
@@ -99,32 +170,51 @@ async function materialize(source, marketplaceInstallLocation, marketplace, plug
99
170
  const r = await materializeGit(url, source.ref, placeholder);
100
171
  if (!r.ok) {
101
172
  if (existsSync(placeholder))
102
- rmSync(placeholder, { recursive: true, force: true });
173
+ await rm(placeholder, { recursive: true, force: true });
103
174
  return r;
104
175
  }
176
+ // Supply-chain check: when the marketplace entry pins a SHA, fail the
177
+ // install if the cloned HEAD doesn't match. Without this the `sha`
178
+ // field is decorative — present in metadata, never enforced.
179
+ if (source.sha && !shaMatches(source.sha, r.sha)) {
180
+ if (existsSync(placeholder))
181
+ await rm(placeholder, { recursive: true, force: true });
182
+ return {
183
+ ok: false,
184
+ error: `sha mismatch for ${plugin}: expected ${source.sha}, got ${r.sha}`,
185
+ };
186
+ }
105
187
  const version = shortSha(r.sha);
106
188
  const finalDir = pluginCacheDir(marketplace, plugin, version);
107
189
  if (existsSync(finalDir))
108
- rmSync(finalDir, { recursive: true, force: true });
109
- mkdirSync(dirname(finalDir), { recursive: true });
110
- cpSync(placeholder, finalDir, { recursive: true });
111
- rmSync(placeholder, { recursive: true, force: true });
190
+ await rm(finalDir, { recursive: true, force: true });
191
+ await mkdir(dirname(finalDir), { recursive: true });
192
+ await cp(placeholder, finalDir, { recursive: true });
193
+ await rm(placeholder, { recursive: true, force: true });
112
194
  return { ok: true, cacheDir: finalDir, version, sha: r.sha };
113
195
  }
114
196
  if (source.source === "git-subdir") {
115
197
  const r = await materializeGitSubdir(source.url, source.path, source.ref, placeholder);
116
198
  if (!r.ok) {
117
199
  if (existsSync(placeholder))
118
- rmSync(placeholder, { recursive: true, force: true });
200
+ await rm(placeholder, { recursive: true, force: true });
119
201
  return r;
120
202
  }
203
+ if (source.sha && !shaMatches(source.sha, r.sha)) {
204
+ if (existsSync(placeholder))
205
+ await rm(placeholder, { recursive: true, force: true });
206
+ return {
207
+ ok: false,
208
+ error: `sha mismatch for ${plugin}: expected ${source.sha}, got ${r.sha}`,
209
+ };
210
+ }
121
211
  const version = shortSha(r.sha);
122
212
  const finalDir = pluginCacheDir(marketplace, plugin, version);
123
213
  if (existsSync(finalDir))
124
- rmSync(finalDir, { recursive: true, force: true });
125
- mkdirSync(dirname(finalDir), { recursive: true });
126
- cpSync(placeholder, finalDir, { recursive: true });
127
- rmSync(placeholder, { recursive: true, force: true });
214
+ await rm(finalDir, { recursive: true, force: true });
215
+ await mkdir(dirname(finalDir), { recursive: true });
216
+ await cp(placeholder, finalDir, { recursive: true });
217
+ await rm(placeholder, { recursive: true, force: true });
128
218
  return { ok: true, cacheDir: finalDir, version, sha: r.sha };
129
219
  }
130
220
  return { ok: false, error: `unsupported source type "${source.source}"` };
@@ -194,10 +284,19 @@ export function uninstallPlugin(pluginName, marketplaceName) {
194
284
  const data = readInstalledPlugins();
195
285
  const entries = data.plugins[key];
196
286
  let removedFromDisk = false;
287
+ const cacheRoot = pluginCacheRoot();
197
288
  if (entries) {
198
289
  for (const e of entries) {
199
- if (e.installPath && existsSync(e.installPath)) {
200
- rmSync(e.installPath, { recursive: true, force: true });
290
+ if (!e.installPath)
291
+ continue;
292
+ // Containment check: the on-disk installed_plugins.json can be tampered
293
+ // with to point at arbitrary paths (e.g. "/", "$HOME"). Refuse to rm
294
+ // anything that doesn't realpath to a strict child of the plugin cache.
295
+ const safePath = resolveSafePluginPath(e.installPath, cacheRoot);
296
+ if (!safePath)
297
+ continue;
298
+ if (existsSync(safePath)) {
299
+ rmSync(safePath, { recursive: true, force: true });
201
300
  removedFromDisk = true;
202
301
  }
203
302
  }
@@ -44,6 +44,30 @@ export function validatePluginEntrySource(raw, path) {
44
44
  },
45
45
  };
46
46
  }
47
+ if (kind === "local") {
48
+ // Codex / agents-format marketplaces (e.g. awesome-codex-plugins) express
49
+ // a local plugin as { source: "local", path }. Our installer's local path
50
+ // source is a bare string, so normalize onto that.
51
+ if (typeof raw.path !== "string" || !raw.path) {
52
+ return { ok: false, error: `${path}: local source requires path` };
53
+ }
54
+ return { ok: true, value: raw.path };
55
+ }
56
+ if (kind === "url") {
57
+ // The official claude-plugins-official marketplace emits a "url" source
58
+ // type for "clone this git repo" (optionally a subdir via `path`). It is
59
+ // semantically identical to our git / git-subdir sources, so normalize it
60
+ // onto those rather than carrying a fourth code path through the installer.
61
+ if (typeof raw.url !== "string" || !raw.url) {
62
+ return { ok: false, error: `${path}: url source requires url` };
63
+ }
64
+ const ref = typeof raw.ref === "string" ? raw.ref : undefined;
65
+ const sha = typeof raw.sha === "string" ? raw.sha : undefined;
66
+ if (typeof raw.path === "string" && raw.path) {
67
+ return { ok: true, value: { source: "git-subdir", url: raw.url, path: raw.path, ref, sha } };
68
+ }
69
+ return { ok: true, value: { source: "git", url: raw.url, ref, sha } };
70
+ }
47
71
  if (kind === "git-subdir") {
48
72
  if (typeof raw.url !== "string" || !raw.url) {
49
73
  return { ok: false, error: `${path}: git-subdir source requires url` };
@@ -108,8 +132,21 @@ export function validateMarketplace(raw) {
108
132
  if (typeof raw.name !== "string" || !raw.name) {
109
133
  return { ok: false, error: "marketplace.json: name is required" };
110
134
  }
111
- if (!isObject(raw.owner) || typeof raw.owner.name !== "string") {
112
- return { ok: false, error: "marketplace.json: owner.name is required" };
135
+ // Claude Code marketplaces carry a top-level `owner: { name }`. Codex /
136
+ // agents-format marketplaces omit it and instead carry `interface.displayName`.
137
+ // Accept either, falling back to the marketplace name as a last resort, so a
138
+ // Codex manifest is not rejected purely for lacking an owner.
139
+ let ownerName;
140
+ let ownerEmail;
141
+ if (isObject(raw.owner) && typeof raw.owner.name === "string") {
142
+ ownerName = raw.owner.name;
143
+ ownerEmail = typeof raw.owner.email === "string" ? raw.owner.email : undefined;
144
+ }
145
+ else if (isObject(raw.interface) && typeof raw.interface.displayName === "string") {
146
+ ownerName = raw.interface.displayName;
147
+ }
148
+ else {
149
+ ownerName = raw.name;
113
150
  }
114
151
  if (!Array.isArray(raw.plugins)) {
115
152
  return { ok: false, error: "marketplace.json: plugins must be an array" };
@@ -126,10 +163,7 @@ export function validateMarketplace(raw) {
126
163
  value: {
127
164
  name: raw.name,
128
165
  description: typeof raw.description === "string" ? raw.description : undefined,
129
- owner: {
130
- name: raw.owner.name,
131
- email: typeof raw.owner.email === "string" ? raw.owner.email : undefined,
132
- },
166
+ owner: { name: ownerName, email: ownerEmail },
133
167
  plugins,
134
168
  },
135
169
  };
@@ -11,10 +11,14 @@ export type MarketplaceSource = {
11
11
  source: "git";
12
12
  url: string;
13
13
  };
14
+ /** Which plugin-manifest convention a cloned marketplace ships. */
15
+ export type MarketplaceFormat = "claude-code" | "codex" | "universal";
14
16
  export interface KnownMarketplace {
15
17
  source: MarketplaceSource;
16
18
  installLocation: string;
17
19
  lastUpdated: string;
20
+ /** Optional: absent on entries written before format detection existed. */
21
+ format?: MarketplaceFormat;
18
22
  }
19
23
  export type KnownMarketplaces = Record<string, KnownMarketplace>;
20
24
  export type PluginEntrySource = string | {
@@ -20,6 +20,7 @@ const GENERAL_BUILTIN_TOOLS = [
20
20
  "Bash",
21
21
  "WebSearch",
22
22
  "WebFetch",
23
+ "GenerateImage",
23
24
  "AskUserQuestion",
24
25
  "Agent",
25
26
  // AgentStatus removed: background agents now write to ~/.code-shell/agents/
@@ -42,6 +43,17 @@ const GENERAL_BUILTIN_TOOLS = [
42
43
  "RemoteTrigger",
43
44
  "REPL",
44
45
  "PowerShell",
46
+ // Persistent cross-session memory. Including these in the default preset is
47
+ // what lets the LLM list/read/save/delete memories during a normal session,
48
+ // AND what makes the end-of-session auto-dream consolidation work — its
49
+ // tool-call loop pulls these from the registry, so without them every dream
50
+ // run bailed with "missing memory tools". Save/Delete in the user scope stay
51
+ // permission-gated (the tools declare permissionDefault: "ask"); dream-scope
52
+ // writes go through freely.
53
+ "MemoryList",
54
+ "MemoryRead",
55
+ "MemorySave",
56
+ "MemoryDelete",
45
57
  ];
46
58
  const TERMINAL_CODING_EXTRA_TOOLS = [
47
59
  "EnterWorktree",
@@ -71,6 +83,10 @@ const GENERAL_PERMISSION_RULES = [
71
83
  { tool: "Skill", decision: "allow" },
72
84
  { tool: "ListMcpResources", decision: "allow" },
73
85
  { tool: "ReadMcpResource", decision: "allow" },
86
+ // Reading memory is always safe; writes (MemorySave/MemoryDelete) stay gated
87
+ // by the tools' own permissionDefault so the user confirms each change.
88
+ { tool: "MemoryList", decision: "allow" },
89
+ { tool: "MemoryRead", decision: "allow" },
74
90
  ];
75
91
  // ─── Preset definitions ──────────────────────────────────────────
76
92
  export const BUILTIN_AGENT_PRESETS = {
@@ -16,6 +16,10 @@ export interface ComposerOptions {
16
16
  preset?: AgentPreset;
17
17
  customSystemPrompt?: string;
18
18
  appendSystemPrompt?: string;
19
+ /** User's preferred response language (free text), injected as a stable system section. */
20
+ responseLanguage?: string;
21
+ /** How to address the user / short profile (free text). */
22
+ userProfile?: string;
19
23
  /**
20
24
  * Skill names (full names including any "<plugin>:" prefix) the user
21
25
  * has disabled in settings. Filtered out of the LLM's skills listing
@@ -23,6 +27,12 @@ export interface ComposerOptions {
23
27
  * dispatch — see scanSkills(opts.disabledSkills) and skillTool.
24
28
  */
25
29
  disabledSkills?: string[];
30
+ /**
31
+ * Plugin names the user has totally disabled. Coarser knob than
32
+ * disabledSkills — every skill whose namespaced name starts with
33
+ * `${pluginName}:` is filtered. See scanSkills(opts.disabledPlugins).
34
+ */
35
+ disabledPlugins?: string[];
26
36
  }
27
37
  export declare class PromptComposer {
28
38
  private readonly options;
@@ -34,7 +34,11 @@ export class PromptComposer {
34
34
  const memoryContext = this.getMemoryContext();
35
35
  if (!instructions && !memoryContext)
36
36
  return null;
37
- const currentDate = `Today's date is ${new Date().toISOString().split("T")[0]}.`;
37
+ // Local date, not UTC toISOString() is UTC and would show the wrong
38
+ // "today" for users whose timezone has crossed midnight (review-2026-05-30).
39
+ const now = new Date();
40
+ const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
41
+ const currentDate = `Today's date is ${ymd}.`;
38
42
  let content = `<system-reminder>\n${currentDate}\n\n`;
39
43
  if (instructions)
40
44
  content += `${instructions}\n\n`;
@@ -111,13 +115,16 @@ export class PromptComposer {
111
115
  compute: () => this.options.customSystemPrompt,
112
116
  });
113
117
  }
114
- // Tool definitions
118
+ // Tool listing — name + one-line description only. The full JSON schema
119
+ // is NOT repeated here: the provider clients already send it in the
120
+ // native `tools` field (Anthropic tools / OpenAI functions), so dumping
121
+ // `Parameters: {...}` into the system prompt sent every tool's schema
122
+ // twice — a large, per-request token cost for no added model signal.
115
123
  if (tools.length > 0) {
116
124
  sections.push({
117
125
  name: "tool_definitions",
118
126
  compute: () => {
119
- const toolLines = tools.map((t) => `### ${t.name}\n${t.description}\n` +
120
- `Parameters: ${JSON.stringify(t.inputSchema, null, 2)}`);
127
+ const toolLines = tools.map((t) => `### ${t.name}\n${t.description}`);
121
128
  return `# Available Tools\n\n${toolLines.join("\n\n")}`;
122
129
  },
123
130
  });
@@ -136,6 +143,7 @@ export class PromptComposer {
136
143
  compute: () => {
137
144
  const skills = scanSkills(this.options.cwd, {
138
145
  disabledSkills: this.options.disabledSkills,
146
+ disabledPlugins: this.options.disabledPlugins,
139
147
  });
140
148
  return buildSkillListing(skills);
141
149
  },
@@ -147,6 +155,23 @@ export class PromptComposer {
147
155
  compute: () => this.options.appendSystemPrompt,
148
156
  });
149
157
  }
158
+ // Personalization — stable user preferences (language + how to address
159
+ // the user). Placed in the cacheable system prefix because it doesn't
160
+ // change per-turn. Only emitted when at least one field is set.
161
+ const { responseLanguage, userProfile } = this.options;
162
+ if (responseLanguage || userProfile) {
163
+ sections.push({
164
+ name: "personalization",
165
+ compute: () => {
166
+ const lines = ["# User & Response Preferences"];
167
+ if (userProfile)
168
+ lines.push(`- About the user: ${userProfile}`);
169
+ if (responseLanguage)
170
+ lines.push(`- Response language: ${responseLanguage}`);
171
+ return lines.join("\n");
172
+ },
173
+ });
174
+ }
150
175
  return sections;
151
176
  }
152
177
  getInstructions() {
@@ -6,6 +6,14 @@
6
6
  - Avoid creating files unless they are genuinely needed. Prefer editing existing files to creating new ones.
7
7
  - Avoid giving time estimates or predictions for how long tasks will take.
8
8
 
9
+ # Delegating to sub-agents (Agent)
10
+ - A sub-agent runs in its own **clean, isolated context** and returns only its final report. It is stateless — you cannot send follow-up messages, so its prompt must be a complete, self-contained task description.
11
+ - **The primary reason to delegate is context hygiene, not speed.** When a task needs to read many files or run a long exploration but you only need the *conclusion*, hand it to a sub-agent: it does the noisy work in its own context and you keep the answer, not the file dumps. This is the default move for any sweep that would otherwise flood your own context with intermediate output.
12
+ - **Parallel fan-out is the exception, not the default.** Only launch several sub-agents at once when the work genuinely splits into independent pieces with no shared state or ordering. Don't open multiple agents just because a task *could* be subdivided — one well-scoped delegation usually beats a swarm.
13
+ - **Delegate the dirty work, keep the thinking.** Push file-reading, searching, and broad investigation into sub-agents; do the synthesis, decisions, and edits yourself in the main thread.
14
+ - **Don't delegate trivial lookups.** If you know the file/symbol/value and expect a few matches, use Read/Grep/Glob directly — spinning up an agent for that wastes a turn and tokens.
15
+ - If you delegate a chunk of work, add a TodoWrite item for it so the user sees it without reading the sub-agent's transcript.
16
+
9
17
  # Task tracking (TodoWrite)
10
18
  - When the user's request decomposes into 3+ discrete steps, call **TodoWrite** with the complete list up front. The user has a pinned task panel and relies on it to see what's queued, in progress, and done.
11
19
  - **TodoWrite takes the complete list each time** — there is no per-item update. To change a status, call TodoWrite again with the whole list, only that item's `status` changed. Your previous TodoWrite input IS the source of truth — re-read it before rewriting.
@@ -2,7 +2,7 @@ import { ChatSession } from "./chat-session.js";
2
2
  import type { Engine } from "../engine/engine.js";
3
3
  import type { EngineRuntime } from "../engine/runtime.js";
4
4
  import type { EngineConfig } from "../engine/engine.js";
5
- export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "maxTurns" | "maxContextTokens" | "cwd">;
5
+ export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd">;
6
6
  export interface ChatSessionManagerOptions {
7
7
  /**
8
8
  * Shared runtime, supplied here so callers and engineFactory closures
@@ -24,6 +24,13 @@ export declare class ChatSessionManager {
24
24
  constructor(opts: ChatSessionManagerOptions);
25
25
  getOrCreate(sessionId: string, slice: EngineConfigSlice): ChatSession;
26
26
  get(sessionId: string): ChatSession | undefined;
27
+ /**
28
+ * Iterate every live session once. Public iterator so callers (e.g. the
29
+ * protocol server's config hot-reload / sessions query) don't reach into
30
+ * the private `sessions` map with an `as any` cast. Iterates a snapshot of
31
+ * the values so a callback that closes a session can't perturb the walk.
32
+ */
33
+ forEachSession(fn: (s: ChatSession) => void): void;
27
34
  close(sessionId: string): void;
28
35
  closeAll(): void;
29
36
  sessionCount(): number;
@@ -29,6 +29,16 @@ export class ChatSessionManager {
29
29
  get(sessionId) {
30
30
  return this.sessions.get(sessionId);
31
31
  }
32
+ /**
33
+ * Iterate every live session once. Public iterator so callers (e.g. the
34
+ * protocol server's config hot-reload / sessions query) don't reach into
35
+ * the private `sessions` map with an `as any` cast. Iterates a snapshot of
36
+ * the values so a callback that closes a session can't perturb the walk.
37
+ */
38
+ forEachSession(fn) {
39
+ for (const s of [...this.sessions.values()])
40
+ fn(s);
41
+ }
32
42
  close(sessionId) {
33
43
  const s = this.sessions.get(sessionId);
34
44
  if (!s)
@@ -10,6 +10,9 @@ export interface TurnOpts {
10
10
  * configured cwd. */
11
11
  cwd?: string;
12
12
  onStream?: (event: StreamEvent) => void;
13
+ /** Goal mode for this turn — forwarded to engine.run (loop-until-done).
14
+ * String objective or full GoalConfig (objective + optional budgets). */
15
+ goal?: string | import("../engine/goal.js").GoalConfig;
13
16
  }
14
17
  /**
15
18
  * One ChatSession per UI chat tab. Owns a single Engine, an AbortController
@@ -32,6 +35,13 @@ export declare class ChatSession {
32
35
  private active;
33
36
  private controller;
34
37
  private readonly defaultOnStream?;
38
+ /**
39
+ * Model-pool key requested via requestModelSwitch while a turn was in
40
+ * flight. Applied at the next run boundary (before the next turn starts) so
41
+ * we never swap the model out from under a running LLM client — the bug the
42
+ * session-isolation research flagged. null = nothing pending.
43
+ */
44
+ private pendingModel;
35
45
  constructor(opts: ChatSessionOptions);
36
46
  enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
37
47
  /**
@@ -44,6 +54,13 @@ export declare class ChatSession {
44
54
  */
45
55
  cancel(): void;
46
56
  isBusy(): boolean;
57
+ /**
58
+ * Switch this session's active model (per-session, not worker-global).
59
+ * Applies immediately when idle; defers to the next run boundary when a
60
+ * turn is in flight so a hot switch never mutates the model under a
61
+ * running LLM client.
62
+ */
63
+ requestModelSwitch(key: string): void;
47
64
  queueDepth(): number;
48
65
  private pump;
49
66
  }
@@ -19,6 +19,13 @@ export class ChatSession {
19
19
  active = null;
20
20
  controller = null;
21
21
  defaultOnStream;
22
+ /**
23
+ * Model-pool key requested via requestModelSwitch while a turn was in
24
+ * flight. Applied at the next run boundary (before the next turn starts) so
25
+ * we never swap the model out from under a running LLM client — the bug the
26
+ * session-isolation research flagged. null = nothing pending.
27
+ */
28
+ pendingModel = null;
22
29
  constructor(opts) {
23
30
  this.id = opts.id;
24
31
  this.engine = opts.engine;
@@ -50,6 +57,19 @@ export class ChatSession {
50
57
  isBusy() {
51
58
  return this.active !== null;
52
59
  }
60
+ /**
61
+ * Switch this session's active model (per-session, not worker-global).
62
+ * Applies immediately when idle; defers to the next run boundary when a
63
+ * turn is in flight so a hot switch never mutates the model under a
64
+ * running LLM client.
65
+ */
66
+ requestModelSwitch(key) {
67
+ if (this.isBusy()) {
68
+ this.pendingModel = key;
69
+ return;
70
+ }
71
+ this.engine.switchModel(key);
72
+ }
53
73
  queueDepth() {
54
74
  return this.queue.length;
55
75
  }
@@ -68,6 +88,7 @@ export class ChatSession {
68
88
  sessionId: this.id,
69
89
  signal: this.controller.signal,
70
90
  onStream,
91
+ goal: next.opts.goal,
71
92
  });
72
93
  this.lastActivityAt = Date.now();
73
94
  next.resolve(result);
@@ -78,6 +99,13 @@ export class ChatSession {
78
99
  finally {
79
100
  this.active = null;
80
101
  this.controller = null;
102
+ // Run boundary: apply any model switch that was deferred because it was
103
+ // requested mid-run. Done here (not in pump) so it still applies when
104
+ // no further turn is queued.
105
+ if (this.pendingModel !== null) {
106
+ this.engine.switchModel(this.pendingModel);
107
+ this.pendingModel = null;
108
+ }
81
109
  // Drain the next turn if one is waiting.
82
110
  if (this.queue.length > 0)
83
111
  void this.pump();
@@ -0,0 +1,50 @@
1
+ /** Produce a short preview (`sk-…abcd`) so UIs can render presence without revealing the secret. */
2
+ export declare function makeApiKeyPreview(apiKey: string | undefined | null): string | undefined;
3
+ /**
4
+ * Return true when a dotted setting key path points at a secret field.
5
+ * Used to decide whether `config_get`'s response should redact the value.
6
+ */
7
+ export declare function isSecretKeyPath(key: string): boolean;
8
+ /**
9
+ * Redact a single value returned by config_get.
10
+ *
11
+ * Two-stage rule:
12
+ * 1. If the caller-supplied key path itself looks secret
13
+ * ("llm.apiKey", "headers.authorization", …) → collapse to "[redacted]".
14
+ * 2. Otherwise, if the value is an object/array (e.g. config_get("llm") or
15
+ * config_get("providers")), walk it with the shared secret redactor
16
+ * from logging/sanitize-messages so nested apiKey / token / Bearer
17
+ * fields don't slip through verbatim. Primitives pass unchanged.
18
+ *
19
+ * Pre-fix this only inspected the top-level key string, so
20
+ * config_get("providers") returned an array of provider objects with raw
21
+ * apiKey values intact. That defeated the purpose of the boundary.
22
+ */
23
+ export declare function maskSecretValue<T>(key: string, value: T): T | "[redacted]";
24
+ /**
25
+ * Shape of the LLM block returned to clients. We deliberately omit `apiKey`
26
+ * and surface presence/preview instead. Callers that need the raw key already
27
+ * have it server-side; clients should never see it.
28
+ */
29
+ export interface RedactedLlmConfig {
30
+ provider?: string;
31
+ model?: string;
32
+ baseUrl?: string;
33
+ maxTokens?: number;
34
+ /** True when an apiKey is configured. */
35
+ hasApiKey: boolean;
36
+ /** Short preview when an apiKey is configured; omitted otherwise. */
37
+ apiKeyPreview?: string;
38
+ }
39
+ /**
40
+ * Rewrite the LLM section of a config snapshot for transport. We only forward
41
+ * fields the client legitimately needs to render status; secrets become
42
+ * derived flags.
43
+ */
44
+ export declare function redactLlmConfig(llm: {
45
+ provider?: string;
46
+ model?: string;
47
+ apiKey?: string;
48
+ baseUrl?: string;
49
+ maxTokens?: number;
50
+ }): RedactedLlmConfig;