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.
- package/CHANGELOG.md +17 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-Cs4kWugD.js} +1 -1
- package/dist/assets/BossSpawnModal-BudMgJqj.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-Bh6_3qtH.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-Nf4wQFfz.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-BfRXRX50.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-CXUtjA0r.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D-_rwicD.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-CdaXsQK0.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-ZsSVxcWr.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-BAf-klB2.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-BjRbh5z4.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-SaywDZVL.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-jru2Ez9a.js} +1 -1
- package/dist/assets/Scene2DCanvas-NJHoe2Qg.js +1 -0
- package/dist/assets/SceneManager-i9wVMa_V.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-BHfKLzWe.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-BfVevCSo.js} +1 -1
- package/dist/assets/SpawnModal-29x5z53B.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-MbIByjWD.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-DF0vYaM6.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-EGcKS1Pt.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-BLmejfqe.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-C4fgpLgO.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-B-2H7O_Y.js} +2 -2
- package/dist/assets/{index-DelocTaH.js → index-B9xpLAcH.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-BZ4wZwRu.js} +3 -3
- package/dist/assets/index-BiiLWTXJ.js +1 -0
- package/dist/assets/{index-maZSt2YW.js → index-C6Kf4ewu.js} +5 -5
- package/dist/assets/{index-Cz-ypmob.js → index-C8CGfmXP.js} +1 -1
- package/dist/assets/index-DThK5_yS.js +5 -0
- package/dist/assets/index-DYgVP0G3.js +2 -0
- package/dist/assets/{index-DOzFBkSH.js → index-DhD3m2Qx.js} +1 -1
- package/dist/assets/main-B0UlZnfk.js +260 -0
- package/dist/assets/main-DeiAIdzy.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-CXC8P25z.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-LGEDa25c.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.js → web-NM4dUqWg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/tools.json +4 -2
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +53 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +125 -4
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +280 -0
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +231 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +138 -0
- package/dist/src/packages/server/grok/session-watcher.js +460 -0
- package/dist/src/packages/server/routes/agents.js +7 -2
- 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/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 +63 -7
- 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-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
|
@@ -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';
|