@gajae-code/coding-agent 0.4.5 → 0.5.1

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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
package/src/cursor.ts CHANGED
@@ -160,6 +160,20 @@ function formatMcpToolErrorMessage(toolName: string, availableTools: string[]):
160
160
  return `MCP tool "${toolName}" not found. Available tools: ${list}`;
161
161
  }
162
162
 
163
+ /**
164
+ * Cursor's wire protocol carries shell timeouts in milliseconds — the
165
+ * model-facing parameter is `block_until_ms`, and `ShellArgs.hard_timeout` is
166
+ * likewise documented in ms — while the bash tool's `timeout` is seconds.
167
+ * Passing the raw value through made a requested 30 s wait (30000 ms) arrive
168
+ * as 30000 s and clamp to the 3600 s ceiling, i.e. an accidental 1-hour
169
+ * timeout on a blocking command. Convert, rounding sub-second values up to 1 s
170
+ * so a tiny requested wait does not collapse to "no timeout".
171
+ */
172
+ function shellTimeoutSeconds(timeout: number | undefined): number | undefined {
173
+ if (!timeout || timeout <= 0) return undefined;
174
+ return Math.max(1, Math.ceil(timeout / 1000));
175
+ }
176
+
163
177
  export class CursorExecHandlers implements ICursorExecHandlers {
164
178
  constructor(private options: CursorExecBridgeOptions) {
165
179
  // Bind every native handler so methods stay instance-safe when invoked
@@ -240,7 +254,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
240
254
 
241
255
  async shell(args: Parameters<NonNullable<ICursorExecHandlers["shell"]>>[0]) {
242
256
  const toolCallId = decodeToolCallId(args.toolCallId);
243
- const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
257
+ const timeoutSeconds = shellTimeoutSeconds(args.timeout);
244
258
  const toolResultMessage = await executeTool(this.#optionsForCall(), "bash", toolCallId, {
245
259
  command: args.command,
246
260
  cwd: args.workingDirectory || undefined,
@@ -262,7 +276,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
262
276
  return createToolResultMessage(toolCallId, toolName, result, true);
263
277
  }
264
278
 
265
- const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
279
+ const timeoutSeconds = shellTimeoutSeconds(args.timeout);
266
280
  const toolArgs: Record<string, unknown> = {
267
281
  command: args.command,
268
282
  cwd: args.workingDirectory || undefined,
@@ -0,0 +1,36 @@
1
+ # Merge into ~/.gjc/agent/models.yml (or use as reference)
2
+ # Remove global x-grok-model-override when using multiple Grok Build models.
3
+
4
+ providers:
5
+ grok-build:
6
+ baseUrl: https://cli-chat-proxy.grok.com/v1
7
+ apiKeyEnv: GROK_CLI_OAUTH_TOKEN
8
+ api: grok-cli-responses
9
+ auth: oauth
10
+ headers:
11
+ x-xai-token-auth: xai-grok-cli
12
+ x-grok-client-identifier: gjc-grok-cli
13
+ x-grok-client-version: 0.2.33
14
+ models:
15
+ - id: grok-composer-2.5-fast
16
+ name: Composer 2.5 Fast
17
+ reasoning: false
18
+ input: [text, image]
19
+ contextWindow: 200000
20
+ maxTokens: 30000
21
+ cost:
22
+ input: 3
23
+ output: 15
24
+ cacheRead: 0.5
25
+ cacheWrite: 0
26
+ - id: grok-build
27
+ name: Grok Build
28
+ reasoning: true
29
+ input: [text, image]
30
+ contextWindow: 512000
31
+ maxTokens: 30000
32
+ cost:
33
+ input: 1
34
+ output: 2
35
+ cacheRead: 0.2
36
+ cacheWrite: 0.2
@@ -0,0 +1 @@
1
+ export { default } from "../grok-cli-vendor/src/index.js";
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "gjc-grok-build",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "GJC extension: Grok Build OAuth and grok-cli models"
7
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
3
+ "root": false,
4
+ "vcs": {
5
+ "enabled": true,
6
+ "clientKind": "git",
7
+ "useIgnoreFile": true
8
+ },
9
+ "files": {
10
+ "ignoreUnknown": false
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2,
16
+ "lineWidth": 100
17
+ },
18
+ "linter": {
19
+ "enabled": true,
20
+ "rules": {
21
+ "recommended": true
22
+ }
23
+ },
24
+ "javascript": {
25
+ "formatter": {
26
+ "quoteStyle": "single",
27
+ "trailingCommas": "all",
28
+ "semicolons": "always"
29
+ }
30
+ },
31
+ "assist": {
32
+ "enabled": true,
33
+ "actions": {
34
+ "source": {
35
+ "organizeImports": "on"
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "gjc-grok-cli-vendor",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Grok CLI chat proxy provider for GJC (SuperGrok OAuth, Composer 2.5 Fast)",
7
+ "dependencies": {}
8
+ }
@@ -0,0 +1 @@
1
+ export { default } from './provider/register.js';
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Model definitions for Grok CLI's API.
3
+ */
4
+
5
+ // ─── Cost constants ($/M tokens) ──────────────────────────────────────────────
6
+
7
+ const COST_BUILD = { input: 1, output: 2, cacheRead: 0.2, cacheWrite: 0.2 };
8
+ const COST_COMPOSER_FAST = { input: 3, output: 15, cacheRead: 0.5, cacheWrite: 0 };
9
+ const COST_43 = { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 };
10
+ const COST_420 = { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 };
11
+
12
+ // ─── Model type ───────────────────────────────────────────────────────────────
13
+
14
+ export interface GrokCliModelConfig {
15
+ id: string;
16
+ name: string;
17
+ reasoning: boolean;
18
+ input: ('text' | 'image')[];
19
+ cost: {
20
+ input: number;
21
+ output: number;
22
+ cacheRead: number;
23
+ cacheWrite: number;
24
+ };
25
+ contextWindow: number;
26
+ maxTokens: number;
27
+ /** Models that don't support reasoning.effort get a thinkingLevelMap. */
28
+ thinkingLevelMap?: Record<string, string | null>;
29
+ }
30
+
31
+ // ─── Hardcoded fallback catalog ───────────────────────────────────────────────
32
+ //
33
+ // These are the models observed via the Grok CLI's /v1/models endpoint and
34
+ // the actual traffic captured through cli-chat-proxy.grok.com.
35
+
36
+ const FALLBACK_MODELS: GrokCliModelConfig[] = [
37
+ {
38
+ id: 'grok-composer-2.5-fast',
39
+ name: 'Composer 2.5 Fast (Grok CLI)',
40
+ reasoning: false,
41
+ input: ['text', 'image'],
42
+ cost: COST_COMPOSER_FAST,
43
+ contextWindow: 200_000,
44
+ maxTokens: 30_000,
45
+ thinkingLevelMap: {
46
+ off: 'none',
47
+ minimal: null,
48
+ low: null,
49
+ medium: null,
50
+ high: null,
51
+ xhigh: null,
52
+ },
53
+ },
54
+ {
55
+ id: 'grok-build',
56
+ name: 'Grok Build',
57
+ reasoning: true,
58
+ input: ['text', 'image'],
59
+ cost: COST_BUILD,
60
+ contextWindow: 512_000,
61
+ maxTokens: 30_000,
62
+ },
63
+ {
64
+ id: 'grok-4.3',
65
+ name: 'Grok 4.3',
66
+ reasoning: true,
67
+ input: ['text', 'image'],
68
+ cost: COST_43,
69
+ contextWindow: 1_000_000,
70
+ maxTokens: 30_000,
71
+ },
72
+ {
73
+ id: 'grok-4.20-0309-reasoning',
74
+ name: 'Grok 4.20 Reasoning',
75
+ reasoning: true,
76
+ input: ['text', 'image'],
77
+ cost: COST_420,
78
+ contextWindow: 2_000_000,
79
+ maxTokens: 30_000,
80
+ },
81
+ {
82
+ id: 'grok-4.20-0309-non-reasoning',
83
+ name: 'Grok 4.20 Non-Reasoning',
84
+ reasoning: false,
85
+ input: ['text', 'image'],
86
+ cost: COST_420,
87
+ contextWindow: 2_000_000,
88
+ maxTokens: 30_000,
89
+ thinkingLevelMap: {
90
+ off: 'none',
91
+ minimal: null,
92
+ low: null,
93
+ medium: null,
94
+ high: null,
95
+ xhigh: null,
96
+ },
97
+ },
98
+ {
99
+ id: 'grok-4.20-multi-agent-0309',
100
+ name: 'Grok 4.20 Multi-Agent',
101
+ reasoning: true,
102
+ input: ['text', 'image'],
103
+ cost: COST_420,
104
+ contextWindow: 2_000_000,
105
+ maxTokens: 30_000,
106
+ },
107
+ ];
108
+
109
+ const EFFORT_CAPABLE_PREFIXES = ['grok-3-mini', 'grok-4.20-multi-agent', 'grok-4.3'];
110
+
111
+ export function supportsReasoningEffort(modelId: string): boolean {
112
+ const parts = modelId.split('/');
113
+ const name = (parts.at(-1) ?? modelId).toLowerCase();
114
+ if (!EFFORT_CAPABLE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
115
+ return false;
116
+ }
117
+ const model = resolveModels().find((entry) => entry.id.toLowerCase() === name);
118
+ if (model) {
119
+ if (!model.reasoning) return false;
120
+ if (!model.thinkingLevelMap) return true;
121
+ return Object.values(model.thinkingLevelMap).some(
122
+ (level) => level !== null && level !== 'none',
123
+ );
124
+ }
125
+ // Effort-capable id not listed in GJC_GROK_CLI_MODELS env list — still honor prefix (avoids spurious 400s).
126
+ return true;
127
+ }
128
+
129
+ // ─── GJC_GROK_CLI_MODELS env override ─────────────────────────────────────
130
+
131
+ /**
132
+ * Resolve the active model list. If `GJC_GROK_CLI_MODELS` is set,
133
+ * it filters/reorders the fallback list; unknown IDs get sensible defaults.
134
+ */
135
+ export function resolveModels(): GrokCliModelConfig[] {
136
+ const env = (process.env.GJC_GROK_CLI_MODELS || '')
137
+ .split(',')
138
+ .map((s) => s.trim())
139
+ .filter(Boolean);
140
+ if (env.length === 0) return FALLBACK_MODELS;
141
+
142
+ const byId = new Map(FALLBACK_MODELS.map((m) => [m.id, m]));
143
+ return env.map(
144
+ (id) =>
145
+ byId.get(id) ?? {
146
+ id,
147
+ name: id,
148
+ reasoning: true,
149
+ input: ['text'] as ('text' | 'image')[],
150
+ cost: COST_BUILD,
151
+ contextWindow: 1_000_000,
152
+ maxTokens: 30_000,
153
+ },
154
+ );
155
+ }
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Payload sanitization for xAI's Responses API via cli-chat-proxy.grok.com.
3
+ *
4
+ * xAI's endpoint has quirks compared to stock OpenAI:
5
+ * - Replayed or encrypted `reasoning` items in input cause 400 errors.
6
+ * - `reasoning.effort` is only supported on a subset of models.
7
+ * - Empty-string content items cause validation failures.
8
+ * - `function_call_output.output` cannot contain image arrays.
9
+ * - `image_url` parts must be normalized to `input_image` with data URIs.
10
+ * - Local image paths must be resolved to base64 data URIs.
11
+ * - xAI rejects `role: "developer"` and `role: "system"` in the input
12
+ * array; these must be moved to top-level `instructions`.
13
+ * - xAI uses `text.format` instead of OpenAI's `response_format`.
14
+ * - xAI uses `prompt_cache_key` for conversation caching.
15
+ * - xAI doesn't support `prompt_cache_retention`.
16
+ *
17
+ * Additional Grok CLI-specific behavior:
18
+ * - Adds x-grok-* headers for client identification
19
+ * - Uses prompt_cache_key for session affinity
20
+ */
21
+
22
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
23
+ import { extname, isAbsolute, resolve, sep } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { supportsReasoningEffort } from '../models/catalog.js';
26
+
27
+ // ─── Content text extraction ─────────────────────────────────────────────────
28
+
29
+ function textFromContent(content: unknown): string {
30
+ if (typeof content === 'string') return content;
31
+ if (!Array.isArray(content)) return '';
32
+ return content
33
+ .map((part) => {
34
+ if (typeof part === 'string') return part;
35
+ if (!part || typeof part !== 'object') return '';
36
+ const item = part as Record<string, unknown>;
37
+ const type = typeof item.type === 'string' ? item.type : '';
38
+ return ['text', 'input_text', 'output_text'].includes(type) && typeof item.text === 'string'
39
+ ? item.text
40
+ : '';
41
+ })
42
+ .filter(Boolean)
43
+ .join('\n');
44
+ }
45
+
46
+ // ─── Image helpers ────────────────────────────────────────────────────────────
47
+
48
+ function stripShellQuotes(value: string): string {
49
+ const trimmed = value.trim();
50
+ if (
51
+ trimmed.length >= 2 &&
52
+ ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
53
+ (trimmed.startsWith("'") && trimmed.endsWith("'")))
54
+ ) {
55
+ return trimmed.slice(1, -1);
56
+ }
57
+ return trimmed;
58
+ }
59
+
60
+ function unescapeShellPath(value: string): string {
61
+ return stripShellQuotes(value).replace(/\\([\\\s'"()&;@])/g, '$1');
62
+ }
63
+
64
+ function imageMimeTypeForPath(path: string): string {
65
+ switch (extname(path).toLowerCase()) {
66
+ case '.jpg':
67
+ case '.jpeg':
68
+ return 'image/jpeg';
69
+ case '.png':
70
+ return 'image/png';
71
+ default:
72
+ throw new Error('xAI image understanding supports local .jpg, .jpeg, and .png files only');
73
+ }
74
+ }
75
+
76
+ function ensurePathWithinWorkspace(cwd: string, filePath: string) {
77
+ const realCwd = realpathSync(cwd);
78
+ const realPath = realpathSync(filePath);
79
+ if (realPath !== realCwd && !realPath.startsWith(`${realCwd}${sep}`)) {
80
+ throw new Error('Image path is outside the workspace');
81
+ }
82
+ return realPath;
83
+ }
84
+
85
+ function resolveLocalImagePath(value: string, cwd: string): string | undefined {
86
+ const cleaned = unescapeShellPath(value);
87
+ if (!cleaned) return undefined;
88
+
89
+ if (cleaned.startsWith('file://')) {
90
+ try {
91
+ const filePath = fileURLToPath(cleaned);
92
+ return existsSync(filePath) ? ensurePathWithinWorkspace(cwd, filePath) : undefined;
93
+ } catch {
94
+ return undefined;
95
+ }
96
+ }
97
+
98
+ const candidate = isAbsolute(cleaned) ? cleaned : resolve(cwd, cleaned);
99
+
100
+ return existsSync(candidate) ? ensurePathWithinWorkspace(cwd, candidate) : undefined;
101
+ }
102
+
103
+ function normalizeImageInput(value: unknown, cwd: string): string | undefined {
104
+ if (typeof value !== 'string' || !value.trim()) return undefined;
105
+ const cleaned = stripShellQuotes(value);
106
+
107
+ if (/^https?:\/\//i.test(cleaned) || /^data:image\//i.test(cleaned)) {
108
+ return cleaned;
109
+ }
110
+
111
+ const localPath = resolveLocalImagePath(cleaned, cwd);
112
+ if (!localPath) {
113
+ throw new Error(`Image file does not exist or is not a valid URL: ${cleaned}`);
114
+ }
115
+
116
+ const mimeType = imageMimeTypeForPath(localPath);
117
+ const data = readFileSync(localPath).toString('base64');
118
+ return `data:${mimeType};base64,${data}`;
119
+ }
120
+
121
+ // ─── Content part normalization ───────────────────────────────────────────────
122
+
123
+ function isInputImagePart(value: unknown): value is Record<string, unknown> {
124
+ return (
125
+ !!value &&
126
+ typeof value === 'object' &&
127
+ (value as Record<string, unknown>).type === 'input_image'
128
+ );
129
+ }
130
+
131
+ function getImageUrlAndDetail(obj: Record<string, unknown>): {
132
+ imageUrl: unknown;
133
+ detail: unknown;
134
+ } {
135
+ if (typeof obj.image_url === 'object' && obj.image_url) {
136
+ const imageUrl = obj.image_url as Record<string, unknown>;
137
+ return { imageUrl: imageUrl.url, detail: imageUrl.detail };
138
+ }
139
+
140
+ return { imageUrl: obj.image_url, detail: obj.detail };
141
+ }
142
+
143
+ function normalizeImageParts(value: unknown, cwd: string): unknown {
144
+ if (Array.isArray(value)) return value.map((item) => normalizeImageParts(item, cwd));
145
+ if (!value || typeof value !== 'object') return value;
146
+
147
+ const obj = { ...(value as Record<string, unknown>) };
148
+
149
+ if (obj.type === 'image' && typeof obj.data === 'string' && typeof obj.mimeType === 'string') {
150
+ return {
151
+ type: 'input_image',
152
+ image_url: `data:${obj.mimeType};base64,${obj.data}`,
153
+ detail: typeof obj.detail === 'string' && obj.detail ? obj.detail : 'auto',
154
+ };
155
+ }
156
+
157
+ if (obj.type === 'image_url') {
158
+ const { imageUrl, detail } = getImageUrlAndDetail(obj);
159
+ obj.type = 'input_image';
160
+ obj.image_url = imageUrl;
161
+ if (typeof detail === 'string' && detail) obj.detail = detail;
162
+ }
163
+
164
+ if (obj.type === 'input_image') {
165
+ const { imageUrl, detail } = getImageUrlAndDetail(obj);
166
+ const normalized = normalizeImageInput(imageUrl, cwd);
167
+ if (normalized) obj.image_url = normalized;
168
+ if (typeof detail === 'string' && detail) obj.detail = detail;
169
+ if (typeof obj.detail !== 'string' || !obj.detail) obj.detail = 'auto';
170
+ }
171
+
172
+ if (Array.isArray(obj.content)) obj.content = normalizeImageParts(obj.content, cwd);
173
+ if (Array.isArray(obj.output)) obj.output = normalizeImageParts(obj.output, cwd);
174
+ return obj;
175
+ }
176
+
177
+ // ─── function_call_output rewrite ─────────────────────────────────────────────
178
+
179
+ function rewriteFunctionCallOutput(input: Record<string, unknown>[]): Record<string, unknown>[] {
180
+ const rewritten: Record<string, unknown>[] = [];
181
+
182
+ for (const item of input) {
183
+ if (
184
+ !item ||
185
+ typeof item !== 'object' ||
186
+ item.type !== 'function_call_output' ||
187
+ !Array.isArray(item.output)
188
+ ) {
189
+ rewritten.push(item);
190
+ continue;
191
+ }
192
+
193
+ const outputParts = item.output as unknown[];
194
+ const imageParts = outputParts.filter(isInputImagePart);
195
+ const textParts = outputParts.filter((p) => !isInputImagePart(p));
196
+
197
+ const textChunks: string[] = [];
198
+ for (const part of textParts) {
199
+ if (typeof part === 'string') {
200
+ textChunks.push(part);
201
+ } else if (part && typeof part === 'object') {
202
+ const p = part as Record<string, unknown>;
203
+ if (typeof p.text === 'string') textChunks.push(p.text);
204
+ }
205
+ }
206
+ let imageCount = 0;
207
+ for (const _ of imageParts) imageCount++;
208
+
209
+ const outputText = textChunks.join('\n') || '(tool returned no text output)';
210
+ rewritten.push({ ...item, output: outputText });
211
+
212
+ if (imageCount > 0) {
213
+ const callId = item.call_id ? ` (${String(item.call_id)})` : '';
214
+ const label = `The previous tool result${callId} included ${imageCount} image${imageCount === 1 ? '' : 's'}. Use the attached image${imageCount === 1 ? '' : 's'} as the visual output from that tool.`;
215
+ rewritten.push({
216
+ role: 'user',
217
+ content: [{ type: 'input_text', text: label }, ...imageParts],
218
+ });
219
+ }
220
+ }
221
+
222
+ return rewritten;
223
+ }
224
+
225
+ // ─── xAI 400 guards ───────────────────────────────────────────────────────────
226
+
227
+ const REPLAYED_INPUT_TYPES = new Set([
228
+ 'reasoning',
229
+ 'reasoning.encrypted_content',
230
+ 'encrypted_content',
231
+ 'item_reference',
232
+ ]);
233
+
234
+ function isReplayedOrUnsupportedInputItem(obj: Record<string, unknown>): boolean {
235
+ const type = typeof obj.type === 'string' ? obj.type : '';
236
+ if (REPLAYED_INPUT_TYPES.has(type)) return true;
237
+ if (type.startsWith('reasoning')) return true;
238
+ if ('encrypted_content' in obj || 'reasoning_encrypted_content' in obj) return true;
239
+ return false;
240
+ }
241
+
242
+ function isEmptyMessageItem(obj: Record<string, unknown>): boolean {
243
+ if (typeof obj.content === 'string') return obj.content.trim().length === 0;
244
+ if (!Array.isArray(obj.content)) return false;
245
+ const parts = obj.content as unknown[];
246
+ if (parts.length === 0) return true;
247
+ return parts.every((part) => {
248
+ if (typeof part === 'string') return part.length === 0;
249
+ if (!part || typeof part !== 'object') return true;
250
+ const p = part as Record<string, unknown>;
251
+ const t = typeof p.type === 'string' ? p.type : '';
252
+ if (['text', 'input_text', 'output_text'].includes(t)) {
253
+ return typeof p.text !== 'string' || p.text.trim().length === 0;
254
+ }
255
+ return false;
256
+ });
257
+ }
258
+
259
+ function stripUnsupportedTopLevelFields(next: Record<string, unknown>): void {
260
+ delete next.prompt_cache_retention;
261
+ delete next.parallel_tool_calls;
262
+ delete next.store;
263
+ delete next.metadata;
264
+ delete next.user;
265
+ delete next.service_tier;
266
+ delete next.truncation;
267
+ }
268
+
269
+ // ─── Main sanitization ────────────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Sanitize a provider request payload for xAI's Responses API via
273
+ * cli-chat-proxy.grok.com.
274
+ *
275
+ * Returns the modified payload. Mutates the input in place for efficiency.
276
+ */
277
+ export function sanitizePayload(
278
+ params: Record<string, unknown>,
279
+ modelId: string,
280
+ sessionId: string | undefined,
281
+ cwd: string,
282
+ ): Record<string, unknown> {
283
+ const next = params;
284
+
285
+ // ── Sanitize input array ──────────────────────────────────────────────
286
+ if (Array.isArray(next.input)) {
287
+ let input = (next.input as unknown[])
288
+ .map((item: unknown) => {
289
+ if (!item || typeof item !== 'object') return item;
290
+ const obj = item as Record<string, unknown>;
291
+
292
+ if (isReplayedOrUnsupportedInputItem(obj)) return null;
293
+ if (isEmptyMessageItem(obj)) return null;
294
+
295
+ return obj;
296
+ })
297
+ .filter(Boolean) as Record<string, unknown>[];
298
+
299
+ // Move system/developer messages to top-level instructions.
300
+ // xAI rejects role: "developer" and role: "system" in the input array.
301
+ const instructionParts: string[] = [];
302
+ input = input.filter((item) => {
303
+ const role = (item as Record<string, unknown>).role;
304
+ if (role !== 'developer' && role !== 'system') return true;
305
+ const text = textFromContent((item as Record<string, unknown>).content).trim();
306
+ if (text) instructionParts.push(text);
307
+ return false;
308
+ });
309
+ if (instructionParts.length > 0) {
310
+ const existing =
311
+ typeof next.instructions === 'string' && next.instructions ? next.instructions : '';
312
+ const merged = [existing, ...instructionParts].filter((part) => part.length > 0).join('\n\n');
313
+ next.instructions = merged;
314
+ }
315
+
316
+ // Normalize image parts (resolve local paths, fix types)
317
+ input = normalizeImageParts(input, cwd) as Record<string, unknown>[];
318
+
319
+ // Rewrite function_call_output with images
320
+ input = rewriteFunctionCallOutput(input);
321
+
322
+ next.input = input;
323
+ } else if (typeof next.input === 'string') {
324
+ // String input is valid and should stay string-shaped.
325
+ }
326
+
327
+ // ── response_format → text.format ────────────────────────────────────
328
+ if (next.response_format) {
329
+ if (!next.text) next.text = { format: next.response_format };
330
+ delete next.response_format;
331
+ }
332
+
333
+ // ── Reasoning effort ──────────────────────────────────────────────────
334
+ if (supportsReasoningEffort(modelId)) {
335
+ const reasoning = next.reasoning as Record<string, unknown> | undefined;
336
+ if (reasoning) {
337
+ const effort = reasoning.effort === 'minimal' ? 'low' : reasoning.effort;
338
+ next.reasoning = reasoning.summary !== undefined ? { effort } : { ...reasoning, effort };
339
+ }
340
+ } else {
341
+ delete next.reasoning;
342
+ delete next.reasoningEffort;
343
+ }
344
+
345
+ // ── Strip/filter unsupported fields ──────────────────────────────────
346
+ if (Array.isArray(next.include)) {
347
+ next.include = (next.include as unknown[]).filter(
348
+ (item) => item !== 'reasoning.encrypted_content',
349
+ );
350
+ if ((next.include as unknown[]).length === 0) delete next.include;
351
+ }
352
+
353
+ stripUnsupportedTopLevelFields(next);
354
+
355
+ // Add prompt_cache_key for conversation caching (routes to same server).
356
+ if (sessionId && !next.prompt_cache_key) {
357
+ next.prompt_cache_key = sessionId;
358
+ }
359
+
360
+ return next;
361
+ }