@gajae-code/coding-agent 0.12.21 → 0.13.0

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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -47,12 +47,18 @@ interface ProviderPreset {
47
47
  compatibility: ProviderCompatibility;
48
48
  api: ProviderSetupApi;
49
49
  providerId: string;
50
- baseUrl: string;
50
+ baseUrl?: string;
51
51
  apiKeyEnv: string;
52
52
  models?: readonly string[];
53
53
  modelApi?: Readonly<Record<string, ProviderSetupApi>>;
54
54
  compat?: ProviderCompatConfig;
55
55
  discovery?: ProviderDiscovery;
56
+ /**
57
+ * Parameterized presets (proxy gateways) do not hardcode a base URL or an
58
+ * unoverridable apiKeyEnv: the caller must supply `--base-url` and may
59
+ * override `--api-key-env`. `baseUrl` is absent for these presets.
60
+ */
61
+ parameterized?: boolean;
56
62
  }
57
63
 
58
64
  const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
@@ -144,11 +150,16 @@ function resolvePresetInput(input: ProviderSetupInput): {
144
150
  `Provider preset '${preset.id}' is ${preset.compatibility}-compatible; omit --compat or use '${preset.compatibility}'.`,
145
151
  );
146
152
  }
147
- if (preset && input.baseUrl !== undefined) {
153
+ if (preset && input.baseUrl !== undefined && !preset.parameterized) {
148
154
  throw new Error(
149
155
  `Provider preset '${preset.id}' uses a fixed base URL; omit --base-url or use --compat openai for a custom provider.`,
150
156
  );
151
157
  }
158
+ if (preset?.parameterized && !input.baseUrl) {
159
+ throw new Error(
160
+ `Provider preset '${preset.id}' requires --base-url <url> (your proxy endpoint). Use --compat openai for a fully custom provider instead.`,
161
+ );
162
+ }
152
163
  if (preset && input.models && input.models.length > 0) {
153
164
  const catalogMode =
154
165
  preset.models && preset.models.length > 0 ? "uses fixed model ids" : "discovers models automatically";
@@ -156,7 +167,12 @@ function resolvePresetInput(input: ProviderSetupInput): {
156
167
  `Provider preset '${preset.id}' ${catalogMode}; omit --model or use --compat openai for a custom provider.`,
157
168
  );
158
169
  }
159
- if (preset && input.apiKeyEnv !== undefined && input.apiKeyEnv.trim() !== preset.apiKeyEnv) {
170
+ if (
171
+ preset &&
172
+ input.apiKeyEnv !== undefined &&
173
+ !preset.parameterized &&
174
+ input.apiKeyEnv.trim() !== preset.apiKeyEnv
175
+ ) {
160
176
  throw new Error(
161
177
  `Provider preset '${preset.id}' uses ${preset.apiKeyEnv}; omit --api-key-env or use --compat openai for a custom provider.`,
162
178
  );
@@ -4,5 +4,7 @@
4
4
  {"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
5
5
  {"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","qwen3.8-max","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","qwen3.8-max":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}},
6
6
  {"id":"cline-pass","aliases":["clinepass","cline"],"name":"ClinePass","description":"ClinePass subscription models discovered from Cline's upstream models.dev catalog","compatibility":"openai","api":"openai-completions","providerId":"cline-pass","baseUrl":"https://api.cline.bot/api/v1","apiKeyEnv":"CLINE_API_KEY","discovery":{"type":"models-dev","modelsDevProvider":"cline-pass"},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning"}},
7
- {"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude-":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}}
7
+ {"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude-":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
8
+ {"id":"litellm","aliases":["litellm-proxy"],"name":"LiteLLM Proxy","description":"OpenAI-compatible LiteLLM proxy endpoint (user-supplied base URL) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"litellm","parameterized":true,"apiKeyEnv":"LITELLM_API_KEY","discovery":{"type":"openai-models-list"}},
9
+ {"id":"openai-compatible-proxy","aliases":["openai-proxy","compatible-proxy","custom-proxy"],"name":"OpenAI-Compatible Proxy","description":"Generic OpenAI-compatible gateway (OpenRouter, OpenCode Go, custom) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"openai-compatible-proxy","parameterized":true,"apiKeyEnv":"OPENAI_COMPATIBLE_API_KEY","discovery":{"type":"openai-models-list"}}
8
10
  ]
@@ -6,7 +6,8 @@ import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
6
  import { PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
7
7
  import { setProjectDir } from "@gajae-code/utils";
8
8
  import { jobElapsedMs } from "../async";
9
- import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
9
+ import { activateModelProfile, materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
10
+ import { formatModelProfileDisplayLabel } from "../config/model-profiles";
10
11
  import {
11
12
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
12
13
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -243,6 +244,54 @@ function parseModelCommandArgs(args: string): ParsedModelCommandArgs {
243
244
  }
244
245
  return { kind: "assign", targetId: "default", selector: args.trim() };
245
246
  }
247
+ /**
248
+ * Optional namespace prefix accepted on `/model <preset>` selectors so users
249
+ * can disambiguate preset names from model ids with `gajae-code/<preset>`.
250
+ * The bare preset name remains the canonical form.
251
+ */
252
+ const MODEL_PRESET_NAMESPACE_PREFIX = "gajae-code/";
253
+
254
+ /**
255
+ * Resolve a `/model <selector>` argument against the merged model-profile
256
+ * registry. Returns the canonical profile name when `selector` is either a
257
+ * bare preset name (`codex-medium`) or a namespaced one
258
+ * (`gajae-code/codex-medium`); returns `undefined` otherwise so the caller
259
+ * falls through to ordinary model resolution.
260
+ *
261
+ * Only selectors that do NOT look like `provider/model` references are
262
+ * considered, so `/model anthropic/claude-...` is never hijacked by a preset.
263
+ */
264
+ export function resolvePresetSelector(
265
+ selector: string,
266
+ modelRegistry: { getModelProfile?: (name: string) => unknown; getError?: () => unknown },
267
+ ): string | undefined {
268
+ const trimmed = selector.trim();
269
+ if (!trimmed) return undefined;
270
+
271
+ // Reject `provider/model` references outright: even if a preset happened to
272
+ // be named `anthropic/claude`, the slash form is a model selector, not a
273
+ // preset shortcut. The only accepted slash form is the `gajae-code/`
274
+ // namespace prefix.
275
+ if (trimmed.includes("/")) {
276
+ if (!trimmed.toLowerCase().startsWith(MODEL_PRESET_NAMESPACE_PREFIX)) return undefined;
277
+ const stripped = trimmed.slice(MODEL_PRESET_NAMESPACE_PREFIX.length);
278
+ if (!stripped || stripped.includes("/")) return undefined;
279
+ return matchProfileName(stripped, modelRegistry);
280
+ }
281
+
282
+ return matchProfileName(trimmed, modelRegistry);
283
+ }
284
+
285
+ function matchProfileName(
286
+ candidate: string,
287
+ modelRegistry: { getModelProfile?: (name: string) => unknown; getError?: () => unknown },
288
+ ): string | undefined {
289
+ // A registry load error means we cannot trust the profile index; fall
290
+ // through to model resolution rather than guessing. A registry without a
291
+ // getModelProfile surface (e.g. a minimal test fixture) has no presets.
292
+ if (modelRegistry.getError?.()) return undefined;
293
+ return modelRegistry.getModelProfile?.(candidate) ? candidate : undefined;
294
+ }
246
295
 
247
296
  function splitExplicitThinkingSelector(selector: string): { baseSelector: string; thinkingLevel?: ThinkingLevel } {
248
297
  const trimmed = selector.trim();
@@ -280,6 +329,8 @@ function resolveModelCommandSelectionFromAvailable(
280
329
  const matchPreferences = { usageOrder: runtime.settings.getStorage()?.getModelUsageOrder() };
281
330
  const resolved = parseModelPattern(selector, availableModels, matchPreferences, {
282
331
  modelRegistry: runtime.session.modelRegistry,
332
+ sessionId: runtime.session.sessionId,
333
+ credentialSessionId: runtime.session.credentialSessionId,
283
334
  });
284
335
  if (!resolved.model) {
285
336
  return undefined;
@@ -389,7 +440,7 @@ function formatModelAssignmentSuccess(
389
440
  return `Role-agent models set to ${selector} for EXECUTOR, ARCHITECT, PLANNER, CRITIC.`;
390
441
  }
391
442
  if (targetId === "all-targets") {
392
- return `All model targets set to ${selector} for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, CRITIC.`;
443
+ return `All model targets set to ${selector} for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, CRITIC, IMAGE.`;
393
444
  }
394
445
  if (targetId === "default") return `Default model set to ${selector}.`;
395
446
  return `${targetId} agent model set to ${selector}.`;
@@ -400,6 +451,7 @@ function modelSelectionUsage(runtime: SlashCommandRuntime, currentModelLine?: st
400
451
  currentModelLine,
401
452
  formatModelAssignmentSummary(runtime),
402
453
  "Use /model <model> for DEFAULT, or /model <target> <model[:effort]> for EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
454
+ "Use /model <preset> or /model gajae-code/<preset> to activate a known model profile.",
403
455
  formatModelOnboardingGuidance(),
404
456
  ]
405
457
  .filter((line): line is string => Boolean(line))
@@ -745,6 +797,36 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
745
797
  runtime,
746
798
  );
747
799
  }
800
+ // Preset shortcut: when the selector names a known model profile
801
+ // (optionally `gajae-code/`-prefixed) and no explicit role target
802
+ // was given, activate the profile immediately instead of treating
803
+ // the preset name as a model id and failing with "Unknown model".
804
+ // `/model <role> <preset>` keeps assigning to the named role.
805
+ if (parsedArgs.targetId === "default") {
806
+ const presetName = resolvePresetSelector(modelId, runtime.session.modelRegistry);
807
+ if (presetName) {
808
+ try {
809
+ const profileLabel = formatModelProfileDisplayLabel(
810
+ runtime.session.modelRegistry.getModelProfile(presetName) ?? { name: presetName },
811
+ );
812
+ await activateModelProfile(
813
+ {
814
+ session: runtime.session,
815
+ modelRegistry: runtime.session.modelRegistry,
816
+ settings: runtime.settings,
817
+ profileName: presetName,
818
+ },
819
+ { persistDefault: false },
820
+ );
821
+ await runtime.output(`Model profile: ${profileLabel}`);
822
+ await runtime.notifyTitleChanged?.();
823
+ await runtime.notifyConfigChanged?.();
824
+ return commandConsumed();
825
+ } catch (err) {
826
+ return usage(`Failed to activate model profile: ${errorMessage(err)}`, runtime);
827
+ }
828
+ }
829
+ }
748
830
  const resolution = await resolveModelCommandSelection(runtime, modelId);
749
831
  if (!resolution.ok) {
750
832
  return usage(modelSelectionUsage(runtime, resolution.failure.message), runtime);
@@ -756,7 +838,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
756
838
  if (includesRoleAgent) {
757
839
  const apiKey = await runtime.session.modelRegistry.getApiKey(
758
840
  selection.model,
759
- runtime.session.sessionId,
841
+ runtime.session.credentialSessionId,
760
842
  );
761
843
  if (!apiKey) {
762
844
  throw new Error(`No API key for ${selection.model.provider}/${selection.model.id}`);
@@ -1,5 +1,6 @@
1
1
  import { $which, logger } from "@gajae-code/utils";
2
2
  import { $ } from "bun";
3
+ import { getRecorderInstallHint } from "./setup";
3
4
  import { resolvePython } from "./transcriber";
4
5
 
5
6
  export interface DownloadProgress {
@@ -32,9 +33,7 @@ async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
32
33
  return;
33
34
  }
34
35
 
35
- throw new Error(
36
- "No audio recording tool found. Install SoX: sudo apt install sox, or FFmpeg: sudo apt install ffmpeg",
37
- );
36
+ throw new Error(`No audio recording tool found. ${getRecorderInstallHint()}`);
38
37
  }
39
38
 
40
39
  // ── Python whisper ─────────────────────────────────────────────────
package/src/stt/setup.ts CHANGED
@@ -1,7 +1,34 @@
1
1
  import { detectRecordingTools } from "./recorder";
2
2
  import { resolvePython } from "./transcriber";
3
3
 
4
- const isWindows = process.platform === "win32";
4
+ export function getRecorderInstallHint(platform: NodeJS.Platform = process.platform): string {
5
+ if (platform === "win32") return "PowerShell fallback available. For better quality: install SoX or FFmpeg.";
6
+ if (platform === "darwin") return "Install a recorder with Homebrew: brew install sox (or brew install ffmpeg)";
7
+ return "Install a recorder: sudo apt install sox (or sudo apt install ffmpeg)";
8
+ }
9
+
10
+ export function formatSTTUsage(
11
+ platform: NodeJS.Platform = process.platform,
12
+ terminalProgram: string | undefined = Bun.env.TERM_PROGRAM,
13
+ ): string {
14
+ const lines = [
15
+ "Enable STT: gjc config set stt.enabled true",
16
+ "You can also enable it in /settings > Interaction > Speech-to-Text.",
17
+ "In the composer, press Alt+H to start recording, then press Alt+H again to stop and transcribe.",
18
+ "The transcription is inserted into the composer for review before you send it.",
19
+ "Shortcut fallback: press Ctrl+P and select Toggle speech-to-text; repeat to stop and transcribe.",
20
+ ];
21
+ if (platform === "darwin") {
22
+ lines.push("On macOS, Alt+H is Option+H. Your terminal must forward Option as Meta/Esc.");
23
+ if (terminalProgram?.toLowerCase().includes("ghostty")) {
24
+ lines.push(
25
+ "Ghostty: set macos-option-as-alt = true in its config, then reload the config or restart Ghostty.",
26
+ );
27
+ }
28
+ }
29
+ lines.push("Run /hotkeys inside GJC to confirm the active shortcut.");
30
+ return lines.join("\n");
31
+ }
5
32
 
6
33
  export interface STTDependencyStatus {
7
34
  recorder: { available: boolean; tool: string | null; installHint: string };
@@ -11,9 +38,7 @@ export interface STTDependencyStatus {
11
38
 
12
39
  export async function checkDependencies(): Promise<STTDependencyStatus> {
13
40
  const recorderTools = detectRecordingTools();
14
- const recorderHint = isWindows
15
- ? "PowerShell fallback available. For better quality: install SoX or FFmpeg."
16
- : "Install SoX: sudo apt install sox, or FFmpeg: sudo apt install ffmpeg";
41
+ const recorderHint = getRecorderInstallHint();
17
42
 
18
43
  const pythonCmd = resolvePython();
19
44
  const pythonHint = "Install Python 3.8+ from https://python.org";
@@ -40,7 +40,7 @@ import type { AgentSession, AgentSessionEvent, ForkContextSeed } from "../sessio
40
40
  import type { ArtifactManager } from "../session/artifacts";
41
41
  import type { AuthStorage } from "../session/auth-storage";
42
42
  import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
43
- import { SessionManager } from "../session/session-manager";
43
+ import { SessionManager, type SessionMemoryMode } from "../session/session-manager";
44
44
  import { FileSessionStorage } from "../session/session-storage";
45
45
  import { truncateTail } from "../session/streaming-output";
46
46
  // Ensure mandatory subagent result extraction is available even when a session is mocked.
@@ -214,7 +214,15 @@ export interface ExecutorOptions {
214
214
  * if the resolved subagent model has no working credentials. See #985.
215
215
  */
216
216
  parentActiveModelPattern?: string;
217
+ /**
218
+ * Whether the live parent session has an active model profile. When set,
219
+ * persisted `task.agentModelOverrides` may resolve through preset-equivalent
220
+ * aliases (bare profile aliases re-resolve to an equivalent provider variant).
221
+ * Manual/direct parents (no active profile) keep exact resolution.
222
+ */
223
+ parentActiveModelProfile?: string;
217
224
  parentSessionId?: string;
225
+ parentCredentialSessionId?: string;
218
226
  thinkingLevel?: ThinkingLevel;
219
227
  outputSchema?: unknown;
220
228
  /** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
@@ -288,7 +296,7 @@ export class ManagedTaskPersistence {
288
296
  throw new Error("Managed task persistence authority is unavailable");
289
297
  }
290
298
 
291
- async openSession(cwd: string, sessionMemoryMode: "off" | "shadow" | "enabled" = "shadow"): Promise<SessionManager> {
299
+ async openSession(cwd: string, sessionMemoryMode: SessionMemoryMode = "shadow"): Promise<SessionManager> {
292
300
  const store = this.#artifacts.getManagedStore();
293
301
  if (!store) throw new Error("Managed task persistence authority is unavailable");
294
302
  this.#artifacts.assertManagedBinding();
@@ -1513,8 +1521,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1513
1521
  options.parentActiveModelPattern,
1514
1522
  modelRegistry,
1515
1523
  settings,
1516
- canonicalChildScope,
1517
- { managedFallback: true },
1524
+ options.parentCredentialSessionId ?? options.parentSessionId,
1525
+ {
1526
+ managedFallback: true,
1527
+ ...(options.parentActiveModelProfile ? { aliasIntent: "preset-equivalent" } : {}),
1528
+ },
1518
1529
  canonicalChildScope,
1519
1530
  ),
1520
1531
  );
@@ -1664,6 +1675,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1664
1675
  providerSessionId: canonicalChildScope,
1665
1676
  model,
1666
1677
  thinkingLevel: effectiveThinkingLevel,
1678
+ activeModelProfile: options.parentActiveModelProfile,
1679
+ credentialSessionId: options.parentCredentialSessionId ?? options.parentSessionId,
1667
1680
  modelSubstitution:
1668
1681
  modelSubstitutionWarning?.reason === "auth_unavailable" && requestedModel
1669
1682
  ? { requestedModel, reason: modelSubstitutionWarning.reason }
@@ -1733,20 +1746,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1733
1746
  );
1734
1747
 
1735
1748
  activeSession = session;
1736
- // Each subagent invocation owns a fresh controller; its configured chain
1737
- // is scoped to this child session and never shares parent sticky state.
1738
- // Auth-aware resolution can substitute the parent model only after every
1739
- // override entry was unavailable. Rebase the controller to that concrete
1740
- // parent selector so its request is never charged to override index zero.
1741
- session.setConfiguredModelChain(
1742
- "default",
1743
- parentFallbackSelector ? [parentFallbackSelector] : modelPatterns,
1744
- "subagent",
1745
- agent.name,
1746
- true,
1747
- );
1748
- if (activeIndex !== undefined && !parentFallbackSelector) {
1749
- session.seedDefaultFallbackResolution(activeIndex, skips);
1749
+ const configuredChildChain = parentFallbackSelector ? [parentFallbackSelector] : modelPatterns;
1750
+ session.setConfiguredModelChain("default", configuredChildChain, "subagent", agent.name, true);
1751
+ if (!parentFallbackSelector) {
1752
+ session.seedDefaultFallbackResolution(activeIndex ?? 0, skips);
1750
1753
  }
1751
1754
  const liveSubagentId = options.subagentId ?? id;
1752
1755
  const manager = AsyncJobManager.instance();
@@ -1871,6 +1874,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1871
1874
  },
1872
1875
  {
1873
1876
  getModel: () => session.model,
1877
+ getCredentialSessionId: () => session.credentialSessionId,
1874
1878
  isIdle: () => !session.isStreaming,
1875
1879
  getActivePromptHandle: () => session.activePromptHandle,
1876
1880
  abort: () => session.abort(),
package/src/task/index.ts CHANGED
@@ -21,6 +21,7 @@ import type { Model, Usage } from "@gajae-code/ai/core";
21
21
  import { $pickenv, prompt, Snowflake } from "@gajae-code/utils";
22
22
  import type { ToolSession } from "..";
23
23
  import { AsyncJobManager, OwnerSubagentShutdownError, type ResumeRunner, type SubagentRunOutcome } from "../async";
24
+ import { resolveProfileBindings } from "../config/model-profiles";
24
25
  import { resolveAgentModelPatterns } from "../config/model-resolver";
25
26
  import type { Theme } from "../modes/theme/theme";
26
27
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
@@ -1642,6 +1643,20 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1642
1643
  const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
1643
1644
  const settingsModelOverride = agentModelOverrides[agentName];
1644
1645
  const parentActiveModelPattern = this.session.getActiveModelString?.();
1646
+ // A live active model profile claims persisted agent overrides: bare
1647
+ // profile aliases may re-resolve to an equivalent provider variant at
1648
+ // dispatch. Manual/direct sessions (no active profile) stay exact.
1649
+ const parentActiveModelProfile = (
1650
+ this.session as { getActiveModelProfile?: () => string | undefined }
1651
+ ).getActiveModelProfile?.();
1652
+ const parentProfileDefinition = parentActiveModelProfile
1653
+ ? this.session.modelRegistry?.getModelProfile?.(parentActiveModelProfile)
1654
+ : undefined;
1655
+ const parentOwnedModelProfile =
1656
+ parentProfileDefinition &&
1657
+ Object.hasOwn(resolveProfileBindings(parentProfileDefinition).agentModelOverrides, agentName)
1658
+ ? parentActiveModelProfile
1659
+ : undefined;
1645
1660
  const modelOverride = resolveAgentModelPatterns({
1646
1661
  settingsOverride: settingsModelOverride,
1647
1662
  agentModel: effectiveAgent.model,
@@ -1985,7 +2000,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1985
2000
  taskDepth,
1986
2001
  modelOverride,
1987
2002
  parentActiveModelPattern,
2003
+ parentActiveModelProfile: parentOwnedModelProfile,
1988
2004
  parentSessionId: this.session.getSessionId?.() ?? undefined,
2005
+ parentCredentialSessionId:
2006
+ this.session.getCredentialSessionId?.() ?? this.session.getSessionId?.() ?? undefined,
1989
2007
  thinkingLevel: thinkingLevelOverride,
1990
2008
  outputSchema: effectiveOutputSchema,
1991
2009
  sessionFile: taskSessionFile,
@@ -2060,7 +2078,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2060
2078
  taskDepth,
2061
2079
  modelOverride,
2062
2080
  parentActiveModelPattern,
2081
+ parentActiveModelProfile: parentOwnedModelProfile,
2063
2082
  parentSessionId: this.session.getSessionId?.() ?? undefined,
2083
+ parentCredentialSessionId:
2084
+ this.session.getCredentialSessionId?.() ?? this.session.getSessionId?.() ?? undefined,
2064
2085
  thinkingLevel: thinkingLevelOverride,
2065
2086
  outputSchema: effectiveOutputSchema,
2066
2087
  sessionFile: taskSessionFile,
@@ -2117,7 +2138,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2117
2138
  diff,
2118
2139
  this.session.modelRegistry!,
2119
2140
  this.session.settings,
2120
- this.session.getSessionId?.() ?? undefined,
2141
+ this.session.getCredentialSessionId?.() ??
2142
+ this.session.getSessionId?.() ??
2143
+ undefined,
2121
2144
  );
2122
2145
  }
2123
2146
  : undefined;
@@ -156,10 +156,19 @@ async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
156
156
  }
157
157
 
158
158
  async function readZipEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
159
- const { unzipSync } = await loadFflate();
159
+ const { unzip } = await loadFflate();
160
+ const result = Promise.withResolvers<Record<string, Uint8Array>>();
161
+ unzip(bytes, (error, files) => {
162
+ if (error) {
163
+ result.reject(error);
164
+ return;
165
+ }
166
+ result.resolve(files);
167
+ });
168
+
160
169
  let files: Record<string, Uint8Array>;
161
170
  try {
162
- files = unzipSync(bytes);
171
+ files = await result.promise;
163
172
  } catch (error) {
164
173
  throw new ToolError(error instanceof Error ? error.message : String(error));
165
174
  }