tide-commander 1.148.0 → 1.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
- package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
- package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
- package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
- package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/index-Ay7dKyeR.js +1 -0
- package/dist/assets/index-BMw7AAAK.js +5 -0
- package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
- package/dist/assets/index-CIon0DLf.js +2 -0
- package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
- package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
- package/dist/assets/index-DR6_WeF_.js +1 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
- package/dist/assets/main-D5sqeuwB.js +286 -0
- package/dist/assets/main-JBkwGXfE.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +3 -1
- package/dist/locales/en/config.json +2 -0
- package/dist/locales/en/terminal.json +6 -0
- package/dist/locales/en/tools.json +4 -2
- package/dist/locales/es/config.json +2 -0
- package/dist/locales/fr/config.json +3 -1
- package/dist/locales/hi/config.json +3 -1
- package/dist/locales/it/config.json +3 -1
- package/dist/locales/ja/config.json +3 -1
- package/dist/locales/pt/config.json +3 -1
- package/dist/locales/ru/config.json +3 -1
- package/dist/locales/zh-CN/config.json +3 -1
- package/dist/src/packages/server/claude/backend.js +114 -10
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +295 -0
- package/dist/src/packages/server/codex/json-event-parser.js +149 -4
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +235 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +153 -0
- package/dist/src/packages/server/grok/session-watcher.js +493 -0
- package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
- package/dist/src/packages/server/routes/agents.js +19 -11
- package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +1 -0
- package/dist/src/packages/server/services/agent-service.js +26 -5
- package/dist/src/packages/server/services/grok-usage-service.js +374 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
- package/dist/src/packages/server/services/runtime-events.js +46 -7
- package/dist/src/packages/server/services/runtime-service.js +19 -3
- package/dist/src/packages/server/utils/tool-formatting.js +40 -12
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
- package/dist/src/packages/shared/agent-types.js +10 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
- package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
- package/dist/assets/SceneManager-QqKygyaw.js +0 -104
- package/dist/assets/SpawnModal-BOeStihN.js +0 -1
- package/dist/assets/index-DOzFBkSH.js +0 -1
- package/dist/assets/index-DP7kd91h.js +0 -1
- package/dist/assets/index-DRyMARFk.js +0 -5
- package/dist/assets/index-DuWmOZdY.js +0 -2
- package/dist/assets/main-C2zgPci-.js +0 -256
- package/dist/assets/main-C3JO2TRI.css +0 -1
|
@@ -156,6 +156,15 @@ export class CodexJsonEventParser {
|
|
|
156
156
|
enableFileDiffEnrichment;
|
|
157
157
|
workingDirectory;
|
|
158
158
|
gitRootCache = new Map();
|
|
159
|
+
/**
|
|
160
|
+
* Progressive agent_message text per item.id (for item.updated / delta streams).
|
|
161
|
+
* Current `codex exec --json` only emits agent_message on item.completed, but
|
|
162
|
+
* app-server / newer CLIs can stream via item.updated or agentMessage deltas.
|
|
163
|
+
*/
|
|
164
|
+
streamedAgentTextByItemId = new Map();
|
|
165
|
+
openAgentTextUuids = new Set();
|
|
166
|
+
streamedThinkingByItemId = new Map();
|
|
167
|
+
openThinkingUuids = new Set();
|
|
159
168
|
constructor(options = {}) {
|
|
160
169
|
this.enableFileDiffEnrichment = options.enableFileDiffEnrichment === true;
|
|
161
170
|
this.workingDirectory = options.workingDirectory || process.cwd();
|
|
@@ -190,6 +199,9 @@ export class CodexJsonEventParser {
|
|
|
190
199
|
if (event.type === 'item.started') {
|
|
191
200
|
return this.parseItemStarted(event.item);
|
|
192
201
|
}
|
|
202
|
+
if (event.type === 'item.updated') {
|
|
203
|
+
return this.parseItemUpdated(event.item);
|
|
204
|
+
}
|
|
193
205
|
if (event.type === 'item.completed') {
|
|
194
206
|
// Capture agent message text for later use in step_complete
|
|
195
207
|
if (event.item?.type === 'agent_message' && event.item?.text) {
|
|
@@ -197,6 +209,12 @@ export class CodexJsonEventParser {
|
|
|
197
209
|
}
|
|
198
210
|
return this.parseItemCompleted(event.item);
|
|
199
211
|
}
|
|
212
|
+
// App-server style notification (when present on exec stream): streaming text delta.
|
|
213
|
+
if (event.type === 'item/agentMessage/delta' ||
|
|
214
|
+
event.type === 'item.agentMessage.delta' ||
|
|
215
|
+
event.type === 'agent_message_delta') {
|
|
216
|
+
return this.parseAgentMessageDelta(rawEvent);
|
|
217
|
+
}
|
|
200
218
|
if (event.type === 'turn.completed') {
|
|
201
219
|
return this.parseTurnCompleted(event.usage);
|
|
202
220
|
}
|
|
@@ -228,12 +246,27 @@ export class CodexJsonEventParser {
|
|
|
228
246
|
};
|
|
229
247
|
return [];
|
|
230
248
|
}
|
|
231
|
-
// agent_reasoning: Map to thinking event
|
|
249
|
+
// agent_reasoning: Map to thinking event (stable uuid when item id present)
|
|
232
250
|
if (payloadType === 'agent_reasoning') {
|
|
233
251
|
const text = asString(payload.text);
|
|
234
252
|
if (!text)
|
|
235
253
|
return [];
|
|
236
|
-
|
|
254
|
+
const itemId = asString(payload.item_id) || asString(payload.id);
|
|
255
|
+
const uuid = itemId ? `codex-thinking-${itemId}` : undefined;
|
|
256
|
+
return [{ type: 'thinking', text, isStreaming: false, uuid }];
|
|
257
|
+
}
|
|
258
|
+
// Streaming agent message delta (app-server / experimental envelopes)
|
|
259
|
+
if (payloadType === 'agent_message_delta' ||
|
|
260
|
+
payloadType === 'agent_message/delta' ||
|
|
261
|
+
payloadType === 'output_text_delta') {
|
|
262
|
+
const delta = asString(payload.text) ||
|
|
263
|
+
asString(payload.delta);
|
|
264
|
+
if (!delta)
|
|
265
|
+
return [];
|
|
266
|
+
const itemId = asString(payload.item_id) ||
|
|
267
|
+
asString(payload.id) ||
|
|
268
|
+
'anon';
|
|
269
|
+
return this.appendAgentTextDelta(itemId, delta);
|
|
237
270
|
}
|
|
238
271
|
// turn_aborted: Silent. The interruption is already visible from the agent stopping.
|
|
239
272
|
if (payloadType === 'turn_aborted') {
|
|
@@ -391,9 +424,109 @@ export class CodexJsonEventParser {
|
|
|
391
424
|
toolOutput,
|
|
392
425
|
}];
|
|
393
426
|
}
|
|
427
|
+
/** Stable uuid for a Codex agent_message item so streaming chunks merge. */
|
|
428
|
+
agentTextUuid(itemId) {
|
|
429
|
+
return `codex-text-${itemId}`;
|
|
430
|
+
}
|
|
431
|
+
appendAgentTextDelta(itemId, delta) {
|
|
432
|
+
if (!delta)
|
|
433
|
+
return [];
|
|
434
|
+
const prev = this.streamedAgentTextByItemId.get(itemId) || '';
|
|
435
|
+
const next = prev + delta;
|
|
436
|
+
this.streamedAgentTextByItemId.set(itemId, next);
|
|
437
|
+
this.lastAgentMessageText = next;
|
|
438
|
+
const uuid = this.agentTextUuid(itemId);
|
|
439
|
+
this.openAgentTextUuids.add(uuid);
|
|
440
|
+
return [{ type: 'text', text: delta, isStreaming: true, uuid }];
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Progressive snapshot for agent_message / reasoning on item.updated.
|
|
444
|
+
* Emits only the new suffix when text grows (prefix match).
|
|
445
|
+
*/
|
|
446
|
+
emitProgressiveItemText(kind, itemId, fullText, isFinal) {
|
|
447
|
+
if (!fullText)
|
|
448
|
+
return [];
|
|
449
|
+
const id = itemId || 'anon';
|
|
450
|
+
const map = kind === 'text' ? this.streamedAgentTextByItemId : this.streamedThinkingByItemId;
|
|
451
|
+
const openSet = kind === 'text' ? this.openAgentTextUuids : this.openThinkingUuids;
|
|
452
|
+
const uuid = kind === 'text' ? this.agentTextUuid(id) : `codex-thinking-${id}`;
|
|
453
|
+
const prev = map.get(id) || '';
|
|
454
|
+
if (kind === 'text')
|
|
455
|
+
this.lastAgentMessageText = fullText;
|
|
456
|
+
if (fullText === prev) {
|
|
457
|
+
if (isFinal && openSet.has(uuid)) {
|
|
458
|
+
openSet.delete(uuid);
|
|
459
|
+
map.delete(id);
|
|
460
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: fullText, isStreaming: false, uuid }];
|
|
461
|
+
}
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
map.set(id, fullText);
|
|
465
|
+
if (!isFinal && fullText.startsWith(prev)) {
|
|
466
|
+
const delta = fullText.slice(prev.length);
|
|
467
|
+
if (!delta)
|
|
468
|
+
return [];
|
|
469
|
+
openSet.add(uuid);
|
|
470
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: delta, isStreaming: true, uuid }];
|
|
471
|
+
}
|
|
472
|
+
openSet.delete(uuid);
|
|
473
|
+
if (isFinal)
|
|
474
|
+
map.delete(id);
|
|
475
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: fullText, isStreaming: false, uuid }];
|
|
476
|
+
}
|
|
477
|
+
parseItemUpdated(item) {
|
|
478
|
+
if (!item?.type)
|
|
479
|
+
return [];
|
|
480
|
+
// Progressive agent_message text (when CLI emits item.updated for messages).
|
|
481
|
+
if (item.type === 'agent_message' && item.text) {
|
|
482
|
+
const sanitized = sanitizeCodexMessageText(item.text);
|
|
483
|
+
if (!sanitized.text)
|
|
484
|
+
return [];
|
|
485
|
+
return this.emitProgressiveItemText('text', item.id, sanitized.text, false);
|
|
486
|
+
}
|
|
487
|
+
if (item.type === 'reasoning' && item.text) {
|
|
488
|
+
return this.emitProgressiveItemText('thinking', item.id, item.text, false);
|
|
489
|
+
}
|
|
490
|
+
// todo_list updates are already handled elsewhere as non-text progress;
|
|
491
|
+
// leave other item.updated types silent to avoid raw dumps.
|
|
492
|
+
if (item.type === 'todo_list') {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
parseAgentMessageDelta(rawEvent) {
|
|
498
|
+
if (!isObject(rawEvent))
|
|
499
|
+
return [];
|
|
500
|
+
const item = isObject(rawEvent.item) ? rawEvent.item : undefined;
|
|
501
|
+
const params = isObject(rawEvent.params) ? rawEvent.params : undefined;
|
|
502
|
+
const delta = asString(rawEvent.delta) ||
|
|
503
|
+
asString(rawEvent.text) ||
|
|
504
|
+
asString(item?.text) ||
|
|
505
|
+
(params ? asString(params.delta) || asString(params.text) : undefined);
|
|
506
|
+
if (!delta)
|
|
507
|
+
return [];
|
|
508
|
+
const itemId = asString(item?.id) ||
|
|
509
|
+
asString(rawEvent.item_id) ||
|
|
510
|
+
asString(rawEvent.id) ||
|
|
511
|
+
(params ? asString(params.item_id) || asString(params.id) : undefined) ||
|
|
512
|
+
'anon';
|
|
513
|
+
return this.appendAgentTextDelta(itemId, delta);
|
|
514
|
+
}
|
|
394
515
|
parseItemStarted(item) {
|
|
395
516
|
if (!item?.type)
|
|
396
517
|
return [];
|
|
518
|
+
// Some Codex builds open agent_message with empty text on item.started;
|
|
519
|
+
// pin the stream uuid so later updates merge into one row.
|
|
520
|
+
if (item.type === 'agent_message') {
|
|
521
|
+
if (item.id) {
|
|
522
|
+
const uuid = this.agentTextUuid(item.id);
|
|
523
|
+
this.openAgentTextUuids.add(uuid);
|
|
524
|
+
if (item.text) {
|
|
525
|
+
return this.emitProgressiveItemText('text', item.id, item.text, false);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
397
530
|
if (item.type === 'web_search') {
|
|
398
531
|
const toolName = 'web_search';
|
|
399
532
|
if (item.id) {
|
|
@@ -446,7 +579,12 @@ export class CodexJsonEventParser {
|
|
|
446
579
|
return [{ type: 'error', errorMessage }];
|
|
447
580
|
}
|
|
448
581
|
if (item.type === 'reasoning' && item.text) {
|
|
449
|
-
|
|
582
|
+
const uuid = item.id ? `codex-thinking-${item.id}` : undefined;
|
|
583
|
+
if (item.id) {
|
|
584
|
+
this.streamedThinkingByItemId.delete(item.id);
|
|
585
|
+
this.openThinkingUuids.delete(uuid);
|
|
586
|
+
}
|
|
587
|
+
return [{ type: 'thinking', text: item.text, isStreaming: false, uuid }];
|
|
450
588
|
}
|
|
451
589
|
// Handle agent_message regardless of whether `text` is present. An empty
|
|
452
590
|
// agent_message (text:"" or missing) must be suppressed — NOT allowed to
|
|
@@ -467,7 +605,14 @@ export class CodexJsonEventParser {
|
|
|
467
605
|
// Empty message → render nothing (no empty bubble, no raw debug line).
|
|
468
606
|
if (!sanitized.text)
|
|
469
607
|
return [];
|
|
470
|
-
|
|
608
|
+
const uuid = item.id ? `codex-text-${item.id}` : undefined;
|
|
609
|
+
if (item.id) {
|
|
610
|
+
this.streamedAgentTextByItemId.delete(item.id);
|
|
611
|
+
if (uuid)
|
|
612
|
+
this.openAgentTextUuids.delete(uuid);
|
|
613
|
+
}
|
|
614
|
+
// isStreaming:false finalizes any prior streaming row with the same uuid.
|
|
615
|
+
return [{ type: 'text', text: sanitized.text, isStreaming: false, uuid }];
|
|
471
616
|
}
|
|
472
617
|
if (item.type === 'web_search') {
|
|
473
618
|
const toolName = item.id ? (this.activeToolByItemId.get(item.id) ?? 'web_search') : 'web_search';
|
|
@@ -129,6 +129,7 @@ function toStoredAgents(agents) {
|
|
|
129
129
|
codexModel: agent.codexModel,
|
|
130
130
|
codexConfig: agent.codexConfig,
|
|
131
131
|
opencodeModel: agent.opencodeModel,
|
|
132
|
+
grokModel: agent.grokModel,
|
|
132
133
|
createdAt: agent.createdAt,
|
|
133
134
|
lastActivity: agent.lastActivity,
|
|
134
135
|
sessionId: agent.sessionId,
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok CLI Backend (headless mode)
|
|
3
|
+
*
|
|
4
|
+
* Spawns: grok -p … --output-format streaming-json [--yolo] [--resume id] …
|
|
5
|
+
* Large prompts go through --prompt-file (Grok does not read the prompt from stdin).
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { GrokJsonEventParser } from './json-event-parser.js';
|
|
12
|
+
import { TIDE_COMMANDER_APPENDED_PROMPT } from '../prompts/tide-commander.js';
|
|
13
|
+
import { getSystemPrompt, isEchoPromptEnabled } from '../services/system-prompt-service.js';
|
|
14
|
+
import { consumeInstructionsDirty, isBareSlashCommand } from '../services/instruction-refresh.js';
|
|
15
|
+
import { loadAreas } from '../data/index.js';
|
|
16
|
+
import { createLogger } from '../utils/logger.js';
|
|
17
|
+
const log = createLogger('GrokBackend');
|
|
18
|
+
/** Prompt size above which we switch from -p to --prompt-file (argv safety). */
|
|
19
|
+
const PROMPT_FILE_THRESHOLD = 4000;
|
|
20
|
+
export function buildGrokPrompt(config) {
|
|
21
|
+
const userPrompt = config.prompt?.trim() || 'Continue the task.';
|
|
22
|
+
// Bare slash commands must reach the CLI verbatim.
|
|
23
|
+
if (isBareSlashCommand(userPrompt)) {
|
|
24
|
+
return userPrompt;
|
|
25
|
+
}
|
|
26
|
+
const echoedUserPrompt = isEchoPromptEnabled()
|
|
27
|
+
? userPrompt + '\n\n---\n\n' + userPrompt
|
|
28
|
+
: userPrompt;
|
|
29
|
+
// On resume the instruction block is already in session history — only
|
|
30
|
+
// re-inject when an instruction source was dirtied mid-session.
|
|
31
|
+
const refreshInstructions = consumeInstructionsDirty(config.agentId);
|
|
32
|
+
if (config.sessionId && !refreshInstructions) {
|
|
33
|
+
return echoedUserPrompt;
|
|
34
|
+
}
|
|
35
|
+
const injectedSections = [];
|
|
36
|
+
const systemLevelPrompt = getSystemPrompt().trim();
|
|
37
|
+
if (systemLevelPrompt) {
|
|
38
|
+
injectedSections.push(`## System-Level Custom Prompt\n${systemLevelPrompt}`);
|
|
39
|
+
}
|
|
40
|
+
if (config.agentId) {
|
|
41
|
+
const areas = loadAreas();
|
|
42
|
+
const agentArea = areas.find(a => a.assignedAgentIds.includes(config.agentId));
|
|
43
|
+
const areaPrompt = agentArea?.prompt?.trim();
|
|
44
|
+
if (areaPrompt) {
|
|
45
|
+
injectedSections.push(`## Area-Level Prompt (${agentArea.name})\n${areaPrompt}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const customPrompt = config.customAgent?.definition?.prompt?.trim();
|
|
49
|
+
if (customPrompt) {
|
|
50
|
+
injectedSections.push(`## Agent Instructions\n${customPrompt}`);
|
|
51
|
+
}
|
|
52
|
+
const systemPrompt = config.systemPrompt?.trim();
|
|
53
|
+
if (systemPrompt) {
|
|
54
|
+
injectedSections.push(`## System Context\n${systemPrompt}`);
|
|
55
|
+
}
|
|
56
|
+
injectedSections.push(TIDE_COMMANDER_APPENDED_PROMPT);
|
|
57
|
+
return [
|
|
58
|
+
'Follow all instructions below for this task.',
|
|
59
|
+
...injectedSections,
|
|
60
|
+
'## User Request',
|
|
61
|
+
echoedUserPrompt,
|
|
62
|
+
].join('\n\n');
|
|
63
|
+
}
|
|
64
|
+
function shouldPassGrokModel(model) {
|
|
65
|
+
if (!model)
|
|
66
|
+
return false;
|
|
67
|
+
// Reject Claude/Codex short names that may leak across provider switches
|
|
68
|
+
if (model === 'sonnet' ||
|
|
69
|
+
model === 'opus' ||
|
|
70
|
+
model === 'haiku' ||
|
|
71
|
+
model === 'codex' ||
|
|
72
|
+
model.startsWith('claude-') ||
|
|
73
|
+
model.startsWith('gpt-')) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
export class GrokBackend {
|
|
79
|
+
name = 'grok';
|
|
80
|
+
parser = new GrokJsonEventParser();
|
|
81
|
+
/** Temp prompt files created for this process; cleaned after formatStdin (no-op) or next buildArgs. */
|
|
82
|
+
lastPromptFile;
|
|
83
|
+
buildArgs(config) {
|
|
84
|
+
// Clean previous prompt file if any (best-effort)
|
|
85
|
+
this.cleanupPromptFile();
|
|
86
|
+
const prompt = buildGrokPrompt(config);
|
|
87
|
+
const args = ['--output-format', 'streaming-json', '--no-auto-update'];
|
|
88
|
+
// Autonomous by default (matches OpenCode/Codex Tide defaults). Interactive
|
|
89
|
+
// mode still uses yolo for headless — Grok can't prompt through our UI yet.
|
|
90
|
+
if (config.permissionMode !== 'interactive') {
|
|
91
|
+
args.push('--yolo');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Interactive still needs unattended tool approval for headless; use
|
|
95
|
+
// always-approve so runs don't hang waiting for a TTY prompt.
|
|
96
|
+
args.push('--always-approve');
|
|
97
|
+
}
|
|
98
|
+
if (config.workingDir) {
|
|
99
|
+
args.push('--cwd', config.workingDir);
|
|
100
|
+
}
|
|
101
|
+
if (shouldPassGrokModel(config.model)) {
|
|
102
|
+
args.push('-m', config.model);
|
|
103
|
+
}
|
|
104
|
+
if (config.effort) {
|
|
105
|
+
// Map Tide effort labels to Grok reasoning-effort values
|
|
106
|
+
const effortMap = {
|
|
107
|
+
low: 'low',
|
|
108
|
+
medium: 'medium',
|
|
109
|
+
high: 'high',
|
|
110
|
+
xHigh: 'xhigh',
|
|
111
|
+
max: 'max',
|
|
112
|
+
};
|
|
113
|
+
const mapped = effortMap[config.effort] || config.effort.toLowerCase();
|
|
114
|
+
args.push('--reasoning-effort', mapped);
|
|
115
|
+
}
|
|
116
|
+
// Session resume / fork
|
|
117
|
+
if (config.sessionId) {
|
|
118
|
+
args.push('--resume', config.sessionId);
|
|
119
|
+
if (config.forkSession) {
|
|
120
|
+
args.push('--fork-session');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Prompt delivery: -p for small prompts, --prompt-file for large ones
|
|
124
|
+
// (Grok does not read the prompt from stdin).
|
|
125
|
+
if (prompt.length > PROMPT_FILE_THRESHOLD) {
|
|
126
|
+
const promptFile = path.join(os.tmpdir(), `tide-grok-prompt-${config.agentId || 'anon'}-${Date.now()}.txt`);
|
|
127
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
128
|
+
this.lastPromptFile = promptFile;
|
|
129
|
+
args.push('--prompt-file', promptFile);
|
|
130
|
+
log.log(`buildArgs: prompt via file (${prompt.length} chars) sessionId=${config.sessionId ? 'yes' : 'no'}`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
args.push('-p', prompt);
|
|
134
|
+
log.log(`buildArgs: prompt via -p (${prompt.length} chars) sessionId=${config.sessionId ? 'yes' : 'no'}`);
|
|
135
|
+
}
|
|
136
|
+
return args;
|
|
137
|
+
}
|
|
138
|
+
parseEvent(rawEvent) {
|
|
139
|
+
const events = this.parser.parseEvent(rawEvent);
|
|
140
|
+
if (events.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return events.length === 1 ? events[0] : events;
|
|
143
|
+
}
|
|
144
|
+
/** See CLIBackend.breakOpenStreams — tool-boundary stream split for Grok. */
|
|
145
|
+
breakOpenStreams() {
|
|
146
|
+
return this.parser.breakOpenStreams();
|
|
147
|
+
}
|
|
148
|
+
extractSessionId(rawEvent) {
|
|
149
|
+
const event = rawEvent;
|
|
150
|
+
if (event?.sessionId && typeof event.sessionId === 'string') {
|
|
151
|
+
return event.sessionId;
|
|
152
|
+
}
|
|
153
|
+
// end event also carries sessionId
|
|
154
|
+
if (event?.type === 'end' && typeof event.sessionId === 'string') {
|
|
155
|
+
return event.sessionId;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
getExecutablePath() {
|
|
160
|
+
const envBinary = process.env.GROK_BINARY;
|
|
161
|
+
if (envBinary && fs.existsSync(envBinary)) {
|
|
162
|
+
return envBinary;
|
|
163
|
+
}
|
|
164
|
+
return this.detectInstallation() || 'grok';
|
|
165
|
+
}
|
|
166
|
+
detectInstallation() {
|
|
167
|
+
const homeDir = os.homedir();
|
|
168
|
+
const isWindows = process.platform === 'win32';
|
|
169
|
+
const possiblePaths = isWindows
|
|
170
|
+
? [
|
|
171
|
+
path.join(homeDir, '.grok', 'bin', 'grok.exe'),
|
|
172
|
+
path.join(homeDir, 'AppData', 'Roaming', 'npm', 'grok.cmd'),
|
|
173
|
+
]
|
|
174
|
+
: [
|
|
175
|
+
path.join(homeDir, '.grok', 'bin', 'grok'),
|
|
176
|
+
path.join(homeDir, '.local', 'bin', 'grok'),
|
|
177
|
+
'/usr/local/bin/grok',
|
|
178
|
+
'/usr/bin/grok',
|
|
179
|
+
];
|
|
180
|
+
for (const p of possiblePaths) {
|
|
181
|
+
if (fs.existsSync(p))
|
|
182
|
+
return p;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const result = execSync('which grok', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
186
|
+
return result || null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
getExtraEnv() {
|
|
193
|
+
// Ensure ~/.grok/bin is on PATH if present (managed installs live there)
|
|
194
|
+
const grokBin = path.join(os.homedir(), '.grok', 'bin');
|
|
195
|
+
if (fs.existsSync(grokBin)) {
|
|
196
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
197
|
+
return { PATH: grokBin + sep + (process.env.PATH || '') };
|
|
198
|
+
}
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Prompt is passed via -p / --prompt-file, not stdin.
|
|
203
|
+
*/
|
|
204
|
+
requiresStdinInput() {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Mark as stdin-closed so follow-up messages queue and respawn with --resume
|
|
209
|
+
* (same delivery path as Codex/OpenCode).
|
|
210
|
+
*/
|
|
211
|
+
shouldCloseStdinAfterPrompt() {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
supportsSessionResume() {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
formatStdinInput(_prompt) {
|
|
218
|
+
// Not used (requiresStdinInput=false). Clean up any leftover prompt file.
|
|
219
|
+
this.cleanupPromptFile();
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
cleanupPromptFile() {
|
|
223
|
+
if (!this.lastPromptFile)
|
|
224
|
+
return;
|
|
225
|
+
try {
|
|
226
|
+
if (fs.existsSync(this.lastPromptFile)) {
|
|
227
|
+
fs.unlinkSync(this.lastPromptFile);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// best-effort
|
|
232
|
+
}
|
|
233
|
+
this.lastPromptFile = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { GrokBackend, buildGrokPrompt } from './backend.js';
|
|
2
|
+
export { GrokJsonEventParser } from './json-event-parser.js';
|
|
3
|
+
export { startGrokSessionWatcher, encodeGrokProjectKey, getGrokSessionsRoot, getGrokSessionDir, normalizeGrokToolName, } from './session-watcher.js';
|
|
4
|
+
export const GROK_PROVIDER_NAME = 'grok';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok headless streaming-json event parser.
|
|
3
|
+
* Maps NDJSON events from `grok -p … --output-format streaming-json` to StandardEvent.
|
|
4
|
+
*
|
|
5
|
+
* Documented event types: text, thought, end, error.
|
|
6
|
+
* Also handles max_turns_reached / auto_compact_* defensively.
|
|
7
|
+
*
|
|
8
|
+
* Grok emits one tiny token per NDJSON line. The client only merges streaming
|
|
9
|
+
* chunks when they share a `uuid`, so we mint a stable stream id per block
|
|
10
|
+
* (text / thinking) for the turn and finalize with isStreaming=false on end.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
const log = createLogger('GrokParser');
|
|
15
|
+
export class GrokJsonEventParser {
|
|
16
|
+
lastTextContent = '';
|
|
17
|
+
lastThinkingContent = '';
|
|
18
|
+
textEventEmittedInTurn = false;
|
|
19
|
+
/** Stable uuid so token-sized deltas merge into one terminal row. */
|
|
20
|
+
textStreamUuid;
|
|
21
|
+
thinkingStreamUuid;
|
|
22
|
+
parseEvent(rawEvent) {
|
|
23
|
+
const event = rawEvent;
|
|
24
|
+
if (!event || typeof event !== 'object' || !event.type) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
log.log(`parseEvent: type=${event.type}, sessionId=${event.sessionId || 'none'}`);
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case 'text':
|
|
30
|
+
return this.parseText(event);
|
|
31
|
+
case 'thought':
|
|
32
|
+
return this.parseThought(event);
|
|
33
|
+
case 'end':
|
|
34
|
+
return this.parseEnd(event);
|
|
35
|
+
case 'error':
|
|
36
|
+
return this.parseError(event);
|
|
37
|
+
case 'max_turns_reached':
|
|
38
|
+
return [{
|
|
39
|
+
type: 'error',
|
|
40
|
+
errorMessage: event.message || 'Max turns reached',
|
|
41
|
+
sessionId: event.sessionId,
|
|
42
|
+
}];
|
|
43
|
+
default:
|
|
44
|
+
// Unknown / telemetry events — ignore for UI, but keep the stream alive
|
|
45
|
+
log.log(`parseEvent: unknown event type '${event.type}' - skipping`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
ensureTextStreamUuid() {
|
|
50
|
+
if (!this.textStreamUuid) {
|
|
51
|
+
this.textStreamUuid = `grok-text-${randomUUID()}`;
|
|
52
|
+
}
|
|
53
|
+
return this.textStreamUuid;
|
|
54
|
+
}
|
|
55
|
+
ensureThinkingStreamUuid() {
|
|
56
|
+
if (!this.thinkingStreamUuid) {
|
|
57
|
+
this.thinkingStreamUuid = `grok-thinking-${randomUUID()}`;
|
|
58
|
+
}
|
|
59
|
+
return this.thinkingStreamUuid;
|
|
60
|
+
}
|
|
61
|
+
parseText(event) {
|
|
62
|
+
const chunk = event.data ?? event.text;
|
|
63
|
+
if (!chunk)
|
|
64
|
+
return [];
|
|
65
|
+
this.lastTextContent += chunk;
|
|
66
|
+
this.textEventEmittedInTurn = true;
|
|
67
|
+
return [{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: chunk,
|
|
70
|
+
isStreaming: true,
|
|
71
|
+
uuid: this.ensureTextStreamUuid(),
|
|
72
|
+
sessionId: event.sessionId,
|
|
73
|
+
}];
|
|
74
|
+
}
|
|
75
|
+
parseThought(event) {
|
|
76
|
+
const chunk = event.data ?? event.text;
|
|
77
|
+
if (!chunk)
|
|
78
|
+
return [];
|
|
79
|
+
this.lastThinkingContent += chunk;
|
|
80
|
+
return [{
|
|
81
|
+
type: 'thinking',
|
|
82
|
+
text: chunk,
|
|
83
|
+
isStreaming: true,
|
|
84
|
+
uuid: this.ensureThinkingStreamUuid(),
|
|
85
|
+
sessionId: event.sessionId,
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Finalize open text/thinking streams and mint new uuids for the next
|
|
90
|
+
* generation — WITHOUT step_complete (agent stays "working").
|
|
91
|
+
*
|
|
92
|
+
* Grok keeps one agentic turn across many tool rounds and does not emit
|
|
93
|
+
* `end` between them. Without this break, every intermediate status line
|
|
94
|
+
* concatenates into a single mashed bubble until the final end.
|
|
95
|
+
*/
|
|
96
|
+
breakOpenStreams(sessionId) {
|
|
97
|
+
const events = [];
|
|
98
|
+
if (this.thinkingStreamUuid && this.lastThinkingContent) {
|
|
99
|
+
events.push({
|
|
100
|
+
type: 'thinking',
|
|
101
|
+
text: this.lastThinkingContent,
|
|
102
|
+
isStreaming: false,
|
|
103
|
+
uuid: this.thinkingStreamUuid,
|
|
104
|
+
sessionId,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (this.textStreamUuid && this.lastTextContent) {
|
|
108
|
+
events.push({
|
|
109
|
+
type: 'text',
|
|
110
|
+
text: this.lastTextContent,
|
|
111
|
+
isStreaming: false,
|
|
112
|
+
uuid: this.textStreamUuid,
|
|
113
|
+
sessionId,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
this.lastTextContent = '';
|
|
117
|
+
this.lastThinkingContent = '';
|
|
118
|
+
// Keep textEventEmittedInTurn so a later end/step_complete still knows
|
|
119
|
+
// we produced text this turn (avoids empty-thinking fallback).
|
|
120
|
+
this.textStreamUuid = undefined;
|
|
121
|
+
this.thinkingStreamUuid = undefined;
|
|
122
|
+
return events;
|
|
123
|
+
}
|
|
124
|
+
parseEnd(event) {
|
|
125
|
+
// Finalize open streams (same as a tool-boundary break) then step_complete.
|
|
126
|
+
const events = this.breakOpenStreams(event.sessionId);
|
|
127
|
+
// breakOpenStreams cleared accumulators — re-read is not possible. Capture
|
|
128
|
+
// resultText from the finalize events we just built.
|
|
129
|
+
const finalizedText = events.find((e) => e.type === 'text' && e.text)?.text;
|
|
130
|
+
const finalizedThinking = events.find((e) => e.type === 'thinking' && e.text)?.text;
|
|
131
|
+
const stepComplete = {
|
|
132
|
+
type: 'step_complete',
|
|
133
|
+
sessionId: event.sessionId,
|
|
134
|
+
};
|
|
135
|
+
if (finalizedText) {
|
|
136
|
+
stepComplete.resultText = finalizedText;
|
|
137
|
+
}
|
|
138
|
+
else if (finalizedThinking && !this.textEventEmittedInTurn) {
|
|
139
|
+
// Thinking-only turn — surface a marker so the terminal isn't blank
|
|
140
|
+
stepComplete.resultText = `(Empty response: thinking only, stopReason=${event.stopReason || 'EndTurn'})`;
|
|
141
|
+
}
|
|
142
|
+
events.push(stepComplete);
|
|
143
|
+
this.textEventEmittedInTurn = false;
|
|
144
|
+
return events;
|
|
145
|
+
}
|
|
146
|
+
parseError(event) {
|
|
147
|
+
return [{
|
|
148
|
+
type: 'error',
|
|
149
|
+
errorMessage: event.message || event.data || 'Grok CLI error',
|
|
150
|
+
sessionId: event.sessionId,
|
|
151
|
+
}];
|
|
152
|
+
}
|
|
153
|
+
}
|