@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.
- package/CHANGELOG.md +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- 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
|
|
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
|
|
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,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 @@
|
|
|
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
|
+
}
|