@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -0,0 +1,19 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export async function realSkillPath(baseDir: string, targetPath: string): Promise<string> {
5
+ const realBase = await fs.realpath(baseDir);
6
+ let realTarget: string;
7
+ try {
8
+ realTarget = await fs.realpath(targetPath);
9
+ } catch (error) {
10
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
11
+ throw new Error(`File not found: ${targetPath}`);
12
+ }
13
+ throw error;
14
+ }
15
+ if (realTarget !== realBase && !realTarget.startsWith(realBase + path.sep)) {
16
+ throw new Error("Path traversal is not allowed");
17
+ }
18
+ return realTarget;
19
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import * as path from "node:path";
11
11
  import { getActiveSkills } from "../extensibility/skills";
12
+ import { realSkillPath } from "./skill-path";
12
13
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
13
14
 
14
15
  function getContentType(filePath: string): InternalResource["contentType"] {
@@ -70,6 +71,7 @@ export class SkillProtocolHandler implements ProtocolHandler {
70
71
  if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
71
72
  throw new Error("Path traversal is not allowed");
72
73
  }
74
+ targetPath = await realSkillPath(skill.baseDir, targetPath);
73
75
  } else {
74
76
  targetPath = skill.filePath;
75
77
  }
package/src/lsp/edits.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  WorkspaceEdit,
14
14
  } from "./types";
15
15
  import { uriToFile } from "./utils";
16
+ import { assertInsideWorkspace } from "./workspace-path";
16
17
 
17
18
  // =============================================================================
18
19
  // Text Edit Application
@@ -133,6 +134,7 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
133
134
  const textEditsByUri = flattenWorkspaceTextEdits(edit);
134
135
  for (const [uri, textEdits] of textEditsByUri) {
135
136
  const filePath = uriToFile(uri);
137
+ await assertInsideWorkspace(cwd, filePath);
136
138
  await applyTextEdits(filePath, textEdits);
137
139
  applied.push(`Applied ${textEdits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
138
140
  }
@@ -144,18 +146,22 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
144
146
  if (change.kind === "create") {
145
147
  const createOp = change as CreateFile;
146
148
  const filePath = uriToFile(createOp.uri);
149
+ await assertInsideWorkspace(cwd, filePath);
147
150
  await Bun.write(filePath, "");
148
151
  applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
149
152
  } else if (change.kind === "rename") {
150
153
  const renameOp = change as RenameFile;
151
154
  const oldPath = uriToFile(renameOp.oldUri);
152
155
  const newPath = uriToFile(renameOp.newUri);
156
+ await assertInsideWorkspace(cwd, oldPath);
157
+ await assertInsideWorkspace(cwd, newPath);
153
158
  await fs.mkdir(path.dirname(newPath), { recursive: true });
154
159
  await fs.rename(oldPath, newPath);
155
160
  applied.push(`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`);
156
161
  } else if (change.kind === "delete") {
157
162
  const deleteOp = change as DeleteFile;
158
163
  const filePath = uriToFile(deleteOp.uri);
164
+ await assertInsideWorkspace(cwd, filePath);
159
165
  await fs.rm(filePath, { recursive: true });
160
166
  applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
161
167
  }
@@ -0,0 +1,51 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ToolError } from "../tools/tool-errors";
4
+
5
+ function isEnoent(error: unknown): boolean {
6
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
7
+ }
8
+
9
+ function escapes(root: string, candidate: string): boolean {
10
+ return candidate !== root && !candidate.startsWith(`${root}${path.sep}`);
11
+ }
12
+
13
+ /** Resolve a missing path through the nearest existing ancestor. */
14
+ async function canonicalize(filePath: string): Promise<string> {
15
+ try {
16
+ return await fs.realpath(filePath);
17
+ } catch (error) {
18
+ if (!isEnoent(error)) throw error;
19
+ }
20
+
21
+ const missing: string[] = [];
22
+ let cursor = filePath;
23
+ for (;;) {
24
+ let stat: Awaited<ReturnType<typeof fs.lstat>> | undefined;
25
+ try {
26
+ stat = await fs.lstat(cursor);
27
+ } catch (error) {
28
+ if (!isEnoent(error)) throw error;
29
+ }
30
+ if (stat) {
31
+ try {
32
+ return path.join(await fs.realpath(cursor), ...missing);
33
+ } catch (error) {
34
+ if (isEnoent(error)) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
35
+ throw error;
36
+ }
37
+ }
38
+ const parent = path.dirname(cursor);
39
+ if (parent === cursor) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
40
+ missing.unshift(path.basename(cursor));
41
+ cursor = parent;
42
+ }
43
+ }
44
+
45
+ export async function assertInsideWorkspace(cwd: string, filePath: string): Promise<void> {
46
+ const root = await fs.realpath(cwd);
47
+ const candidate = await canonicalize(filePath);
48
+ if (escapes(root, candidate)) {
49
+ throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
50
+ }
51
+ }
package/src/main.ts CHANGED
@@ -475,6 +475,11 @@ type StartupModelProfileArgs = {
475
475
  preferCachedDefaultProfile?: boolean;
476
476
  };
477
477
 
478
+ type StartupModelProfileFailure = {
479
+ profileName: string;
480
+ error: ModelProfileCredentialError | UnknownModelProfileError;
481
+ };
482
+
478
483
  function staleDefaultProfileMessage(error: UnknownModelProfileError, profileCatalogRefreshUnavailable = false): string {
479
484
  const refreshNotice = profileCatalogRefreshUnavailable
480
485
  ? " The online profile catalog could not be refreshed; continuing with the last accepted catalog."
@@ -488,6 +493,8 @@ async function applyStartupModelProfilesWithPolicy(
488
493
  onUnknownDefault?: (error: UnknownModelProfileError, profileCatalogRefreshUnavailable: boolean) => void,
489
494
  ): Promise<void> {
490
495
  let profileCatalogRefreshUnavailable = false;
496
+ let profilePassFailures: StartupModelProfileFailure[] = [];
497
+ let unhandledProfileName: string | undefined;
491
498
  const applyProfile = async (
492
499
  profileName: string,
493
500
  persistDefault: boolean,
@@ -496,37 +503,66 @@ async function applyStartupModelProfilesWithPolicy(
496
503
  tolerateCredentialError?: boolean;
497
504
  tolerateUnknownDefault?: boolean;
498
505
  runtimeBindingsOnly?: boolean;
506
+ userSelectionRevision?: number;
499
507
  } = {},
500
508
  ): Promise<boolean> => {
501
- try {
509
+ const isCurrent =
510
+ options.userSelectionRevision === undefined
511
+ ? undefined
512
+ : () => args.session.getUserModelSelectionRevision() === options.userSelectionRevision;
513
+ if (isCurrent && !isCurrent()) return false;
514
+ const apply = async (): Promise<boolean> => {
515
+ if (isCurrent && !isCurrent()) return false;
502
516
  if (options.runtimeBindingsOnly) {
503
- await applyModelProfileRuntimeBindings({
504
- session: args.session,
505
- modelRegistry: args.modelRegistry,
506
- settings: args.settings,
507
- profileName,
508
- });
517
+ await applyModelProfileRuntimeBindings(
518
+ {
519
+ session: args.session,
520
+ modelRegistry: args.modelRegistry,
521
+ settings: args.settings,
522
+ profileName,
523
+ },
524
+ isCurrent,
525
+ );
509
526
  } else {
510
527
  await activateModelProfile(
511
528
  { session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
512
- { persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
529
+ {
530
+ persistDefault,
531
+ thinkingLevelOverride: options.thinkingLevelOverride,
532
+ isCurrent,
533
+ },
513
534
  );
514
535
  }
515
536
  return true;
537
+ };
538
+ try {
539
+ return isCurrent ? await args.session.withSdkControlMutation(apply) : await apply();
516
540
  } catch (error) {
517
541
  if (error instanceof ModelProfileCredentialError && (onCredentialError || options.tolerateCredentialError)) {
518
- if (onCredentialError) onCredentialError(error);
519
- else process.stderr.write(`${chalk.yellow(`Warning: ${error.message}`)}\n`);
542
+ profilePassFailures.push({ profileName, error });
543
+ logger.warn("Startup model profile not applied: missing provider credentials", {
544
+ profile: profileName,
545
+ errorClass: error.name,
546
+ providers: error.providers,
547
+ role: error.role,
548
+ });
549
+ if (!onCredentialError) process.stderr.write(`${chalk.yellow(`Warning: ${error.message}`)}\n`);
520
550
  return false;
521
551
  }
522
552
  if (error instanceof UnknownModelProfileError && options.tolerateUnknownDefault) {
523
- if (onUnknownDefault) onUnknownDefault(error, profileCatalogRefreshUnavailable);
524
- else
553
+ profilePassFailures.push({ profileName, error });
554
+ logger.warn("Startup model profile not applied: unknown profile", {
555
+ profile: profileName,
556
+ errorClass: error.name,
557
+ catalogRefreshUnavailable: profileCatalogRefreshUnavailable,
558
+ });
559
+ if (!onUnknownDefault)
525
560
  process.stderr.write(
526
561
  `${chalk.yellow(`Warning: ${staleDefaultProfileMessage(error, profileCatalogRefreshUnavailable)}`)}\n`,
527
562
  );
528
563
  return false;
529
564
  }
565
+ unhandledProfileName = profileName;
530
566
  throw error;
531
567
  }
532
568
  };
@@ -546,9 +582,16 @@ async function applyStartupModelProfilesWithPolicy(
546
582
  const preferCachedProfiles =
547
583
  (args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined) ||
548
584
  (args.preferCachedDefaultProfile === true && defaultProfile !== undefined);
549
- const applyConfiguredProfiles = async (allowMissingDefault: boolean): Promise<boolean> => {
585
+ let modelSelectionChangedDuringRecovery = false;
586
+ const applyConfiguredProfiles = async (
587
+ allowMissingDefault: boolean,
588
+ profileNames?: ReadonlySet<string>,
589
+ userSelectionRevision?: number,
590
+ ): Promise<boolean> => {
591
+ profilePassFailures = [];
592
+ unhandledProfileName = undefined;
550
593
  let applied = true;
551
- if (defaultProfile) {
594
+ if (defaultProfile && (!profileNames || profileNames.has(defaultProfile))) {
552
595
  applied =
553
596
  (await applyProfile(defaultProfile, false, {
554
597
  thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
@@ -556,37 +599,141 @@ async function applyStartupModelProfilesWithPolicy(
556
599
  : undefined,
557
600
  tolerateCredentialError: tolerateDefaultProfileFailure,
558
601
  tolerateUnknownDefault:
559
- allowMissingDefault && (onUnknownDefault !== undefined || tolerateDefaultProfileFailure),
602
+ onUnknownDefault !== undefined || (allowMissingDefault && tolerateDefaultProfileFailure),
560
603
  runtimeBindingsOnly: args.session.hasRecoveredDefaultFallbackChain(),
604
+ userSelectionRevision,
561
605
  })) && applied;
562
606
  }
563
- if (args.parsedArgs.mpreset) {
564
- applied = (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true)) && applied;
607
+ if (args.parsedArgs.mpreset && (!profileNames || profileNames.has(args.parsedArgs.mpreset))) {
608
+ applied =
609
+ (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, {
610
+ userSelectionRevision,
611
+ })) && applied;
565
612
  }
566
613
  return applied;
567
614
  };
615
+ const profilesFromFirstFailure = (failedProfiles: ReadonlySet<string>): ReadonlySet<string> => {
616
+ const orderedProfiles = [defaultProfile, args.parsedArgs.mpreset].filter(
617
+ (name): name is string => name !== undefined,
618
+ );
619
+ const firstFailureIndex = orderedProfiles.findIndex(name => failedProfiles.has(name));
620
+ return firstFailureIndex === -1 ? failedProfiles : new Set(orderedProfiles.slice(firstFailureIndex));
621
+ };
622
+ const applyFailedProfiles = async (
623
+ failedProfiles: ReadonlySet<string>,
624
+ userSelectionRevision: number,
625
+ ): Promise<boolean> => {
626
+ const applied = await applyConfiguredProfiles(true, failedProfiles, userSelectionRevision);
627
+ if (
628
+ applied &&
629
+ defaultProfile !== undefined &&
630
+ failedProfiles.has(defaultProfile) &&
631
+ args.parsedArgs.mpreset !== undefined &&
632
+ !failedProfiles.has(args.parsedArgs.mpreset)
633
+ ) {
634
+ return applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, { userSelectionRevision });
635
+ }
636
+ return applied;
637
+ };
638
+ const refreshAuthAndCatalog = async (continueOnRefreshError: boolean): Promise<void> => {
639
+ try {
640
+ await args.modelRegistry.authStorage?.reload();
641
+ } catch (error) {
642
+ logger.warn("Failed to reload auth before startup model profile recovery", {
643
+ error: error instanceof Error ? error.message : String(error),
644
+ });
645
+ }
646
+ try {
647
+ await args.modelRegistry.refreshModelPresetProfilesFromRegistry();
648
+ } catch (error) {
649
+ if (!continueOnRefreshError && args.modelRegistry.getError()) throw error;
650
+ profileCatalogRefreshUnavailable = true;
651
+ logger.warn("Failed to refresh model profile catalog before startup recovery", {
652
+ error: error instanceof Error ? error.message : String(error),
653
+ });
654
+ }
655
+ try {
656
+ await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
657
+ } catch (error) {
658
+ if (!continueOnRefreshError) throw error;
659
+ logger.warn("Failed to refresh model catalog before startup profile recovery", {
660
+ error: error instanceof Error ? error.message : String(error),
661
+ });
662
+ }
663
+ };
664
+ const reportFinalProfileFailures = (markDefaultUnavailable = true): void => {
665
+ for (const failure of profilePassFailures) {
666
+ if (failure.error instanceof ModelProfileCredentialError) {
667
+ onCredentialError?.(failure.error);
668
+ } else {
669
+ onUnknownDefault?.(failure.error, profileCatalogRefreshUnavailable);
670
+ }
671
+ }
672
+ if (
673
+ markDefaultUnavailable &&
674
+ (onCredentialError || onUnknownDefault) &&
675
+ args.parsedArgs.mpreset === undefined &&
676
+ args.parsedArgs.model === undefined
677
+ ) {
678
+ const defaultFailure = profilePassFailures.find(failure => failure.profileName === defaultProfile);
679
+ if (defaultFailure) args.session.setUnavailableModelProfile(defaultFailure.profileName);
680
+ }
681
+ };
568
682
 
683
+ // Deferred --mpreset startup can overlap selector input; never reapply its
684
+ // captured profile after the user has selected a newer model.
569
685
  if (preferCachedProfiles) {
570
686
  let applied: boolean;
571
687
  let refreshedOnline = false;
688
+ const selectionRevision = args.session.getUserModelSelectionRevision();
572
689
  try {
573
- applied = await applyConfiguredProfiles(false);
690
+ applied = await applyConfiguredProfiles(false, undefined, selectionRevision);
574
691
  } catch (error) {
575
692
  if (error instanceof ModelProfileCredentialError) throw error;
576
693
  if (error instanceof UnknownModelProfileError) {
577
- try {
578
- await args.modelRegistry.refreshModelPresetProfilesFromRegistry();
579
- } catch (refreshError) {
580
- if (args.modelRegistry.getError()) throw refreshError;
581
- profileCatalogRefreshUnavailable = true;
582
- logger.warn("Failed to refresh model profile catalog before startup recovery", {
583
- error: refreshError instanceof Error ? refreshError.message : String(refreshError),
584
- });
585
- }
694
+ await refreshAuthAndCatalog(onCredentialError !== undefined || onUnknownDefault !== undefined);
695
+ } else {
696
+ await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
586
697
  }
587
- await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
698
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
588
699
  refreshedOnline = true;
589
- applied = await applyConfiguredProfiles(true);
700
+ const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
701
+ if (unhandledProfileName) failedProfiles.add(unhandledProfileName);
702
+ const profilesToRetry = profilesFromFirstFailure(failedProfiles);
703
+ applied = await applyConfiguredProfiles(
704
+ true,
705
+ profilesToRetry.size > 0 ? profilesToRetry : undefined,
706
+ selectionRevision,
707
+ );
708
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
709
+ }
710
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
711
+ modelSelectionChangedDuringRecovery = true;
712
+ applied = false;
713
+ }
714
+ if (
715
+ !modelSelectionChangedDuringRecovery &&
716
+ !applied &&
717
+ profilePassFailures.length > 0 &&
718
+ (onCredentialError !== undefined || onUnknownDefault !== undefined)
719
+ ) {
720
+ if (!refreshedOnline) {
721
+ const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
722
+ await refreshAuthAndCatalog(true);
723
+ refreshedOnline = true;
724
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
725
+ modelSelectionChangedDuringRecovery = true;
726
+ applied = false;
727
+ } else {
728
+ applied = await applyFailedProfiles(failedProfiles, selectionRevision);
729
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
730
+ modelSelectionChangedDuringRecovery = true;
731
+ applied = false;
732
+ }
733
+ }
734
+ }
735
+ if (applied) args.session.setUnavailableModelProfile(undefined);
736
+ else reportFinalProfileFailures(!modelSelectionChangedDuringRecovery);
590
737
  }
591
738
  if (applied && !refreshedOnline)
592
739
  args.modelRegistry.refreshInBackground("online-if-uncached", args.session.credentialSessionId);
@@ -598,7 +745,7 @@ async function applyStartupModelProfilesWithPolicy(
598
745
  }
599
746
 
600
747
  // Explicit CLI --model/--thinking must win over any activated or skipped profile.
601
- if (explicitModel) {
748
+ if (explicitModel && !modelSelectionChangedDuringRecovery) {
602
749
  await args.session.setModelTemporary(explicitModel, args.startupThinkingLevel ?? args.parsedArgs.thinking, {
603
750
  persistAsSessionDefault: true,
604
751
  cause: "startup-override",
@@ -606,7 +753,7 @@ async function applyStartupModelProfilesWithPolicy(
606
753
  const selector = `${explicitModel.provider}/${explicitModel.id}`;
607
754
  args.session.setConfiguredModelChain("default", [selector], "startup-override", undefined, true);
608
755
  args.session.seedDefaultFallbackResolution(0, []);
609
- } else if (args.parsedArgs.thinking && args.session.model) {
756
+ } else if (!modelSelectionChangedDuringRecovery && args.parsedArgs.thinking && args.session.model) {
610
757
  await args.session.setModelTemporary(args.session.model, args.parsedArgs.thinking, { cause: "startup-override" });
611
758
  }
612
759
  }
@@ -975,6 +1122,21 @@ export async function runInteractiveMode(
975
1122
  );
976
1123
  }
977
1124
 
1125
+ // Startup input bypasses the editor submit path, so seed the automatic title
1126
+ // here. Only text that `prompt` actually dispatches to a loaded skill is
1127
+ // skipped; unknown `/skill:` text is submitted as an ordinary prompt and is
1128
+ // titled like one. It must run before the prompt (the title gate skips
1129
+ // sessions that already hold a user message), and a title failure must never
1130
+ // drop the prompt.
1131
+ const maybeGenerateStartupTitle = (text: string, images?: readonly unknown[]): void => {
1132
+ try {
1133
+ if (session.resolvePromptSkillInvocation(text, images) || session.isLocallyHandledSlashCommand(text)) return;
1134
+ mode.maybeGenerateSessionTitle(text);
1135
+ } catch (error: unknown) {
1136
+ logger.warn("Startup session title generation failed", { error: String(error) });
1137
+ }
1138
+ };
1139
+
978
1140
  const runStartupInputAndPromptLoop = async (): Promise<never> => {
979
1141
  const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
980
1142
  if (!hasStartupInput && resumeAction === "continue-tail") {
@@ -988,6 +1150,7 @@ export async function runInteractiveMode(
988
1150
 
989
1151
  if (initialMessage !== undefined) {
990
1152
  try {
1153
+ maybeGenerateStartupTitle(initialMessage, initialImages);
991
1154
  await session.prompt(initialMessage, { images: initialImages });
992
1155
  } catch (error: unknown) {
993
1156
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -1004,6 +1167,7 @@ export async function runInteractiveMode(
1004
1167
  });
1005
1168
  if (slashResult === true) continue;
1006
1169
  if (typeof slashResult === "string") text = slashResult;
1170
+ maybeGenerateStartupTitle(text);
1007
1171
  await session.prompt(text);
1008
1172
  } catch (error: unknown) {
1009
1173
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";