tide-commander 1.148.0 → 1.149.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 (73) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -3
  3. package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-Cs4kWugD.js} +1 -1
  4. package/dist/assets/BossSpawnModal-BudMgJqj.js +1 -0
  5. package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-Bh6_3qtH.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-Nf4wQFfz.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-BfRXRX50.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-CXUtjA0r.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D-_rwicD.js} +1 -1
  10. package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-CdaXsQK0.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-ZsSVxcWr.js} +2 -2
  12. package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-BAf-klB2.js} +1 -1
  13. package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-BjRbh5z4.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-SaywDZVL.js} +1 -1
  15. package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-jru2Ez9a.js} +1 -1
  16. package/dist/assets/Scene2DCanvas-NJHoe2Qg.js +1 -0
  17. package/dist/assets/SceneManager-i9wVMa_V.js +104 -0
  18. package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-BHfKLzWe.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-BfVevCSo.js} +1 -1
  20. package/dist/assets/SpawnModal-29x5z53B.js +1 -0
  21. package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-MbIByjWD.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-DF0vYaM6.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-EGcKS1Pt.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-BLmejfqe.js} +1 -1
  25. package/dist/assets/{area-logos-CThPeqIM.js → area-logos-C4fgpLgO.js} +1 -1
  26. package/dist/assets/grok.ico +0 -0
  27. package/dist/assets/grok.png +0 -0
  28. package/dist/assets/{index-h3Cuh2Zn.js → index-B-2H7O_Y.js} +2 -2
  29. package/dist/assets/{index-DelocTaH.js → index-B9xpLAcH.js} +1 -1
  30. package/dist/assets/{index-BUN7MVf5.js → index-BZ4wZwRu.js} +3 -3
  31. package/dist/assets/index-BiiLWTXJ.js +1 -0
  32. package/dist/assets/{index-maZSt2YW.js → index-C6Kf4ewu.js} +5 -5
  33. package/dist/assets/{index-Cz-ypmob.js → index-C8CGfmXP.js} +1 -1
  34. package/dist/assets/index-DThK5_yS.js +5 -0
  35. package/dist/assets/index-DYgVP0G3.js +2 -0
  36. package/dist/assets/{index-DOzFBkSH.js → index-DhD3m2Qx.js} +1 -1
  37. package/dist/assets/main-B0UlZnfk.js +260 -0
  38. package/dist/assets/main-DeiAIdzy.css +1 -0
  39. package/dist/assets/{web-CfvEypdU.js → web-CXC8P25z.js} +1 -1
  40. package/dist/assets/{web-CNjTPilF.js → web-LGEDa25c.js} +1 -1
  41. package/dist/assets/{web-bcYaAwP6.js → web-NM4dUqWg.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/en/tools.json +4 -2
  44. package/dist/src/packages/server/claude/runner/process-lifecycle.js +53 -0
  45. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +125 -4
  46. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  47. package/dist/src/packages/server/claude/session-loader.js +280 -0
  48. package/dist/src/packages/server/data/index.js +1 -0
  49. package/dist/src/packages/server/grok/backend.js +231 -0
  50. package/dist/src/packages/server/grok/index.js +4 -0
  51. package/dist/src/packages/server/grok/json-event-parser.js +138 -0
  52. package/dist/src/packages/server/grok/session-watcher.js +460 -0
  53. package/dist/src/packages/server/routes/agents.js +7 -2
  54. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  55. package/dist/src/packages/server/runtime/index.js +1 -0
  56. package/dist/src/packages/server/services/agent-service.js +26 -5
  57. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  58. package/dist/src/packages/server/services/runtime-events.js +46 -7
  59. package/dist/src/packages/server/services/runtime-service.js +19 -3
  60. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  61. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  62. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +63 -7
  63. package/dist/src/packages/shared/agent-types.js +10 -0
  64. package/package.json +1 -1
  65. package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
  66. package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
  67. package/dist/assets/SceneManager-QqKygyaw.js +0 -104
  68. package/dist/assets/SpawnModal-BOeStihN.js +0 -1
  69. package/dist/assets/index-DP7kd91h.js +0 -1
  70. package/dist/assets/index-DRyMARFk.js +0 -5
  71. package/dist/assets/index-DuWmOZdY.js +0 -2
  72. package/dist/assets/main-C2zgPci-.js +0 -256
  73. package/dist/assets/main-C3JO2TRI.css +0 -1
@@ -52,6 +52,27 @@ const OPENCODE_TOOL_NAME_MAP = {
52
52
  function normalizeOpencodeToolName(raw) {
53
53
  return OPENCODE_TOOL_NAME_MAP[raw.toLowerCase()] || raw;
54
54
  }
55
+ // Grok CLI tool names → Tide UI names (keep in sync with grok/session-watcher.ts)
56
+ const GROK_TOOL_NAME_MAP = {
57
+ list_dir: 'ListFiles',
58
+ read_file: 'Read',
59
+ search_replace: 'Edit',
60
+ write: 'Write',
61
+ run_terminal_cmd: 'Bash',
62
+ run_terminal_command: 'Bash',
63
+ grep: 'Grep',
64
+ web_search: 'WebSearch',
65
+ web_fetch: 'WebFetch',
66
+ open_page: 'WebFetch',
67
+ open_page_with_find: 'WebFetch',
68
+ spawn_subagent: 'Task',
69
+ todo_write: 'TodoWrite',
70
+ };
71
+ function normalizeGrokToolName(raw) {
72
+ return GROK_TOOL_NAME_MAP[raw.toLowerCase()] || raw;
73
+ }
74
+ const GROK_DIR = path.join(os.homedir(), '.grok');
75
+ const GROK_SESSIONS_DIR = path.join(GROK_DIR, 'sessions');
55
76
  const codexSessionFileById = new Map();
56
77
  const opencodeSessionFileById = new Map();
57
78
  let cachedOpencodeDb = null;
@@ -452,6 +473,49 @@ function findOpencodeSessionFile(sessionId) {
452
473
  }
453
474
  return null;
454
475
  }
476
+ /**
477
+ * Locate a Grok session directory for (cwd, sessionId).
478
+ * Grok keys projects by encodeURIComponent(absolute cwd) under ~/.grok/sessions/.
479
+ */
480
+ function findGrokSessionDir(cwd, sessionId) {
481
+ if (!sessionId)
482
+ return null;
483
+ const candidates = new Set();
484
+ try {
485
+ candidates.add(path.resolve(cwd));
486
+ }
487
+ catch {
488
+ // ignore
489
+ }
490
+ candidates.add(cwd.replace(/\/+$/, ''));
491
+ candidates.add(cwd);
492
+ for (const c of candidates) {
493
+ if (!c)
494
+ continue;
495
+ const dir = path.join(GROK_SESSIONS_DIR, encodeURIComponent(c), sessionId);
496
+ const chatPath = path.join(dir, 'chat_history.jsonl');
497
+ if (fs.existsSync(chatPath)) {
498
+ return dir;
499
+ }
500
+ }
501
+ // Fallback: scan all project keys for this session id (slower, rare)
502
+ if (!fs.existsSync(GROK_SESSIONS_DIR))
503
+ return null;
504
+ try {
505
+ for (const entry of fs.readdirSync(GROK_SESSIONS_DIR, { withFileTypes: true })) {
506
+ if (!entry.isDirectory())
507
+ continue;
508
+ const dir = path.join(GROK_SESSIONS_DIR, entry.name, sessionId);
509
+ if (fs.existsSync(path.join(dir, 'chat_history.jsonl'))) {
510
+ return dir;
511
+ }
512
+ }
513
+ }
514
+ catch {
515
+ // ignore
516
+ }
517
+ return null;
518
+ }
455
519
  function resolveSessionFile(cwd, sessionId) {
456
520
  const claudeFile = path.join(getProjectDir(cwd), `${sessionId}.jsonl`);
457
521
  if (fs.existsSync(claudeFile)) {
@@ -473,6 +537,14 @@ function resolveSessionFile(cwd, sessionId) {
473
537
  if (opencodeFile && fs.existsSync(opencodeFile)) {
474
538
  return { provider: 'opencode', filePath: opencodeFile };
475
539
  }
540
+ const grokDir = findGrokSessionDir(cwd, sessionId);
541
+ if (grokDir) {
542
+ return {
543
+ provider: 'grok',
544
+ filePath: path.join(grokDir, 'chat_history.jsonl'),
545
+ grokSessionDir: grokDir,
546
+ };
547
+ }
476
548
  return null;
477
549
  }
478
550
  /**
@@ -1374,6 +1446,191 @@ async function parseOpencodeSessionMessages(sessionFilePath) {
1374
1446
  lastMessageTimestamp: last?.timestamp ? new Date(last.timestamp) : null,
1375
1447
  };
1376
1448
  }
1449
+ function extractGrokTextContent(content) {
1450
+ if (typeof content === 'string')
1451
+ return content;
1452
+ if (!Array.isArray(content))
1453
+ return '';
1454
+ const parts = [];
1455
+ for (const block of content) {
1456
+ if (!block || typeof block !== 'object')
1457
+ continue;
1458
+ const b = block;
1459
+ if (b.type === 'text' && typeof b.text === 'string') {
1460
+ parts.push(b.text);
1461
+ }
1462
+ }
1463
+ return parts.join('\n');
1464
+ }
1465
+ /** Prefer the Tide-injected <user_query> body when present. */
1466
+ function extractGrokUserDisplayText(content) {
1467
+ const raw = extractGrokTextContent(content).trim();
1468
+ if (!raw)
1469
+ return '';
1470
+ const queryMatch = raw.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
1471
+ if (queryMatch?.[1]) {
1472
+ return queryMatch[1].trim();
1473
+ }
1474
+ return raw;
1475
+ }
1476
+ function parseGrokToolArguments(args) {
1477
+ if (!args)
1478
+ return {};
1479
+ if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
1480
+ return args;
1481
+ }
1482
+ if (typeof args === 'string') {
1483
+ try {
1484
+ const parsed = JSON.parse(args);
1485
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1486
+ return parsed;
1487
+ }
1488
+ return { raw: args };
1489
+ }
1490
+ catch {
1491
+ return { raw: args };
1492
+ }
1493
+ }
1494
+ return {};
1495
+ }
1496
+ /**
1497
+ * Parse Grok chat_history.jsonl into Tide SessionMessages.
1498
+ * chat_history has no per-line timestamps — we synthesize monotonic ISO times
1499
+ * so the client sort order matches file order.
1500
+ */
1501
+ function parseGrokSessionMessages(chatHistoryPath) {
1502
+ const messages = [];
1503
+ if (!fs.existsSync(chatHistoryPath)) {
1504
+ return { messages: [], lastMessageType: null, lastMessageTimestamp: null };
1505
+ }
1506
+ // Base clock from file mtime so relative times feel recent; +1s per line for order.
1507
+ let baseMs = Date.now() - 3_600_000;
1508
+ try {
1509
+ baseMs = fs.statSync(chatHistoryPath).mtimeMs - 3_600_000;
1510
+ }
1511
+ catch {
1512
+ // ignore
1513
+ }
1514
+ const content = fs.readFileSync(chatHistoryPath, 'utf-8');
1515
+ const lines = content.split('\n');
1516
+ let lineIndex = 0;
1517
+ const toolUseIdToName = new Map();
1518
+ for (const line of lines) {
1519
+ if (!line.trim())
1520
+ continue;
1521
+ lineIndex += 1;
1522
+ let entry;
1523
+ try {
1524
+ entry = JSON.parse(line);
1525
+ }
1526
+ catch {
1527
+ continue;
1528
+ }
1529
+ const type = typeof entry.type === 'string' ? entry.type : '';
1530
+ const ts = new Date(baseMs + lineIndex * 1000).toISOString();
1531
+ // Skip system prompt and synthetic MCP/system-reminder injections
1532
+ if (type === 'system')
1533
+ continue;
1534
+ if (type === 'user' && entry.synthetic_reason === 'system_reminder')
1535
+ continue;
1536
+ if (type === 'user') {
1537
+ const text = extractGrokUserDisplayText(entry.content);
1538
+ if (!text)
1539
+ continue;
1540
+ // Skip pure scaffolding (user_info / system-reminder without user_query)
1541
+ if (text.startsWith('<user_info>') ||
1542
+ text.startsWith('<system-reminder>') ||
1543
+ (text.includes('<system-reminder>') && !text.includes('<user_query>'))) {
1544
+ continue;
1545
+ }
1546
+ messages.push({
1547
+ type: 'user',
1548
+ content: text,
1549
+ timestamp: ts,
1550
+ uuid: `grok-user-${lineIndex}`,
1551
+ });
1552
+ continue;
1553
+ }
1554
+ if (type === 'reasoning') {
1555
+ // Optional thinking summaries — surface as assistant thinking lines
1556
+ const summary = entry.summary;
1557
+ let thinking = '';
1558
+ if (Array.isArray(summary)) {
1559
+ thinking = summary
1560
+ .map((s) => {
1561
+ if (s && typeof s === 'object' && typeof s.text === 'string') {
1562
+ return s.text;
1563
+ }
1564
+ return '';
1565
+ })
1566
+ .filter(Boolean)
1567
+ .join('\n');
1568
+ }
1569
+ if (thinking) {
1570
+ messages.push({
1571
+ type: 'assistant',
1572
+ content: `[thinking] ${thinking}`,
1573
+ timestamp: ts,
1574
+ uuid: typeof entry.id === 'string' ? entry.id : `grok-reasoning-${lineIndex}`,
1575
+ });
1576
+ }
1577
+ continue;
1578
+ }
1579
+ if (type === 'assistant') {
1580
+ const text = typeof entry.content === 'string' ? entry.content : extractGrokTextContent(entry.content);
1581
+ if (text.trim()) {
1582
+ messages.push({
1583
+ type: 'assistant',
1584
+ content: text,
1585
+ timestamp: ts,
1586
+ uuid: `grok-assistant-${lineIndex}`,
1587
+ });
1588
+ }
1589
+ const toolCalls = entry.tool_calls;
1590
+ if (Array.isArray(toolCalls)) {
1591
+ for (let ti = 0; ti < toolCalls.length; ti++) {
1592
+ const call = toolCalls[ti];
1593
+ const callId = call.id || `grok-call-${lineIndex}-${ti}`;
1594
+ const toolName = normalizeGrokToolName(call.name || 'unknown');
1595
+ toolUseIdToName.set(callId, toolName);
1596
+ messages.push({
1597
+ type: 'tool_use',
1598
+ content: '',
1599
+ timestamp: ts,
1600
+ uuid: callId,
1601
+ toolName,
1602
+ toolInput: parseGrokToolArguments(call.arguments),
1603
+ toolUseId: callId,
1604
+ });
1605
+ }
1606
+ }
1607
+ continue;
1608
+ }
1609
+ if (type === 'tool_result') {
1610
+ const callId = typeof entry.tool_call_id === 'string' ? entry.tool_call_id : `grok-result-${lineIndex}`;
1611
+ const content = typeof entry.content === 'string'
1612
+ ? entry.content
1613
+ : entry.content != null
1614
+ ? JSON.stringify(entry.content)
1615
+ : '';
1616
+ messages.push({
1617
+ type: 'tool_result',
1618
+ content,
1619
+ timestamp: ts,
1620
+ uuid: `${callId}-result`,
1621
+ toolUseId: callId,
1622
+ toolName: toolUseIdToName.get(callId) || 'unknown',
1623
+ });
1624
+ }
1625
+ }
1626
+ const dedupedMessages = deduplicateSessionMessages(messages);
1627
+ const last = dedupedMessages.length > 0 ? dedupedMessages[dedupedMessages.length - 1] : null;
1628
+ return {
1629
+ messages: dedupedMessages,
1630
+ lastMessageType: last?.type ?? null,
1631
+ lastMessageTimestamp: last?.timestamp ? new Date(last.timestamp) : null,
1632
+ };
1633
+ }
1377
1634
  async function parseSessionMessages(resolved) {
1378
1635
  if (resolved.provider === 'opencode') {
1379
1636
  if (resolved.opencodeDbSessionId) {
@@ -1381,6 +1638,9 @@ async function parseSessionMessages(resolved) {
1381
1638
  }
1382
1639
  return parseOpencodeSessionMessages(resolved.filePath);
1383
1640
  }
1641
+ if (resolved.provider === 'grok') {
1642
+ return parseGrokSessionMessages(resolved.filePath);
1643
+ }
1384
1644
  const messages = [];
1385
1645
  const toolUseIdToName = new Map();
1386
1646
  const fileStream = fs.createReadStream(resolved.filePath);
@@ -1685,12 +1945,16 @@ const PROVIDER_PROCESS_PATTERNS = {
1685
1945
  claude: '(claude$|/claude( |$)|claude\\.cmd|claude\\.exe)',
1686
1946
  codex: '(codex($| )|/codex( |$)|codex\\.cmd|codex\\.exe|codex\\.js|@openai/codex)',
1687
1947
  opencode: '(opencode($| )|/opencode( |$)|opencode\\.cmd|opencode\\.exe)',
1948
+ // Match `grok` CLI but not unrelated tools with "grok" in the path/name as a substring of a longer token.
1949
+ grok: '(^|/)grok($| )|grok\\.cmd|grok\\.exe',
1688
1950
  };
1689
1951
  function providerDisplayName(provider) {
1690
1952
  if (provider === 'codex')
1691
1953
  return 'Codex';
1692
1954
  if (provider === 'opencode')
1693
1955
  return 'OpenCode';
1956
+ if (provider === 'grok')
1957
+ return 'Grok';
1694
1958
  return 'Claude';
1695
1959
  }
1696
1960
  function getProviderProcessPids(provider, execSync) {
@@ -1891,6 +2155,22 @@ export async function findOpencodeProcessPidInCwd(cwd) {
1891
2155
  export async function killOpencodeProcessInCwd(cwd) {
1892
2156
  return killProviderProcessInCwd(cwd, 'opencode');
1893
2157
  }
2158
+ /**
2159
+ * Check if there's a Grok process running in a specific directory.
2160
+ */
2161
+ export async function isGrokProcessRunningInCwd(cwd) {
2162
+ return isProviderProcessRunningInCwd(cwd, 'grok');
2163
+ }
2164
+ export async function findGrokProcessPidInCwd(cwd) {
2165
+ return findProviderProcessPidInCwd(cwd, 'grok');
2166
+ }
2167
+ /**
2168
+ * Kill any Grok process running in the specified directory.
2169
+ * Returns true if a process was found and killed.
2170
+ */
2171
+ export async function killGrokProcessInCwd(cwd) {
2172
+ return killProviderProcessInCwd(cwd, 'grok');
2173
+ }
1894
2174
  export async function loadToolHistory(cwd, sessionId, agentId, agentName, limit = 100) {
1895
2175
  const toolExecutions = [];
1896
2176
  const fileChanges = [];
@@ -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,231 @@
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
+ extractSessionId(rawEvent) {
145
+ const event = rawEvent;
146
+ if (event?.sessionId && typeof event.sessionId === 'string') {
147
+ return event.sessionId;
148
+ }
149
+ // end event also carries sessionId
150
+ if (event?.type === 'end' && typeof event.sessionId === 'string') {
151
+ return event.sessionId;
152
+ }
153
+ return null;
154
+ }
155
+ getExecutablePath() {
156
+ const envBinary = process.env.GROK_BINARY;
157
+ if (envBinary && fs.existsSync(envBinary)) {
158
+ return envBinary;
159
+ }
160
+ return this.detectInstallation() || 'grok';
161
+ }
162
+ detectInstallation() {
163
+ const homeDir = os.homedir();
164
+ const isWindows = process.platform === 'win32';
165
+ const possiblePaths = isWindows
166
+ ? [
167
+ path.join(homeDir, '.grok', 'bin', 'grok.exe'),
168
+ path.join(homeDir, 'AppData', 'Roaming', 'npm', 'grok.cmd'),
169
+ ]
170
+ : [
171
+ path.join(homeDir, '.grok', 'bin', 'grok'),
172
+ path.join(homeDir, '.local', 'bin', 'grok'),
173
+ '/usr/local/bin/grok',
174
+ '/usr/bin/grok',
175
+ ];
176
+ for (const p of possiblePaths) {
177
+ if (fs.existsSync(p))
178
+ return p;
179
+ }
180
+ try {
181
+ const result = execSync('which grok', { encoding: 'utf-8', timeout: 5000 }).trim();
182
+ return result || null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ getExtraEnv() {
189
+ // Ensure ~/.grok/bin is on PATH if present (managed installs live there)
190
+ const grokBin = path.join(os.homedir(), '.grok', 'bin');
191
+ if (fs.existsSync(grokBin)) {
192
+ const sep = process.platform === 'win32' ? ';' : ':';
193
+ return { PATH: grokBin + sep + (process.env.PATH || '') };
194
+ }
195
+ return {};
196
+ }
197
+ /**
198
+ * Prompt is passed via -p / --prompt-file, not stdin.
199
+ */
200
+ requiresStdinInput() {
201
+ return false;
202
+ }
203
+ /**
204
+ * Mark as stdin-closed so follow-up messages queue and respawn with --resume
205
+ * (same delivery path as Codex/OpenCode).
206
+ */
207
+ shouldCloseStdinAfterPrompt() {
208
+ return true;
209
+ }
210
+ supportsSessionResume() {
211
+ return true;
212
+ }
213
+ formatStdinInput(_prompt) {
214
+ // Not used (requiresStdinInput=false). Clean up any leftover prompt file.
215
+ this.cleanupPromptFile();
216
+ return '';
217
+ }
218
+ cleanupPromptFile() {
219
+ if (!this.lastPromptFile)
220
+ return;
221
+ try {
222
+ if (fs.existsSync(this.lastPromptFile)) {
223
+ fs.unlinkSync(this.lastPromptFile);
224
+ }
225
+ }
226
+ catch {
227
+ // best-effort
228
+ }
229
+ this.lastPromptFile = undefined;
230
+ }
231
+ }
@@ -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';