@gajae-code/coding-agent 0.5.0 → 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 (125) hide show
  1. package/CHANGELOG.md +19 -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/config/file-lock-gc.d.ts +5 -0
  7. package/dist/types/config/file-lock.d.ts +7 -0
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  10. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  11. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  12. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  13. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  14. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  15. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  19. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  22. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  23. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  25. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  26. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  27. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  28. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  29. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  30. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  31. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  32. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  33. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  34. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  35. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  36. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  37. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  38. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  40. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  41. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  42. package/dist/types/session/agent-session.d.ts +1 -1
  43. package/dist/types/session/blob-store.d.ts +39 -3
  44. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  45. package/dist/types/tools/ask.d.ts +15 -1
  46. package/dist/types/tools/subagent.d.ts +6 -0
  47. package/package.json +7 -7
  48. package/src/async/job-manager.ts +52 -0
  49. package/src/cli/args.ts +3 -0
  50. package/src/cli/auth-broker-cli.ts +1 -0
  51. package/src/cli/list-models.ts +13 -1
  52. package/src/cli.ts +1 -0
  53. package/src/commands/gc.ts +22 -0
  54. package/src/commands/harness.ts +7 -3
  55. package/src/config/file-lock-gc.ts +181 -0
  56. package/src/config/file-lock.ts +14 -0
  57. package/src/config/model-profiles.ts +24 -15
  58. package/src/coordinator/contract.ts +1 -0
  59. package/src/coordinator-mcp/server.ts +459 -3
  60. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  61. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  62. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  63. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  66. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  67. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  68. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  69. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  70. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  71. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  72. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  75. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  76. package/src/defaults/gjc-defaults.ts +7 -0
  77. package/src/defaults/gjc-grok-cli.ts +22 -0
  78. package/src/extensibility/extensions/index.ts +1 -0
  79. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  80. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  81. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  82. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  83. package/src/gjc-runtime/gc-render.ts +70 -0
  84. package/src/gjc-runtime/gc-runtime.ts +403 -0
  85. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  86. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  87. package/src/gjc-runtime/state-renderer.ts +12 -3
  88. package/src/gjc-runtime/state-runtime.ts +46 -29
  89. package/src/gjc-runtime/team-gc.ts +49 -0
  90. package/src/gjc-runtime/team-runtime.ts +179 -2
  91. package/src/gjc-runtime/tmux-common.ts +14 -0
  92. package/src/gjc-runtime/tmux-gc.ts +176 -0
  93. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  94. package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
  95. package/src/harness-control-plane/gc-adapter.ts +184 -0
  96. package/src/harness-control-plane/owner.ts +11 -0
  97. package/src/harness-control-plane/storage.ts +70 -0
  98. package/src/internal-urls/docs-index.generated.ts +14 -8
  99. package/src/main.ts +7 -2
  100. package/src/modes/components/hook-selector.ts +19 -0
  101. package/src/modes/components/model-selector.ts +25 -8
  102. package/src/modes/components/status-line/segments.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +25 -6
  104. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  105. package/src/modes/controllers/selector-controller.ts +1 -0
  106. package/src/modes/rpc/rpc-mode.ts +151 -33
  107. package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
  108. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  109. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  110. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  111. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  112. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  113. package/src/sdk.ts +17 -3
  114. package/src/session/agent-session.ts +77 -8
  115. package/src/session/blob-store.ts +59 -3
  116. package/src/session/session-manager.ts +4 -4
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  118. package/src/skill-state/workflow-hud.ts +106 -10
  119. package/src/slash-commands/builtin-registry.ts +3 -2
  120. package/src/task/executor.ts +9 -0
  121. package/src/tools/ask.ts +56 -1
  122. package/src/tools/job.ts +3 -2
  123. package/src/tools/monitor.ts +36 -1
  124. package/src/tools/subagent-render.ts +9 -0
  125. package/src/tools/subagent.ts +26 -2
@@ -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
+ }
@@ -0,0 +1,57 @@
1
+ import { getBaseUrl } from '../shared/base-url.js';
2
+
3
+ interface BillingUsage {
4
+ monthlyLimit: number;
5
+ used: number;
6
+ billingPeriodEnd: string;
7
+ }
8
+
9
+ function parseBillingUsage(payload: unknown): BillingUsage {
10
+ if (!payload || typeof payload !== 'object') throw new Error('invalid billing payload');
11
+ const config = (payload as Record<string, unknown>).config;
12
+ if (!config || typeof config !== 'object') throw new Error('invalid billing payload');
13
+ const monthlyLimit = ((config as Record<string, unknown>).monthlyLimit as Record<string, unknown>)
14
+ ?.val;
15
+ const used = ((config as Record<string, unknown>).used as Record<string, unknown>)?.val;
16
+ const billingPeriodEnd = (config as Record<string, unknown>).billingPeriodEnd;
17
+ if (
18
+ typeof monthlyLimit !== 'number' ||
19
+ !Number.isFinite(monthlyLimit) ||
20
+ typeof used !== 'number' ||
21
+ !Number.isFinite(used) ||
22
+ typeof billingPeriodEnd !== 'string' ||
23
+ !Number.isFinite(new Date(billingPeriodEnd).getTime())
24
+ ) {
25
+ throw new Error('invalid billing payload');
26
+ }
27
+ return { monthlyLimit, used, billingPeriodEnd };
28
+ }
29
+
30
+ export async function fetchBillingUsage(token: string): Promise<BillingUsage> {
31
+ const response = await fetch(`${getBaseUrl()}/billing`, {
32
+ headers: {
33
+ authorization: `Bearer ${token}`,
34
+ 'x-xai-token-auth': 'xai-grok-cli',
35
+ accept: 'application/json',
36
+ },
37
+ });
38
+ if (!response.ok) throw new Error(`billing endpoint returned ${response.status}`);
39
+ return parseBillingUsage(await response.json());
40
+ }
41
+
42
+ export function formatQuota(usage: BillingUsage | undefined) {
43
+ if (!usage) {
44
+ return [
45
+ ' Usage:',
46
+ ' no billing data available — run /login grok-build or set GROK_CLI_OAUTH_TOKEN',
47
+ ];
48
+ }
49
+
50
+ const resetDate = new Date(new Date(usage.billingPeriodEnd).getTime() - 8 * 60 * 60 * 1000);
51
+ return [
52
+ ' Usage:',
53
+ ` ${usage.used.toLocaleString()} / ${usage.monthlyLimit.toLocaleString()} credits used (${Math.round((usage.used / usage.monthlyLimit) * 100)}%)`,
54
+ ` ${(usage.monthlyLimit - usage.used).toLocaleString()} credits remaining`,
55
+ ` Resets at ${['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][resetDate.getUTCMonth()]} ${resetDate.getUTCDate()} ${resetDate.getUTCHours().toString().padStart(2, '0')}:${resetDate.getUTCMinutes().toString().padStart(2, '0')} PT`,
56
+ ];
57
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * GJC Grok Build provider — SuperGrok OAuth + cli-chat-proxy models.
3
+ */
4
+
5
+ import type { Api, Model } from '@gajae-code/ai';
6
+ import { Effort } from '@gajae-code/ai/model-thinking';
7
+ import type { OAuthCredentials, OAuthLoginCallbacks } from '@gajae-code/ai/utils/oauth/types';
8
+ import { loginXai, refreshXaiToken, XAI_OAUTH_SCOPE } from '@gajae-code/ai/utils/oauth/xai';
9
+ import type { ExtensionAPI, ProviderConfig } from '@gajae-code/coding-agent';
10
+ import { type GrokCliModelConfig, resolveModels } from '../models/catalog.js';
11
+ import { sanitizePayload } from '../payload/sanitize.js';
12
+ import { getBaseUrl, isGrokBuildBaseUrlOverrideIgnored } from '../shared/base-url.js';
13
+ import { streamGrokCli } from './stream.js';
14
+ import { registerUsageCommand } from './usage.js';
15
+
16
+ const GROK_BUILD_XAI_AUTHORIZE_PARAMS = {
17
+ plan: 'generic',
18
+ referrer: 'gjc-grok-cli',
19
+ } satisfies Readonly<Record<string, string>>;
20
+
21
+ const GROK_BUILD_XAI_REFRESH_PARAMS = {
22
+ scope: XAI_OAUTH_SCOPE,
23
+ ...GROK_BUILD_XAI_AUTHORIZE_PARAMS,
24
+ } satisfies Readonly<Record<string, string>>;
25
+
26
+ export default function registerGrokCli(api: ExtensionAPI) {
27
+ const baseUrl = getBaseUrl();
28
+ const models = resolveModels();
29
+
30
+ api.registerProvider('grok-build', {
31
+ baseUrl,
32
+ apiKey: process.env.GROK_CLI_OAUTH_TOKEN ? 'GROK_CLI_OAUTH_TOKEN' : undefined,
33
+ api: 'grok-cli-responses',
34
+ models: models.map((m: GrokCliModelConfig) => ({
35
+ id: m.id,
36
+ name: m.name,
37
+ reasoning: m.reasoning,
38
+ thinking: m.reasoning
39
+ ? { minLevel: Effort.Low, maxLevel: Effort.XHigh, mode: 'effort' }
40
+ : undefined,
41
+ input: m.input,
42
+ cost: m.cost,
43
+ contextWindow: m.contextWindow,
44
+ maxTokens: m.maxTokens,
45
+ })),
46
+ oauth: {
47
+ name: 'Grok Build',
48
+
49
+ async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
50
+ return loginXai(callbacks, { extraAuthorizeParams: GROK_BUILD_XAI_AUTHORIZE_PARAMS });
51
+ },
52
+
53
+ async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
54
+ return refreshXaiToken(credentials.refresh, {
55
+ extraTokenParams: GROK_BUILD_XAI_REFRESH_PARAMS,
56
+ });
57
+ },
58
+
59
+ getApiKey(credentials: OAuthCredentials): string {
60
+ return credentials.access;
61
+ },
62
+
63
+ modifyModels(models: Model<Api>[], _credentials: OAuthCredentials) {
64
+ const effectiveBaseUrl = getBaseUrl().replace(/\/+$/, '');
65
+
66
+ return models.map((m) =>
67
+ m.provider === 'grok-build' ? { ...m, baseUrl: effectiveBaseUrl } : m,
68
+ );
69
+ },
70
+ } satisfies ProviderConfig['oauth'],
71
+
72
+ streamSimple: streamGrokCli,
73
+ });
74
+
75
+ api.on('session_start', (_event, ctx) => {
76
+ if (process.env.GROK_CLI_OAUTH_TOKEN) {
77
+ ctx.ui.notify(
78
+ '[Grok Build] Using GROK_CLI_OAUTH_TOKEN env bypass — no auto-refresh, no model discovery. Login with /login grok-build for persisted refreshable auth.',
79
+ 'warning',
80
+ );
81
+ }
82
+ if (isGrokBuildBaseUrlOverrideIgnored()) {
83
+ ctx.ui.notify(
84
+ '[Grok Build] Ignoring unsafe Grok base URL override for OAuth credential safety. Set GJC_GROK_CLI_ALLOW_UNSAFE_BASE_URL=1 only for trusted local testing.',
85
+ 'warning',
86
+ );
87
+ }
88
+ });
89
+
90
+ api.on('before_provider_request', (event, ctx) => {
91
+ if (ctx.model?.provider !== 'grok-build') return;
92
+
93
+ const modelId = ctx.model?.id ?? '';
94
+ const sessionId = ctx.sessionManager?.getSessionId();
95
+ return sanitizePayload(event.payload as Record<string, unknown>, modelId, sessionId, ctx.cwd);
96
+ });
97
+
98
+ registerUsageCommand(api);
99
+ }