@gajae-code/coding-agent 0.18.7 → 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 (279) hide show
  1. package/CHANGELOG.md +279 -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 +2 -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/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -13,6 +13,7 @@ import {
13
13
  type Context,
14
14
  codexContextOverrideKey,
15
15
  createModelManager,
16
+ createTrustedStrippedModelClone,
16
17
  Effort,
17
18
  enrichModelThinking,
18
19
  getBundledModels,
@@ -49,6 +50,8 @@ import {
49
50
  readBoundedModelsJson,
50
51
  resolveLoopbackOpenAIBaseUrl,
51
52
  } from "@gajae-code/ai/utils/discovery/openai-compatible";
53
+ // Internal-only: registerFinalizedModelClone is not part of the public @gajae-code/ai surface
54
+ import { registerFinalizedModelClone } from "@gajae-code/ai/utils/trusted-model-clone";
52
55
 
53
56
  // Sentinels for local-only OAuth tokens — declared inline to avoid loading provider
54
57
  // modules at startup. Must match the provider OAuth modules.
@@ -1771,9 +1774,6 @@ export class ModelRegistry {
1771
1774
  {
1772
1775
  fresh: boolean;
1773
1776
  modelIds: ReadonlySet<string>;
1774
- profileModelIds?: ReadonlySet<string>;
1775
- profileFresh?: boolean;
1776
- profileEndpoint?: string;
1777
1777
  authGeneration: string;
1778
1778
  endpoint: string;
1779
1779
  }
@@ -2746,20 +2746,7 @@ export class ModelRegistry {
2746
2746
  }));
2747
2747
  }
2748
2748
  #stripModelBaseUrlQueries(models: readonly Model<Api>[]): Model<Api>[] {
2749
- return models.map(model => {
2750
- if (!model.baseUrl) return model;
2751
- try {
2752
- const parsed = new URL(model.baseUrl);
2753
- parsed.username = "";
2754
- parsed.password = "";
2755
- parsed.search = "";
2756
- parsed.hash = "";
2757
- return { ...model, baseUrl: parsed.toString().replace(/\/$/, "") };
2758
- } catch {
2759
- const { baseUrl: _baseUrl, ...withoutBaseUrl } = model;
2760
- return withoutBaseUrl as Model<Api>;
2761
- }
2762
- });
2749
+ return models.map(model => createTrustedStrippedModelClone(model));
2763
2750
  }
2764
2751
  #stripUrlUserinfo(url: string | undefined): string | undefined {
2765
2752
  if (!url) return url;
@@ -3288,6 +3275,11 @@ export class ModelRegistry {
3288
3275
  result.provider,
3289
3276
  this.#providerEvidenceApiKeys.get(result.provider),
3290
3277
  ) &&
3278
+ result.configurationGeneration ===
3279
+ this.authStorage.getProviderConfigurationGeneration(
3280
+ result.provider,
3281
+ this.#authStorageConfigOwner,
3282
+ ) &&
3291
3283
  result.endpoint ===
3292
3284
  this.#normalizeDiscoveryEvidenceEndpoint(
3293
3285
  this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "",
@@ -3309,7 +3301,10 @@ export class ModelRegistry {
3309
3301
  this.#providerEvidenceApiKeys.get(result.provider),
3310
3302
  ) ||
3311
3303
  result.configurationGeneration !==
3312
- this.authStorage.getProviderConfigurationGeneration(result.provider) ||
3304
+ this.authStorage.getProviderConfigurationGeneration(
3305
+ result.provider,
3306
+ this.#authStorageConfigOwner,
3307
+ ) ||
3313
3308
  result.endpoint !==
3314
3309
  this.#normalizeDiscoveryEvidenceEndpoint(
3315
3310
  this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "",
@@ -3334,9 +3329,6 @@ export class ModelRegistry {
3334
3329
  | {
3335
3330
  fresh: boolean;
3336
3331
  modelIds: ReadonlySet<string>;
3337
- profileModelIds?: ReadonlySet<string>;
3338
- profileFresh?: boolean;
3339
- profileEndpoint?: string;
3340
3332
  authGeneration: string;
3341
3333
  endpoint: string;
3342
3334
  }
@@ -3481,12 +3473,9 @@ export class ModelRegistry {
3481
3473
  .map(([provider, evidence]) => [
3482
3474
  provider,
3483
3475
  evidence.fresh,
3484
- evidence.profileFresh,
3485
3476
  evidence.authGeneration,
3486
3477
  evidence.endpoint,
3487
- evidence.profileEndpoint,
3488
3478
  [...evidence.modelIds].sort(),
3489
- evidence.profileModelIds === undefined ? undefined : [...evidence.profileModelIds].sort(),
3490
3479
  ]),
3491
3480
  configured: [...this.#configuredDiscoveryEvidence.entries()]
3492
3481
  .sort(([left], [right]) => left.localeCompare(right))
@@ -3540,7 +3529,10 @@ export class ModelRegistry {
3540
3529
  ? this.authStorage.has(provider) ||
3541
3530
  this.authStorage.hasAuth(provider, undefined, { owner: this.#authStorageConfigOwner })
3542
3531
  : !this.#isCredentiallessProvider(provider);
3543
- let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
3532
+ let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(
3533
+ provider,
3534
+ this.#authStorageConfigOwner,
3535
+ );
3544
3536
  let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
3545
3537
  if (shouldPreflightAuth) {
3546
3538
  if (optionalAuth && isCurrentPreflight()) this.#credentiallessAuthFallbackProviders.delete(provider);
@@ -3555,7 +3547,10 @@ export class ModelRegistry {
3555
3547
  credentialSessionId,
3556
3548
  }),
3557
3549
  );
3558
- const currentAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
3550
+ const currentAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(
3551
+ provider,
3552
+ this.#authStorageConfigOwner,
3553
+ );
3559
3554
  if (preflightAuthConfigurationGeneration !== currentAuthConfigurationGeneration) {
3560
3555
  const currentOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
3561
3556
  if (
@@ -3633,6 +3628,9 @@ export class ModelRegistry {
3633
3628
  const isCurrentEndpoint = () =>
3634
3629
  endpoint ===
3635
3630
  this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
3631
+ const isCurrentAuthConfiguration = () =>
3632
+ preflightAuthConfigurationGeneration ===
3633
+ this.authStorage.getProviderConfigurationGeneration(provider, this.#authStorageConfigOwner);
3636
3634
  const isCurrentProviderRefresh = () =>
3637
3635
  providerRefresh === undefined ||
3638
3636
  ("providerId" in providerRefresh
@@ -3704,7 +3702,7 @@ export class ModelRegistry {
3704
3702
  },
3705
3703
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
3706
3704
  cacheDynamicModelProvenance: cacheLookupProvenance,
3707
- canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
3705
+ canPublishCache: () => isCurrentAuthConfiguration() && isCurrentEndpoint() && isCurrentProviderRefresh(),
3708
3706
  });
3709
3707
  const authGeneration =
3710
3708
  mergeInput.authGeneration ??
@@ -3712,6 +3710,7 @@ export class ModelRegistry {
3712
3710
  const current =
3713
3711
  mergeInput.current &&
3714
3712
  authGeneration === this.#getProviderEvidenceGeneration(effectiveProviderConfig.provider, preflightApiKey) &&
3713
+ isCurrentAuthConfiguration() &&
3715
3714
  isCurrentEndpoint();
3716
3715
  if (!current) {
3717
3716
  return {
@@ -3899,9 +3898,15 @@ export class ModelRegistry {
3899
3898
  // The token is only needed if the dynamic fetch fires (cache miss),
3900
3899
  // and failures there are handled gracefully.
3901
3900
  const peekKey = async (descriptor: { providerId: string }) => {
3902
- const configurationGeneration = this.authStorage.getProviderConfigurationGeneration(descriptor.providerId);
3901
+ const configurationGeneration = this.authStorage.getProviderConfigurationGeneration(
3902
+ descriptor.providerId,
3903
+ this.#authStorageConfigOwner,
3904
+ );
3903
3905
  const apiKey = await this.#peekApiKeyForProvider(descriptor.providerId, { credentialSessionId });
3904
- if (configurationGeneration !== this.authStorage.getProviderConfigurationGeneration(descriptor.providerId)) {
3906
+ if (
3907
+ configurationGeneration !==
3908
+ this.authStorage.getProviderConfigurationGeneration(descriptor.providerId, this.#authStorageConfigOwner)
3909
+ ) {
3905
3910
  return { apiKey: undefined, authGeneration: undefined };
3906
3911
  }
3907
3912
  return {
@@ -4071,13 +4076,6 @@ export class ModelRegistry {
4071
4076
  this.#descriptorDiscoveryEvidence.set(options.providerId, {
4072
4077
  fresh: result.fetched,
4073
4078
  modelIds: new Set(models.map(model => model.id)),
4074
- ...(result.dynamicModelIds === undefined
4075
- ? {}
4076
- : {
4077
- profileModelIds: new Set(result.dynamicModelIds),
4078
- profileFresh: !result.stale,
4079
- profileEndpoint: endpoint,
4080
- }),
4081
4079
  authGeneration,
4082
4080
  endpoint: this.#normalizeDiscoveryEvidenceEndpoint(models[0]?.baseUrl ?? endpoint),
4083
4081
  });
@@ -5005,6 +5003,14 @@ export class ModelRegistry {
5005
5003
  }
5006
5004
  const generated = this.#generatedAuthHeaders.get(models[index]!);
5007
5005
  if (generated) this.#generatedAuthHeaders.set(result[index]!, generated);
5006
+ // Register finalized clone as trusted if the original is trusted
5007
+ // registerFinalizedModelClone builds a fresh trusted object from the original's snapshot
5008
+ if (result[index]) {
5009
+ const registeredClone = registerFinalizedModelClone(models[index]!, result[index]);
5010
+ if (registeredClone) {
5011
+ result[index] = registeredClone;
5012
+ }
5013
+ }
5008
5014
  }
5009
5015
  return result;
5010
5016
  }
@@ -5648,9 +5654,10 @@ export class ModelRegistry {
5648
5654
 
5649
5655
  /**
5650
5656
  * Get selectable models with auth configured.
5651
- * This is a fast check that doesn't refresh OAuth tokens. A current,
5652
- * authoritative live catalog also limits each provider to its enrolled ids;
5653
- * bundled entries remain the fallback until that evidence exists.
5657
+ * This is a fast check that doesn't refresh OAuth tokens. The bundled catalog
5658
+ * is authoritative for visibility: live discovery only enriches entries and
5659
+ * adds unknown ids, and never removes a bundled model. A provider that cannot
5660
+ * serve a model reports its own error when the model is used.
5654
5661
  */
5655
5662
  getAvailable(): Model<Api>[] {
5656
5663
  this.#synchronizeEnvironmentCredentials();
@@ -5664,82 +5671,12 @@ export class ModelRegistry {
5664
5671
  ) {
5665
5672
  return this.#availableModelsCache;
5666
5673
  }
5667
- const authoritativeDiscoveryIds = new Map<string, ReadonlySet<string> | undefined>();
5668
- const bundledIdsByProvider = new Map<string, Set<string>>();
5669
- this.#availableModelsCache = this.#models.filter(model => {
5670
- if (!this.#isModelAvailable(model, disabledProviders)) return false;
5671
-
5672
- let liveIds = authoritativeDiscoveryIds.get(model.provider);
5673
- if (!authoritativeDiscoveryIds.has(model.provider)) {
5674
- liveIds = this.#getAuthoritativeDiscoveredModelIds(model.provider);
5675
- authoritativeDiscoveryIds.set(model.provider, liveIds);
5676
- }
5677
- // Undefined means discovery has not run successfully for the current
5678
- // provider context. Keep the bundled catalog as the explicit fallback.
5679
- if (liveIds === undefined) return true;
5680
- if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
5681
-
5682
- const activity = this.#providerActivity.get(model.provider);
5683
- if (!activity?.staticModelIds.has(model.id)) return liveIds.has(model.id);
5684
-
5685
- let bundledIds = bundledIdsByProvider.get(model.provider);
5686
- if (!bundledIds) {
5687
- bundledIds = new Set(
5688
- (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5689
- candidate => candidate.id,
5690
- ),
5691
- );
5692
- bundledIdsByProvider.set(model.provider, bundledIds);
5693
- }
5694
- return !bundledIds.has(model.id) || liveIds.has(model.id);
5695
- });
5674
+ this.#availableModelsCache = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
5696
5675
  this.#availableModelsDisabledProviders = disabledProviderKey;
5697
5676
  this.#availableModelsEnvFingerprint = envFingerprint;
5698
5677
  return this.#availableModelsCache;
5699
5678
  }
5700
5679
 
5701
- /**
5702
- * Return the current live model ids only when discovery produced an
5703
- * authoritative catalog for the provider and endpoint. An unavailable or
5704
- * failed discovery returns undefined so callers retain the bundled fallback.
5705
- */
5706
- #getAuthoritativeDiscoveredModelIds(provider: string): ReadonlySet<string> | undefined {
5707
- const descriptorEvidence = this.#descriptorDiscoveryEvidence.get(provider);
5708
- if (descriptorEvidence?.profileFresh && descriptorEvidence.profileModelIds !== undefined) {
5709
- try {
5710
- if (
5711
- descriptorEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
5712
- descriptorEvidence.profileEndpoint ===
5713
- this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
5714
- ) {
5715
- return descriptorEvidence.profileModelIds;
5716
- }
5717
- } catch {
5718
- // A provider context that can no longer be verified must use its fallback.
5719
- }
5720
- }
5721
-
5722
- const configuredEvidence = this.#configuredDiscoveryEvidence.get(provider);
5723
- if (!configuredEvidence) return undefined;
5724
- const discoveryState = this.#discoveryManager.getState(provider);
5725
- if (
5726
- discoveryState?.status !== "ok" &&
5727
- discoveryState?.status !== "cached" &&
5728
- discoveryState?.status !== "empty"
5729
- ) {
5730
- return undefined;
5731
- }
5732
- try {
5733
- return configuredEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
5734
- configuredEvidence.endpoint ===
5735
- this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
5736
- ? configuredEvidence.modelIds
5737
- : undefined;
5738
- } catch {
5739
- return undefined;
5740
- }
5741
- }
5742
-
5743
5680
  #synchronizeEnvironmentCredentials(): void {
5744
5681
  // Runtime registrations own a provider while their credential is present.
5745
5682
  // Refresh them first so a missing runtime env key can hand ownership back to
@@ -5753,49 +5690,6 @@ export class ModelRegistry {
5753
5690
  }
5754
5691
  }
5755
5692
 
5756
- /**
5757
- * Get authenticated models, excluding bundled entries that a fresh provider
5758
- * catalog has positively shown to be unavailable. Bundled entries remain
5759
- * usable until live catalog evidence exists so offline startup is unchanged.
5760
- */
5761
- getAvailableForProfileActivation(): Model<Api>[] {
5762
- const bundledIdsByProvider = new Map<string, Set<string>>();
5763
- // Evidence staleness depends only on the provider, and resolving it scans the
5764
- // catalog for the provider base URL. Decide it once per provider per call so a
5765
- // multi-thousand-model catalog is not rescanned for every model.
5766
- const staleEvidenceByProvider = new Map<string, boolean>();
5767
- return this.getAvailable().filter(model => {
5768
- const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
5769
- if (!evidence?.profileFresh || evidence.profileModelIds === undefined) return true;
5770
- if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
5771
- let bundledModelIds = bundledIdsByProvider.get(model.provider);
5772
- if (!bundledModelIds) {
5773
- bundledModelIds = new Set(
5774
- (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5775
- candidate => candidate.id,
5776
- ),
5777
- );
5778
- bundledIdsByProvider.set(model.provider, bundledModelIds);
5779
- }
5780
- if (!bundledModelIds.has(model.id)) return true;
5781
- let staleEvidence = staleEvidenceByProvider.get(model.provider);
5782
- if (staleEvidence === undefined) {
5783
- staleEvidence =
5784
- evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
5785
- evidence.profileEndpoint !==
5786
- this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(model.provider) ?? "");
5787
- staleEvidenceByProvider.set(model.provider, staleEvidence);
5788
- }
5789
- if (staleEvidence) return true;
5790
- return evidence.profileModelIds.has(model.id);
5791
- });
5792
- }
5793
-
5794
- #hasCustomModelOverlay(provider: string, id: string): boolean {
5795
- const matches = (overlay: CustomModelOverlay) => overlay.provider === provider && overlay.id === id;
5796
- return this.#customModelOverlays.some(matches) || this.#runtimeModelOverlays.some(matches);
5797
- }
5798
-
5799
5693
  #hasFreshOrStaticModelEvidence(model: Model<Api>): boolean {
5800
5694
  const evidence = this.#providerActivity.get(model.provider);
5801
5695
  if (
@@ -51,6 +51,13 @@ export function resolveSkillScopeTrust(
51
51
  return DEFAULT_SKILL_SCOPE_TRUST;
52
52
  }
53
53
 
54
+ /** Resolve permission to read a user's external skill symlinks from user-owned config only. */
55
+ export function resolveGlobalUserSkillLinkTrust(
56
+ settings: Pick<SkillDiscoverySettings, "trustUserSkills" | "enablePiUser">,
57
+ ): boolean {
58
+ return resolveSkillScopeTrust(settings, "user");
59
+ }
60
+
54
61
  export const DEFAULT_SKILL_DISCOVERY_SETTINGS: SkillDiscoverySettings = {
55
62
  enabled: true,
56
63
  enableSkillCommands: true,
@@ -122,8 +122,9 @@ function parseRootList(value: string | undefined): string[] {
122
122
  }
123
123
 
124
124
  function resolveManagedWorktreeRoot(root: string, configured: string | undefined): string {
125
- const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, os.homedir());
126
- const resolved = template.replaceAll("{repo}", path.basename(root));
125
+ // Function replacers so a `$` in the home or repository path is kept verbatim.
126
+ const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, () => os.homedir());
127
+ const resolved = template.replaceAll("{repo}", () => path.basename(root));
127
128
  return path.resolve(path.isAbsolute(resolved) ? resolved : path.join(path.dirname(root), resolved));
128
129
  }
129
130
 
@@ -0,0 +1,10 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function assertDeleteInsideWorkspace(cwd: string, filePath: string): Promise<void> {
5
+ const root = await fs.realpath(cwd);
6
+ const target = await fs.realpath(filePath);
7
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
8
+ throw new Error(`Delete escapes the workspace: ${filePath}`);
9
+ }
10
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tools Cursor already runs on a dedicated exec channel.
3
+ * Keep this aligned with CURSOR_NATIVE_TOOL_NAMES in packages/ai/src/providers/cursor.ts.
4
+ */
5
+ const CURSOR_DEDICATED_CHANNEL_TOOLS = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]);
6
+
7
+ /**
8
+ * Name to dispatch from a Cursor MCP call.
9
+ * Dedicated-channel tools are refused here. Advertised session tools such as eval stay allowed.
10
+ */
11
+ export function cursorMcpDispatchName(call: { toolName?: string; name?: string }): string | null {
12
+ const toolName = call.toolName || call.name || "";
13
+ if (toolName.length === 0 || CURSOR_DEDICATED_CHANNEL_TOOLS.has(toolName)) return null;
14
+ return toolName;
15
+ }
package/src/cursor.ts CHANGED
@@ -23,6 +23,8 @@ import {
23
23
  piTimeout,
24
24
  } from "@gajae-code/ai/providers/cursor/exec-modern";
25
25
  import { sanitizeText } from "@gajae-code/utils";
26
+ import { assertDeleteInsideWorkspace } from "./cursor-delete-path";
27
+ import { cursorMcpDispatchName } from "./cursor-mcp-name";
26
28
  import { resolveToCwd } from "./tools/path-utils";
27
29
 
28
30
  /**
@@ -184,6 +186,7 @@ async function executeDelete(
184
186
  throw new Error(`Path is not a file: ${pathArg}`);
185
187
  }
186
188
 
189
+ await assertDeleteInsideWorkspace(options.cwd, absolutePath);
187
190
  fs.rmSync(absolutePath);
188
191
 
189
192
  const sizeText = fileStat.size ? ` (${fileStat.size} bytes)` : "";
@@ -646,11 +649,19 @@ export class CursorExecHandlers implements ICursorExecHandlers {
646
649
 
647
650
  async mcp(call: CursorMcpCall, signal?: AbortSignal, markNonAbortable?: () => void) {
648
651
  const options = this.#optionsForCall();
649
- const toolName = call.toolName || call.name;
652
+ const requestedName = call.toolName || call.name || "";
653
+ const toolName = cursorMcpDispatchName(call);
650
654
  const toolCallId = decodeToolCallId(call.toolCallId);
655
+ const availableTools = Array.from(options.tools.keys()).filter(
656
+ name => cursorMcpDispatchName({ toolName: name }) !== null,
657
+ );
658
+ if (!toolName) {
659
+ const message = formatMcpToolErrorMessage(requestedName, availableTools);
660
+ const result = buildToolErrorResult(message);
661
+ return createToolResultMessage(toolCallId, requestedName || "mcp", result, true);
662
+ }
651
663
  const tool = options.tools.get(toolName);
652
664
  if (!tool) {
653
- const availableTools = Array.from(options.tools.keys()).filter(name => name.startsWith("mcp__"));
654
665
  const message = formatMcpToolErrorMessage(toolName, availableTools);
655
666
  const result = buildToolErrorResult(message);
656
667
  return createToolResultMessage(toolCallId, toolName, result, true);
@@ -35,6 +35,8 @@ export interface LoadCustomizationInventoryOptions {
35
35
  home?: string;
36
36
  /** Skill management policy snapshot (trust, include/ignore, disabledExtensions). */
37
37
  policy?: SkillManagementPolicy;
38
+ /** User-owned permission to follow external user skill symlinks. */
39
+ allowExternalUserSkillSymlinks?: boolean;
38
40
  /** Disabled extension ids (`skill:<name>` / `mcp:<name>`) from settings. */
39
41
  disabledExtensions?: string[];
40
42
  }
@@ -94,6 +96,7 @@ async function loadSkillRows(options: LoadCustomizationInventoryOptions, _warnin
94
96
  cwd: options.cwd,
95
97
  home: options.home,
96
98
  policy: options.policy,
99
+ allowExternalUserSkillSymlinks: options.allowExternalUserSkillSymlinks,
97
100
  });
98
101
  const managedPaths = new Set<string>();
99
102
  for (const record of records) {
@@ -0,0 +1,22 @@
1
+ /** True when a session header names this parent file or its basename. */
2
+ export function sessionBelongsToParent(headerLine: string, parentBasename: string): boolean {
3
+ let parentSession: unknown;
4
+ try {
5
+ parentSession = (JSON.parse(headerLine) as { parentSession?: unknown }).parentSession;
6
+ } catch {
7
+ return false;
8
+ }
9
+ if (typeof parentSession !== "string" || parentSession.length === 0) return false;
10
+ const normalized = parentSession.replaceAll("\\", "/");
11
+ const base =
12
+ normalized
13
+ .split("/")
14
+ .pop()
15
+ ?.replace(/\.jsonl$/, "") ?? "";
16
+ return (
17
+ normalized === parentBasename ||
18
+ base === parentBasename ||
19
+ normalized.endsWith(`/${parentBasename}`) ||
20
+ normalized.endsWith(`/${parentBasename}.jsonl`)
21
+ );
22
+ }
@@ -17,6 +17,7 @@ import {
17
17
  resolveEquivalentPath,
18
18
  } from "@gajae-code/utils";
19
19
  import type { CpuProfile, HeapSnapshot } from "./profiler";
20
+ import { sessionBelongsToParent } from "./report-bundle-parent";
20
21
  import { collectSystemInfo, sanitizeEnv } from "./system-info";
21
22
 
22
23
  /** Maximum number of log lines to load into memory at once. */
@@ -283,6 +284,8 @@ async function addSubagentSessions(
283
284
  const archivePath = `subagents/${filename}`;
284
285
  try {
285
286
  const content = await Bun.file(filePath).text();
287
+ const headerLine = content.split("\n", 1)[0] ?? "";
288
+ if (!sessionBelongsToParent(headerLine, parentBasename)) continue;
286
289
  data[archivePath] = content;
287
290
  files.push(archivePath);
288
291
 
@@ -99,7 +99,19 @@ export function formatSystemInfo(info: SystemInfo): string {
99
99
 
100
100
  /** Sanitize environment variables by redacting sensitive values */
101
101
  export function sanitizeEnv(env: Record<string, string | undefined>): Record<string, string> {
102
- const SENSITIVE_PATTERNS = [/key/i, /secret/i, /token/i, /pass/i, /auth/i, /credential/i, /api/i, /private/i];
102
+ const SENSITIVE_PATTERNS = [
103
+ /key/i,
104
+ /secret/i,
105
+ /token/i,
106
+ /pass/i,
107
+ /auth/i,
108
+ /credential/i,
109
+ /api/i,
110
+ /private/i,
111
+ /cookie/i,
112
+ /dsn/i,
113
+ /url$/i,
114
+ ];
103
115
 
104
116
  const result: Record<string, string> = {};
105
117
  for (const [k, v] of Object.entries(env)) {
@@ -2,12 +2,18 @@ import type {
2
2
  Api,
3
3
  AssistantMessageEventStream,
4
4
  Context,
5
+ FetchImpl,
5
6
  Model,
6
7
  SimpleStreamOptions,
7
8
  } from '@gajae-code/ai/core';
8
9
  import { streamOpenAIResponses } from '@gajae-code/ai/providers/openai-responses';
10
+ import {
11
+ getGrokCliVersion,
12
+ parseMinimumVersionFrom426,
13
+ updateVersionFromError,
14
+ } from './version-manager';
9
15
 
10
- const GROK_CLI_VERSION = '0.2.33';
16
+ type FetchInput = Parameters<FetchImpl>[0];
11
17
 
12
18
  /**
13
19
  * Stream function that adds Grok CLI-specific headers to requests.
@@ -16,6 +22,7 @@ const GROK_CLI_VERSION = '0.2.33';
16
22
  * - x-grok-conv-id: <session/conversation ID>
17
23
  * - x-grok-model-override: <model ID>
18
24
  * - x-xai-token-auth: xai-grok-cli
25
+ * - x-grok-client-version: learned from HTTP 426 responses; defaults to 1.0.13 and retries once on upgrade
19
26
  */
20
27
  export function streamGrokCli(
21
28
  model: Model<Api>,
@@ -23,10 +30,14 @@ export function streamGrokCli(
23
30
  options?: SimpleStreamOptions,
24
31
  ): AssistantMessageEventStream {
25
32
  const sessionId = options?.sessionId;
33
+
34
+ // Get the Grok CLI version: learned from 426 responses or fallback
35
+ const grokCliVersion = getGrokCliVersion();
36
+
26
37
  const headers: Record<string, string> = {
27
38
  ...options?.headers,
28
39
  'x-grok-client-identifier': 'gjc-grok-cli',
29
- 'x-grok-client-version': GROK_CLI_VERSION,
40
+ 'x-grok-client-version': grokCliVersion,
30
41
  'x-xai-token-auth': 'xai-grok-cli',
31
42
  'x-grok-model-override': model.id,
32
43
  };
@@ -40,11 +51,63 @@ export function streamGrokCli(
40
51
  api: 'openai-responses',
41
52
  } as Model<'openai-responses'>;
42
53
 
54
+ // Wrap fetch to intercept 426 errors and extract version info
55
+ const baseFetch = options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
56
+ const wrappedFetch = wrapFetchForVersionHandling(baseFetch);
57
+
43
58
  return streamOpenAIResponses(responsesModel, context, {
44
59
  ...options,
45
60
  headers,
46
- onResponse(response) {
47
- options?.onResponse?.(response, model);
61
+ fetch: wrappedFetch,
62
+ onResponse(response, _responseModel, scope, signal) {
63
+ return options?.onResponse?.(response, model, scope, signal);
48
64
  },
49
65
  });
50
66
  }
67
+
68
+ /**
69
+ * Wraps a fetch function to learn from HTTP 426 responses and retry once with a newer version.
70
+ */
71
+ function wrapFetchForVersionHandling(baseFetch: FetchImpl): FetchImpl {
72
+ return Object.assign(
73
+ async (input: FetchInput, init?: RequestInit): Promise<Response> => {
74
+ const firstInput =
75
+ input instanceof Request ? (input.clone() as unknown as FetchInput) : input;
76
+ const response = await baseFetch(firstInput, init);
77
+
78
+ if (response.status !== 426) return response;
79
+
80
+ let errorText: string;
81
+ try {
82
+ errorText = await response.clone().text();
83
+ } catch {
84
+ return response;
85
+ }
86
+
87
+ if (!parseMinimumVersionFrom426(errorText)) return response;
88
+
89
+ const retryHeaders = new Headers(
90
+ init?.headers ?? (input instanceof Request ? input.headers : undefined),
91
+ );
92
+ const requestedVersion = retryHeaders.get('x-grok-client-version');
93
+ const learnedVersion = updateVersionFromError(errorText);
94
+ if (!requestedVersion || learnedVersion === requestedVersion) return response;
95
+
96
+ retryHeaders.set('x-grok-client-version', learnedVersion);
97
+ const retryInput =
98
+ input instanceof Request ? (input.clone() as unknown as FetchInput) : input;
99
+ const retryResponse = await baseFetch(retryInput, { ...init, headers: retryHeaders });
100
+
101
+ if (retryResponse.status === 426) {
102
+ try {
103
+ updateVersionFromError(await retryResponse.clone().text());
104
+ } catch {
105
+ // Keep the retry response intact if its body cannot be inspected.
106
+ }
107
+ }
108
+
109
+ return retryResponse;
110
+ },
111
+ { preconnect: baseFetch.preconnect },
112
+ ) as FetchImpl;
113
+ }