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.
Files changed (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -3
  3. package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
  4. package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
  5. package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
  10. package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
  12. package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
  13. package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
  15. package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
  16. package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
  17. package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
  18. package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
  20. package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
  21. package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
  25. package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
  26. package/dist/assets/grok.ico +0 -0
  27. package/dist/assets/grok.png +0 -0
  28. package/dist/assets/index-Ay7dKyeR.js +1 -0
  29. package/dist/assets/index-BMw7AAAK.js +5 -0
  30. package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
  31. package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
  32. package/dist/assets/index-CIon0DLf.js +2 -0
  33. package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
  34. package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
  35. package/dist/assets/index-DR6_WeF_.js +1 -0
  36. package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
  37. package/dist/assets/main-D5sqeuwB.js +286 -0
  38. package/dist/assets/main-JBkwGXfE.css +1 -0
  39. package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
  40. package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
  41. package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/de/config.json +3 -1
  44. package/dist/locales/en/config.json +2 -0
  45. package/dist/locales/en/terminal.json +6 -0
  46. package/dist/locales/en/tools.json +4 -2
  47. package/dist/locales/es/config.json +2 -0
  48. package/dist/locales/fr/config.json +3 -1
  49. package/dist/locales/hi/config.json +3 -1
  50. package/dist/locales/it/config.json +3 -1
  51. package/dist/locales/ja/config.json +3 -1
  52. package/dist/locales/pt/config.json +3 -1
  53. package/dist/locales/ru/config.json +3 -1
  54. package/dist/locales/zh-CN/config.json +3 -1
  55. package/dist/src/packages/server/claude/backend.js +114 -10
  56. package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
  57. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
  58. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  59. package/dist/src/packages/server/claude/session-loader.js +295 -0
  60. package/dist/src/packages/server/codex/json-event-parser.js +149 -4
  61. package/dist/src/packages/server/data/index.js +1 -0
  62. package/dist/src/packages/server/grok/backend.js +235 -0
  63. package/dist/src/packages/server/grok/index.js +4 -0
  64. package/dist/src/packages/server/grok/json-event-parser.js +153 -0
  65. package/dist/src/packages/server/grok/session-watcher.js +493 -0
  66. package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
  67. package/dist/src/packages/server/routes/agents.js +19 -11
  68. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  69. package/dist/src/packages/server/runtime/index.js +1 -0
  70. package/dist/src/packages/server/services/agent-service.js +26 -5
  71. package/dist/src/packages/server/services/grok-usage-service.js +374 -0
  72. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  73. package/dist/src/packages/server/services/runtime-events.js +46 -7
  74. package/dist/src/packages/server/services/runtime-service.js +19 -3
  75. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  76. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  77. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
  78. package/dist/src/packages/shared/agent-types.js +10 -0
  79. package/package.json +1 -1
  80. package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
  81. package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
  82. package/dist/assets/SceneManager-QqKygyaw.js +0 -104
  83. package/dist/assets/SpawnModal-BOeStihN.js +0 -1
  84. package/dist/assets/index-DOzFBkSH.js +0 -1
  85. package/dist/assets/index-DP7kd91h.js +0 -1
  86. package/dist/assets/index-DRyMARFk.js +0 -5
  87. package/dist/assets/index-DuWmOZdY.js +0 -2
  88. package/dist/assets/main-C2zgPci-.js +0 -256
  89. 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
- return [{ type: 'thinking', text, isStreaming: false }];
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
- return [{ type: 'thinking', text: item.text, isStreaming: false }];
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
- return [{ type: 'text', text: sanitized.text, isStreaming: false }];
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
+ }