tide-commander 1.147.1 → 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 (88) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -3
  3. package/dist/assets/BossLogsModal-Cs4kWugD.js +1 -0
  4. package/dist/assets/BossSpawnModal-BudMgJqj.js +1 -0
  5. package/dist/assets/ControlsModal-Bh6_3qtH.js +1 -0
  6. package/dist/assets/DockerLogsModal-Nf4wQFfz.js +2 -0
  7. package/dist/assets/{EmbeddedEditor-DfM3SrO7.js → EmbeddedEditor-BfRXRX50.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-DBWs7uzU.js → GmailOAuthSetup-CXUtjA0r.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-yQjT5QzU.js → GoogleOAuthSetup-D-_rwicD.js} +1 -1
  10. package/dist/assets/{IframeModal-DUTIyP5I.js → IframeModal-CdaXsQK0.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-DPURuiaz.js → IntegrationsPanel-ZsSVxcWr.js} +2 -2
  12. package/dist/assets/{LogViewerModal-CDZvTdfO.js → LogViewerModal-BAf-klB2.js} +1 -1
  13. package/dist/assets/{MonitoringModal-Dq2EJgbH.js → MonitoringModal-BjRbh5z4.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-C3dlYlGv.js → PM2LogsModal-SaywDZVL.js} +2 -2
  15. package/dist/assets/{RestoreArchivedAreaModal-BMFk0G8D.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-D326VBDa.js → SkillsPanel-BHfKLzWe.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CA4nm925.js → SlackMultiInstanceSetup-BfVevCSo.js} +1 -1
  20. package/dist/assets/SpawnModal-29x5z53B.js +1 -0
  21. package/dist/assets/{StatisticsModal-BUpvkJXA.js → StatisticsModal-MbIByjWD.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-DtVsKDqV.js → SubordinateAssignmentModal-DF0vYaM6.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-OD14xVMw.js → TriggerManagerPanel-EGcKS1Pt.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-B4UF3rcS.js → WorkflowEditorPanel-BLmejfqe.js} +1 -1
  25. package/dist/assets/{area-logos-Uc9SAeYg.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-xBtKu0I8.js → index-B-2H7O_Y.js} +2 -2
  29. package/dist/assets/{index-CuHJm43U.js → index-B9xpLAcH.js} +1 -1
  30. package/dist/assets/index-BZ4wZwRu.js +19 -0
  31. package/dist/assets/index-BiiLWTXJ.js +1 -0
  32. package/dist/assets/{index-BC3Y_BAQ.js → index-C6Kf4ewu.js} +5 -5
  33. package/dist/assets/index-C8CGfmXP.js +1 -0
  34. package/dist/assets/index-DThK5_yS.js +5 -0
  35. package/dist/assets/index-DYgVP0G3.js +2 -0
  36. package/dist/assets/index-DhD3m2Qx.js +1 -0
  37. package/dist/assets/main-B0UlZnfk.js +260 -0
  38. package/dist/assets/main-DeiAIdzy.css +1 -0
  39. package/dist/assets/{web-DlzqHyn8.js → web-CXC8P25z.js} +1 -1
  40. package/dist/assets/{web-B-xc0c6e.js → web-LGEDa25c.js} +1 -1
  41. package/dist/assets/{web-BuDeKWBN.js → web-NM4dUqWg.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/de/tools.json +3 -1
  44. package/dist/locales/en/tools.json +7 -3
  45. package/dist/locales/es/tools.json +3 -1
  46. package/dist/locales/fr/tools.json +4 -2
  47. package/dist/locales/hi/tools.json +4 -2
  48. package/dist/locales/it/tools.json +19 -17
  49. package/dist/locales/ja/tools.json +4 -2
  50. package/dist/locales/pt/tools.json +3 -1
  51. package/dist/locales/ru/tools.json +3 -1
  52. package/dist/locales/zh-CN/tools.json +3 -1
  53. package/dist/src/packages/server/claude/runner/process-lifecycle.js +53 -0
  54. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +125 -4
  55. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  56. package/dist/src/packages/server/claude/session-loader.js +280 -0
  57. package/dist/src/packages/server/data/index.js +1 -0
  58. package/dist/src/packages/server/grok/backend.js +231 -0
  59. package/dist/src/packages/server/grok/index.js +4 -0
  60. package/dist/src/packages/server/grok/json-event-parser.js +138 -0
  61. package/dist/src/packages/server/grok/session-watcher.js +460 -0
  62. package/dist/src/packages/server/routes/agents.js +7 -2
  63. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  64. package/dist/src/packages/server/runtime/index.js +1 -0
  65. package/dist/src/packages/server/services/agent-service.js +26 -5
  66. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  67. package/dist/src/packages/server/services/runtime-events.js +46 -7
  68. package/dist/src/packages/server/services/runtime-service.js +19 -3
  69. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  70. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  71. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +63 -7
  72. package/dist/src/packages/shared/agent-types.js +10 -0
  73. package/package.json +1 -1
  74. package/dist/assets/BossLogsModal-Cn8IVdqn.js +0 -1
  75. package/dist/assets/BossSpawnModal-Don-RZyh.js +0 -1
  76. package/dist/assets/ControlsModal-C9--xWPi.js +0 -1
  77. package/dist/assets/DockerLogsModal-DSGnpfm5.js +0 -2
  78. package/dist/assets/Scene2DCanvas-DlumJCbl.js +0 -1
  79. package/dist/assets/SceneManager-UnhMFja8.js +0 -104
  80. package/dist/assets/SpawnModal-B1QOjZPe.js +0 -1
  81. package/dist/assets/index-BD6swoQU.js +0 -19
  82. package/dist/assets/index-C4mf6B5U.js +0 -1
  83. package/dist/assets/index-COHImd74.js +0 -1
  84. package/dist/assets/index-CScFEdm8.js +0 -1
  85. package/dist/assets/index-CmmpVZ8w.js +0 -2
  86. package/dist/assets/index-DkDzXtdj.js +0 -5
  87. package/dist/assets/main-Bp6V_-sL.js +0 -256
  88. package/dist/assets/main-ijcbyjBn.css +0 -1
@@ -5,7 +5,7 @@
5
5
  import * as fs from 'fs';
6
6
  import * as os from 'os';
7
7
  import * as path from 'path';
8
- import { CLAUDE_MODELS as CLAUDE_MODEL_METADATA } from '../../shared/agent-types.js';
8
+ import { CLAUDE_MODELS as CLAUDE_MODEL_METADATA, GROK_MODELS, DEFAULT_GROK_MODEL } from '../../shared/agent-types.js';
9
9
  import { loadAgents, saveAgents, saveAgentsAsync, getDataDir, loadAreas, saveAreas, loadSessionHistory, saveSessionHistory, addSessionHistoryEntry, getSessionHistoryForAgent } from '../data/index.js';
10
10
  import { listSessions, getSessionSummary, getProjectDir, loadSession, loadToolHistory, searchSession, } from '../claude/session-loader.js';
11
11
  import { loadSubagentHistory } from '../claude/subagent-history-loader.js';
@@ -16,11 +16,19 @@ const VALID_CLAUDE_MODELS = new Set(Object.keys(CLAUDE_MODEL_METADATA));
16
16
  const DEFAULT_CLAUDE_CONTEXT_LIMIT = 200000;
17
17
  const DEFAULT_CODEX_CONTEXT_LIMIT = 258400;
18
18
  const DEFAULT_OPENCODE_CONTEXT_LIMIT = 200000;
19
- function getDefaultContextLimit(provider, model) {
19
+ /** Matches live Grok Build signals.json contextWindowTokens (catalog may list 2M). */
20
+ const DEFAULT_GROK_CONTEXT_LIMIT = 500000;
21
+ function getDefaultContextLimit(provider, model, grokModel) {
20
22
  if (provider === 'codex')
21
23
  return DEFAULT_CODEX_CONTEXT_LIMIT;
22
24
  if (provider === 'opencode')
23
25
  return DEFAULT_OPENCODE_CONTEXT_LIMIT;
26
+ if (provider === 'grok') {
27
+ if (grokModel && GROK_MODELS[grokModel]?.contextWindow) {
28
+ return GROK_MODELS[grokModel].contextWindow;
29
+ }
30
+ return DEFAULT_GROK_CONTEXT_LIMIT;
31
+ }
24
32
  if (model && CLAUDE_MODEL_METADATA[model]) {
25
33
  return CLAUDE_MODEL_METADATA[model].contextWindow;
26
34
  }
@@ -61,6 +69,12 @@ export function sanitizeOpencodeModel(model) {
61
69
  const trimmed = model.trim();
62
70
  return trimmed.length > 0 ? trimmed : undefined;
63
71
  }
72
+ export function sanitizeGrokModel(model) {
73
+ if (typeof model !== 'string')
74
+ return undefined;
75
+ const trimmed = model.trim();
76
+ return trimmed.length > 0 ? trimmed : undefined;
77
+ }
64
78
  function findFileRecursively(rootDir, pattern) {
65
79
  if (!fs.existsSync(rootDir))
66
80
  return null;
@@ -215,6 +229,7 @@ export function initAgents() {
215
229
  codexModel: sanitizeCodexModel(stored.codexModel),
216
230
  codexConfig: stored.codexConfig,
217
231
  opencodeModel: sanitizeOpencodeModel(stored.opencodeModel),
232
+ grokModel: sanitizeGrokModel(stored.grokModel),
218
233
  // Boss field - fallback to checking class for backward compatibility
219
234
  isBoss: stored.isBoss ?? stored.class === 'boss',
220
235
  };
@@ -278,7 +293,7 @@ export function getAgent(id) {
278
293
  export function getAllAgents() {
279
294
  return Array.from(agents.values());
280
295
  }
281
- export async function createAgent(name, agentClass, cwd, position, sessionId, useChrome, permissionMode = 'bypass', initialSkillIds, isBoss, model, codexModel, customInstructions, provider = 'claude', codexConfig, effort, opencodeModel) {
296
+ export async function createAgent(name, agentClass, cwd, position, sessionId, useChrome, permissionMode = 'bypass', initialSkillIds, isBoss, model, codexModel, customInstructions, provider = 'claude', codexConfig, effort, opencodeModel, grokModel) {
282
297
  log.log('🎆 [CREATE_AGENT] Starting agent creation:', {
283
298
  name,
284
299
  agentClass,
@@ -290,6 +305,8 @@ export async function createAgent(name, agentClass, cwd, position, sessionId, us
290
305
  model,
291
306
  codexModel,
292
307
  codexConfig,
308
+ opencodeModel,
309
+ grokModel,
293
310
  customInstructions: customInstructions ? `${customInstructions.length} chars` : undefined,
294
311
  });
295
312
  const id = generateId();
@@ -301,6 +318,9 @@ export async function createAgent(name, agentClass, cwd, position, sessionId, us
301
318
  throw new Error(`Directory does not exist: ${cwd}`);
302
319
  }
303
320
  log.log(` ✅ Directory exists`);
321
+ const sanitizedGrokModel = provider === 'grok'
322
+ ? (sanitizeGrokModel(grokModel) || DEFAULT_GROK_MODEL)
323
+ : undefined;
304
324
  // Create agent object
305
325
  // SessionId can be provided to link to an existing Claude session
306
326
  const agent = {
@@ -318,13 +338,14 @@ export async function createAgent(name, agentClass, cwd, position, sessionId, us
318
338
  useChrome,
319
339
  permissionMode,
320
340
  model: sanitizeModelForProvider(provider, model),
321
- effort: provider === 'claude' ? effort : undefined,
341
+ effort: provider === 'claude' || provider === 'grok' ? effort : undefined,
322
342
  codexModel: provider === 'codex' ? sanitizeCodexModel(codexModel) : undefined,
323
343
  codexConfig,
324
344
  opencodeModel: provider === 'opencode' ? sanitizeOpencodeModel(opencodeModel) : undefined,
345
+ grokModel: sanitizedGrokModel,
325
346
  tokensUsed: 0,
326
347
  contextUsed: 0,
327
- contextLimit: getDefaultContextLimit(provider, sanitizeModelForProvider(provider, model)),
348
+ contextLimit: getDefaultContextLimit(provider, sanitizeModelForProvider(provider, model), sanitizedGrokModel),
328
349
  taskCount: 0, // Initialize task counter
329
350
  createdAt: Date.now(),
330
351
  lastActivity: Date.now(),
@@ -57,8 +57,10 @@ export function createRuntimeCommandExecution(deps) {
57
57
  ? agentService.sanitizeModelForProvider(agent.provider, agent.model)
58
58
  : agent.provider === 'opencode'
59
59
  ? agentService.sanitizeOpencodeModel(agent.opencodeModel)
60
- : agentService.sanitizeCodexModel(agent.codexModel),
61
- effort: agent.provider === 'claude' ? agent.effort : undefined,
60
+ : agent.provider === 'grok'
61
+ ? agentService.sanitizeGrokModel(agent.grokModel)
62
+ : agentService.sanitizeCodexModel(agent.codexModel),
63
+ effort: agent.provider === 'claude' || agent.provider === 'grok' ? agent.effort : undefined,
62
64
  useChrome: agent.useChrome,
63
65
  permissionMode: agent.permissionMode,
64
66
  codexConfig: agent.codexConfig,
@@ -4,6 +4,8 @@ import { clearPendingSilentContextRefresh, consumeStepCompleteReceived, markStep
4
4
  import { addPendingBackgroundTask, clearPendingBackgroundTasks, getActiveSubagentByToolUseId, handleTaskToolResult, handleTaskToolStart, hasPendingBackgroundTasks, resolvePendingBackgroundTask, resolvePendingBackgroundTaskByTaskId, } from './runtime-subagents.js';
5
5
  const DEFAULT_CLAUDE_CONTEXT_WINDOW = 200000;
6
6
  const DEFAULT_CODEX_CONTEXT_WINDOW = 258400;
7
+ /** Practical Grok Build window observed in session signals.json (model catalog may list 2M). */
8
+ const DEFAULT_GROK_CONTEXT_WINDOW = 500000;
7
9
  const CODEX_ROLLING_CONTEXT_TURNS = 40;
8
10
  const CODEX_PLAUSIBLE_USAGE_MULTIPLIER = 1.2;
9
11
  const CODEX_RECOVERABLE_RESUME_ERRORS = [
@@ -41,6 +43,8 @@ function getDefaultContextWindow(provider) {
41
43
  return DEFAULT_CODEX_CONTEXT_WINDOW;
42
44
  if (provider === 'opencode')
43
45
  return DEFAULT_CLAUDE_CONTEXT_WINDOW;
46
+ if (provider === 'grok')
47
+ return DEFAULT_GROK_CONTEXT_WINDOW;
44
48
  return DEFAULT_CLAUDE_CONTEXT_WINDOW;
45
49
  }
46
50
  function updateCodexRollingContextEstimate(agentId, turnGrowth) {
@@ -232,21 +236,28 @@ export function createRuntimeEventHandlers(deps) {
232
236
  // Real-time context tracking from streaming usage data.
233
237
  // Context window usage = input tokens only (output tokens don't count toward the limit).
234
238
  // total_input = cache_read + cache_creation + input_tokens (the full prompt size).
239
+ // Grok: session-watcher maps signals.json contextTokensUsed → tokens.input.
235
240
  if (event.tokens) {
236
241
  const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
237
242
  const isOpencodeProvider = (agent.provider ?? 'claude') === 'opencode';
238
- if (isClaudeProvider || isOpencodeProvider) {
243
+ const isGrokProvider = (agent.provider ?? 'claude') === 'grok';
244
+ if (isClaudeProvider || isOpencodeProvider || isGrokProvider) {
239
245
  const cacheRead = event.tokens.cacheRead || 0;
240
246
  const cacheCreation = event.tokens.cacheCreation || 0;
241
247
  const inputTokens = event.tokens.input || 0;
242
248
  // Context window = input side only (system prompt + tools + messages).
243
249
  // Output tokens are the model's response and don't count toward the limit.
250
+ // For Grok, inputTokens is already the full context fill from signals.json.
244
251
  const snapshotContextUsed = cacheRead + cacheCreation + inputTokens;
245
252
  if (snapshotContextUsed > 0) {
246
- const effectiveLimit = agent.contextLimit || getDefaultContextWindow(agent.provider);
253
+ // Grok signals also carry the authoritative window size.
254
+ const modelWindow = event.modelUsage?.contextWindow;
255
+ const effectiveLimit = Math.max(1, modelWindow || agent.contextLimit || getDefaultContextWindow(agent.provider));
247
256
  // Guard against cumulative session totals: if the sum exceeds the
248
257
  // context window, it can't represent per-request context fill.
249
- if (snapshotContextUsed > effectiveLimit) {
258
+ // Grok signals can report used slightly over window during compaction —
259
+ // clamp instead of skip so the bar still moves.
260
+ if (snapshotContextUsed > effectiveLimit && !isGrokProvider) {
250
261
  log.log(`[usage_snapshot] ${agentId}: sum ${snapshotContextUsed} exceeds limit ${effectiveLimit} (likely cumulative); skipping`);
251
262
  // If the agent's existing contextUsed is also stale (exceeds limit),
252
263
  // reset it to 0 so the UI doesn't keep showing an impossible value.
@@ -256,9 +267,10 @@ export function createRuntimeEventHandlers(deps) {
256
267
  }
257
268
  }
258
269
  else {
259
- const safeContextUsed = Math.max(0, snapshotContextUsed);
270
+ const safeContextUsed = Math.max(0, Math.min(snapshotContextUsed, effectiveLimit));
260
271
  const updates = {
261
272
  contextUsed: safeContextUsed,
273
+ contextLimit: effectiveLimit,
262
274
  };
263
275
  // Always keep contextStats.totalTokens in sync with contextUsed.
264
276
  // If authoritative stats exist (from /context), merge the new total
@@ -267,10 +279,23 @@ export function createRuntimeEventHandlers(deps) {
267
279
  updates.contextStats = updateContextStatsTokens(agent.contextStats, safeContextUsed, effectiveLimit);
268
280
  }
269
281
  else {
270
- updates.contextStats = buildEstimatedContextStats(safeContextUsed, effectiveLimit, isClaudeProvider ? (agent.model || 'claude') : (agent.opencodeModel || 'opencode'));
282
+ const modelLabel = isClaudeProvider
283
+ ? (agent.model || 'claude')
284
+ : isOpencodeProvider
285
+ ? (agent.opencodeModel || 'opencode')
286
+ : (agent.grokModel || 'grok');
287
+ updates.contextStats = buildEstimatedContextStats(safeContextUsed, effectiveLimit, modelLabel);
288
+ }
289
+ // Lifetime tokensUsed: for Grok we don't get per-turn usage, so
290
+ // keep the high-water mark of context fill as a lower bound on work done.
291
+ if (isGrokProvider) {
292
+ const prev = agent.tokensUsed || 0;
293
+ if (safeContextUsed > prev) {
294
+ updates.tokensUsed = safeContextUsed;
295
+ }
271
296
  }
272
297
  agentService.updateAgent(agentId, updates, false);
273
- log.log(`[usage_snapshot] ${agentId}: input=${inputTokens} + cacheRead=${cacheRead} + cacheCreation=${cacheCreation} = ${snapshotContextUsed} (output=${event.tokens.output || 0}) limit=${effectiveLimit}`);
298
+ log.log(`[usage_snapshot] ${agentId}: input=${inputTokens} + cacheRead=${cacheRead} + cacheCreation=${cacheCreation} = ${snapshotContextUsed} (output=${event.tokens.output || 0}) limit=${effectiveLimit}${isGrokProvider ? ' [grok]' : ''}`);
274
299
  }
275
300
  }
276
301
  }
@@ -295,6 +320,7 @@ export function createRuntimeEventHandlers(deps) {
295
320
  const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
296
321
  const isCodexProvider = (agent.provider ?? 'claude') === 'codex';
297
322
  const isOpencodeProvider = (agent.provider ?? 'claude') === 'opencode';
323
+ const isGrokProviderStep = (agent.provider ?? 'claude') === 'grok';
298
324
  const lastTask = agent.lastAssignedTask?.trim() || '';
299
325
  const isContextCommand = lastTask === '/context' || lastTask === '/cost' || lastTask === '/compact';
300
326
  let contextUsed = agent.contextUsed || 0;
@@ -326,6 +352,16 @@ export function createRuntimeEventHandlers(deps) {
326
352
  contextLimit = agent.contextLimit || getDefaultContextWindow('opencode');
327
353
  log.log(`[step_complete] OpenCode agent ${agentId}: preserving usage_snapshot contextUsed=${contextUsed}, contextLimit=${contextLimit}`);
328
354
  }
355
+ else if (isGrokProviderStep) {
356
+ // Grok: streaming-json has no usage; session-watcher feeds usage_snapshot
357
+ // from signals.json. Preserve those values on step_complete.
358
+ contextUsed = agent.contextUsed || 0;
359
+ contextLimit = agent.contextLimit || getDefaultContextWindow('grok');
360
+ if (hasModelUsageData && event.modelUsage?.contextWindow) {
361
+ contextLimit = event.modelUsage.contextWindow;
362
+ }
363
+ log.log(`[step_complete] Grok agent ${agentId}: preserving signals usage_snapshot contextUsed=${contextUsed}, contextLimit=${contextLimit}`);
364
+ }
329
365
  else if (hasModelUsageData && event.modelUsage) {
330
366
  const inputTokens = event.modelUsage.inputTokens || 0;
331
367
  const outputTokens = event.modelUsage.outputTokens || 0;
@@ -389,13 +425,16 @@ export function createRuntimeEventHandlers(deps) {
389
425
  // Always keep contextStats in sync. If authoritative stats exist (from
390
426
  // /context), merge the updated totalTokens while preserving category
391
427
  // breakdowns. Otherwise build fresh estimated stats.
428
+ const isGrokProvider = (agent.provider ?? 'claude') === 'grok';
392
429
  const modelForStats = isClaudeProvider
393
430
  ? (agent.model || 'claude')
394
431
  : isCodexProvider
395
432
  ? (agent.codexModel || agent.model)
396
433
  : isOpencodeProvider
397
434
  ? (agent.opencodeModel || 'opencode')
398
- : (agent.model || 'unknown');
435
+ : isGrokProvider
436
+ ? (agent.grokModel || 'grok')
437
+ : (agent.model || 'unknown');
399
438
  if (agent.contextStats && agent.contextStats.lastUpdated) {
400
439
  updates.contextStats = updateContextStatsTokens(agent.contextStats, Math.max(0, Math.round(contextUsed)), Math.max(1, Math.round(contextLimit)));
401
440
  }
@@ -2,11 +2,11 @@
2
2
  * Runtime Service
3
3
  * Manages Claude/Codex runtime runners and command execution
4
4
  */
5
- import { isClaudeProcessRunningInCwd, isCodexProcessRunningInCwd, isOpencodeProcessRunningInCwd, killClaudeProcessInCwd, killCodexProcessInCwd, killOpencodeProcessInCwd, findClaudeProcessPidInCwd, findCodexProcessPidInCwd, findOpencodeProcessPidInCwd, } from '../claude/session-loader.js';
5
+ import { isClaudeProcessRunningInCwd, isCodexProcessRunningInCwd, isOpencodeProcessRunningInCwd, isGrokProcessRunningInCwd, killClaudeProcessInCwd, killCodexProcessInCwd, killOpencodeProcessInCwd, killGrokProcessInCwd, findClaudeProcessPidInCwd, findCodexProcessPidInCwd, findOpencodeProcessPidInCwd, findGrokProcessPidInCwd, } from '../claude/session-loader.js';
6
6
  import * as agentService from './agent-service.js';
7
7
  import { loadRunningProcesses, isProcessRunning } from '../data/index.js';
8
8
  import { logger } from '../utils/logger.js';
9
- import { createClaudeRuntimeProvider, createCodexRuntimeProvider, createOpencodeRuntimeProvider, } from '../runtime/index.js';
9
+ import { createClaudeRuntimeProvider, createCodexRuntimeProvider, createOpencodeRuntimeProvider, createGrokRuntimeProvider, } from '../runtime/index.js';
10
10
  import { createRuntimeCommandExecution, } from './runtime-command-execution.js';
11
11
  import { createRuntimeEventHandlers } from './runtime-events.js';
12
12
  import { createRuntimeStatusSync } from './runtime-status-sync.js';
@@ -18,6 +18,7 @@ const runtimeProviders = {
18
18
  claude: createClaudeRuntimeProvider(),
19
19
  codex: createCodexRuntimeProvider(),
20
20
  opencode: createOpencodeRuntimeProvider(),
21
+ grok: createGrokRuntimeProvider(),
21
22
  };
22
23
  const runners = new Map();
23
24
  function getRunner(provider) {
@@ -55,6 +56,9 @@ async function isProviderProcessRunningInCwd(provider, cwd) {
55
56
  if (provider === 'opencode') {
56
57
  return isOpencodeProcessRunningInCwd(cwd);
57
58
  }
59
+ if (provider === 'grok') {
60
+ return isGrokProcessRunningInCwd(cwd);
61
+ }
58
62
  return isClaudeProcessRunningInCwd(cwd);
59
63
  }
60
64
  async function killDetachedProviderProcessInCwd(provider, cwd) {
@@ -64,6 +68,9 @@ async function killDetachedProviderProcessInCwd(provider, cwd) {
64
68
  if (provider === 'opencode') {
65
69
  return killOpencodeProcessInCwd(cwd);
66
70
  }
71
+ if (provider === 'grok') {
72
+ return killGrokProcessInCwd(cwd);
73
+ }
67
74
  return killClaudeProcessInCwd(cwd);
68
75
  }
69
76
  export function on(event, listener) {
@@ -147,6 +154,13 @@ export function init() {
147
154
  onComplete: runtimeEvents.handleComplete,
148
155
  onError: runtimeEvents.handleError,
149
156
  }));
157
+ runners.set('grok', runtimeProviders.grok.createRunner({
158
+ onEvent: runtimeEvents.handleEvent,
159
+ onOutput: runtimeEvents.handleOutput,
160
+ onSessionId: runtimeEvents.handleSessionId,
161
+ onComplete: runtimeEvents.handleComplete,
162
+ onError: runtimeEvents.handleError,
163
+ }));
150
164
  // Start each runner's background work (orphan recovery, persist timer,
151
165
  // watchdog). The constructor is now side-effect-free so non-canonical
152
166
  // contexts (tests, scripts, sidecars) that import the runner code don't
@@ -280,7 +294,9 @@ export async function getAgentRuntimeProcessInfo(agentId) {
280
294
  ? await findCodexProcessPidInCwd(agent.cwd)
281
295
  : provider === 'opencode'
282
296
  ? await findOpencodeProcessPidInCwd(agent.cwd)
283
- : await findClaudeProcessPidInCwd(agent.cwd);
297
+ : provider === 'grok'
298
+ ? await findGrokProcessPidInCwd(agent.cwd)
299
+ : await findClaudeProcessPidInCwd(agent.cwd);
284
300
  if (discoveredPid && isProcessRunning(discoveredPid)) {
285
301
  return {
286
302
  pid: discoveredPid,
@@ -28,32 +28,60 @@ export function getShortPath(filePath, maxLen = 40) {
28
28
  */
29
29
  export function getToolKeyParam(toolName, input) {
30
30
  switch (toolName) {
31
- case 'WebSearch':
32
- return truncate(input.query, 50);
33
- case 'WebFetch':
34
- return truncate(input.url, 60);
31
+ case 'WebSearch': {
32
+ const query = (input.query || input.q || input.search);
33
+ return query ? `"${truncate(query, 40)}"` : null;
34
+ }
35
+ case 'WebFetch': {
36
+ const url = (input.url || input.target_url || input.targetUrl);
37
+ return url ? truncate(url, 60) : null;
38
+ }
35
39
  case 'Read':
36
40
  case 'Write':
37
- case 'Edit':
38
- const filePath = (input.file_path || input.path);
41
+ case 'Edit': {
42
+ // Claude: file_path; Grok: target_file
43
+ const filePath = (input.file_path
44
+ || input.filePath
45
+ || input.target_file
46
+ || input.targetFile
47
+ || input.path);
39
48
  return getShortPath(filePath);
49
+ }
50
+ case 'ListFiles':
51
+ case 'list_dir': {
52
+ const dir = (input.target_directory
53
+ || input.targetDirectory
54
+ || input.path
55
+ || input.directory);
56
+ return getShortPath(dir);
57
+ }
40
58
  case 'Bash':
41
- const cmd = input.command;
42
- return cmd ? truncate(cmd, 60) : null;
59
+ case 'ExecuteCommand': {
60
+ const cmd = (input.command || input.cmd);
61
+ if (cmd)
62
+ return truncate(cmd, 60);
63
+ const desc = input.description;
64
+ return desc ? truncate(desc, 60) : null;
65
+ }
43
66
  case 'Grep':
44
- return input.pattern ? `"${truncate(input.pattern, 40)}"` : null;
67
+ case 'SearchFiles': {
68
+ const pattern = (input.pattern || input.query);
69
+ return pattern ? `"${truncate(pattern, 40)}"` : null;
70
+ }
45
71
  case 'Glob':
46
- return truncate(input.pattern, 50);
72
+ return truncate((input.pattern || input.glob), 50);
47
73
  case 'Task':
74
+ case 'Agent':
48
75
  return truncate(input.description, 50);
49
- case 'TodoWrite':
76
+ case 'TodoWrite': {
50
77
  const todos = input.todos;
51
78
  if (todos?.length) {
52
79
  return `${todos.length} item${todos.length > 1 ? 's' : ''}`;
53
80
  }
54
81
  return null;
82
+ }
55
83
  case 'NotebookEdit':
56
- return getFileName(input.notebook_path);
84
+ return getFileName((input.notebook_path || input.notebookPath || input.target_file || input.file_path));
57
85
  case 'AskUserQuestion':
58
86
  return 'clarification';
59
87
  default:
@@ -9,6 +9,7 @@ import { agentService, runtimeService, skillService, customClassService, bossSer
9
9
  import { markInstructionsDirty } from '../../services/instruction-refresh.js';
10
10
  import { createLogger } from '../../utils/index.js';
11
11
  import { ClaudeBackend, parseContextOutput } from '../../claude/backend.js';
12
+ import { getGrokSessionDir, readGrokSignalsUsage } from '../../grok/session-watcher.js';
12
13
  const log = createLogger('AgentHandler');
13
14
  const claudeBackend = new ClaudeBackend();
14
15
  // Test change: Server restart validation - if you see this log, the server restarted successfully
@@ -58,12 +59,14 @@ export async function handleSpawnAgent(ctx, payload) {
58
59
  position: payload.position,
59
60
  initialSkillIds: payload.initialSkillIds,
60
61
  model: payload.model,
62
+ opencodeModel: payload.opencodeModel,
63
+ grokModel: payload.grokModel,
61
64
  customInstructions: payload.customInstructions ? `${payload.customInstructions.length} chars` : undefined,
62
65
  });
63
66
  try {
64
67
  const agent = await agentService.createAgent(payload.name, payload.class, payload.cwd, payload.position, payload.sessionId, payload.useChrome, payload.permissionMode, undefined, // initialSkillIds handled separately below
65
68
  undefined, // isBoss
66
- payload.model, payload.codexModel, payload.customInstructions, payload.provider, payload.codexConfig, payload.effort);
69
+ payload.model, payload.codexModel, payload.customInstructions, payload.provider, payload.codexConfig, payload.effort, payload.opencodeModel, payload.grokModel);
67
70
  log.log('Agent created successfully:', {
68
71
  id: agent.id,
69
72
  name: agent.name,
@@ -119,7 +122,7 @@ async function duplicateAgentConfig(source, name, position) {
119
122
  .map((skill) => skill.id);
120
123
  const agent = await agentService.createAgent(name, source.class, source.cwd, position, undefined, // sessionId - never share the source's; clone=fresh, fork=set below
121
124
  source.useChrome, source.permissionMode, undefined, // initialSkillIds handled separately below
122
- source.isBoss, source.model, source.codexModel, source.customInstructions, source.provider, source.codexConfig, source.effort, source.opencodeModel);
125
+ source.isBoss, source.model, source.codexModel, source.customInstructions, source.provider, source.codexConfig, source.effort, source.opencodeModel, source.grokModel);
123
126
  // Re-apply skills: copied direct assignments + class defaults.
124
127
  const classDefaultSkills = customClassService.getClassDefaultSkillIds(agent.class);
125
128
  const allSkillIds = [...new Set([...directSkillIds, ...classDefaultSkills])];
@@ -135,7 +138,7 @@ function duplicateOffset(source) {
135
138
  // Providers whose conversation history can be forked (resume + native fork flag).
136
139
  // Codex is intentionally excluded for now (no headless fork; would need rollout
137
140
  // duplication) — forking a Codex agent gracefully degrades to a plain clone.
138
- const FORKABLE_PROVIDERS = new Set(['claude', 'opencode']);
141
+ const FORKABLE_PROVIDERS = new Set(['claude', 'opencode', 'grok']);
139
142
  /**
140
143
  * Handle clone_agent message - duplicates an existing agent's configuration
141
144
  * into a brand-new agent (fresh session, no inherited context/memory).
@@ -614,8 +617,11 @@ function buildStatsFromTrackedData(agent) {
614
617
  // Do NOT prefer agent.contextStats here — those can become stale when previous
615
618
  // fallback calls save their (potentially wrong) output back to agent.contextStats
616
619
  // via broadcastContextStats, creating a self-reinforcing feedback loop.
617
- const defaultContextLimit = (agent.provider ?? 'claude') === 'codex' ? 258400
618
- : (agent.provider ?? 'claude') === 'opencode' ? 200000 : 200000;
620
+ const provider = agent.provider ?? 'claude';
621
+ const defaultContextLimit = provider === 'codex' ? 258400
622
+ : provider === 'opencode' ? 200000
623
+ : provider === 'grok' ? 500000
624
+ : 200000;
619
625
  const contextLimit = Math.max(1, Math.round(agent.contextLimit || defaultContextLimit));
620
626
  const rawContextUsed = Math.max(0, Math.round(agent.contextUsed || 0));
621
627
  // Guard: contextUsed can never exceed contextLimit. Values above the limit are
@@ -629,7 +635,7 @@ function buildStatsFromTrackedData(agent) {
629
635
  }
630
636
  const usedPercent = Math.min(100, Math.round((contextUsed / contextLimit) * 100));
631
637
  const freeTokens = Math.max(0, contextLimit - contextUsed);
632
- const model = agent.model || agent.codexModel || agent.opencodeModel || 'unknown';
638
+ const model = agent.model || agent.codexModel || agent.opencodeModel || agent.grokModel || 'unknown';
633
639
  return {
634
640
  model,
635
641
  contextWindow: contextLimit,
@@ -748,12 +754,47 @@ export async function handleRequestContextStats(ctx, payload) {
748
754
  return;
749
755
  }
750
756
  }
757
+ // Grok: streaming-json has no usage events. Read signals.json from the session dir.
758
+ const isGrokProvider = (agent.provider ?? 'claude') === 'grok';
759
+ if (isGrokProvider && agent.sessionId && agent.cwd) {
760
+ try {
761
+ const sessionDir = getGrokSessionDir(agent.cwd, agent.sessionId);
762
+ const usage = readGrokSignalsUsage(sessionDir);
763
+ if (usage) {
764
+ const contextLimit = Math.max(1, usage.contextWindowTokens);
765
+ const contextUsed = Math.max(0, Math.min(usage.contextTokensUsed, contextLimit));
766
+ const usedPercent = Math.min(100, Math.round(usage.contextWindowUsage ?? (contextUsed / contextLimit) * 100));
767
+ const freeTokens = Math.max(0, contextLimit - contextUsed);
768
+ const stats = {
769
+ model: agent.grokModel || agent.model || 'grok',
770
+ contextWindow: contextLimit,
771
+ totalTokens: contextUsed,
772
+ usedPercent,
773
+ categories: {
774
+ systemPrompt: { tokens: 0, percent: 0 },
775
+ systemTools: { tokens: 0, percent: 0 },
776
+ messages: { tokens: contextUsed, percent: Number(((contextUsed / contextLimit) * 100).toFixed(1)) },
777
+ freeSpace: { tokens: freeTokens, percent: Number(((freeTokens / contextLimit) * 100).toFixed(1)) },
778
+ autocompactBuffer: { tokens: 0, percent: 0 },
779
+ },
780
+ lastUpdated: Date.now(),
781
+ };
782
+ log.log(`[contextStats] Grok signals.json for ${agent.name}: ${contextUsed}/${contextLimit}`);
783
+ broadcastContextStats(ctx, payload.agentId, stats, 'from Grok signals.json');
784
+ return;
785
+ }
786
+ log.warn(`[contextStats] Grok signals.json missing/unreadable for ${agent.name} session=${agent.sessionId}`);
787
+ }
788
+ catch (err) {
789
+ log.error(`[contextStats] Grok signals read failed: ${err}`);
790
+ }
791
+ }
751
792
  // Fallback: generate from tracked data
752
793
  const latestAgent = agentService.getAgent(payload.agentId) || agent;
753
794
  const stats = buildStatsFromTrackedData(latestAgent);
754
- const label = latestAgent.provider === 'codex' ? 'estimated from turn usage'
755
- : latestAgent.provider === 'opencode' ? 'estimated from turn usage'
756
- : 'tracked from token usage';
795
+ const label = latestAgent.provider === 'codex' || latestAgent.provider === 'opencode' || latestAgent.provider === 'grok'
796
+ ? 'estimated from turn usage'
797
+ : 'tracked from token usage';
757
798
  broadcastContextStats(ctx, payload.agentId, stats, label);
758
799
  }
759
800
  /**
@@ -813,10 +854,14 @@ export async function handleUpdateAgentProperties(ctx, payload) {
813
854
  const normalizedUpdatedOpencodeModel = updates.opencodeModel !== undefined
814
855
  ? agentService.sanitizeOpencodeModel(updates.opencodeModel)
815
856
  : undefined;
857
+ const normalizedUpdatedGrokModel = updates.grokModel !== undefined
858
+ ? agentService.sanitizeGrokModel(updates.grokModel)
859
+ : undefined;
816
860
  // Track if model changed (requires hot restart to apply new model while preserving context)
817
861
  const modelChanged = updates.model !== undefined && normalizedUpdatedModel !== agent.model;
818
862
  const codexModelChanged = updates.codexModel !== undefined && normalizedUpdatedCodexModel !== agent.codexModel;
819
863
  const opencodeModelChanged = updates.opencodeModel !== undefined && normalizedUpdatedOpencodeModel !== agent.opencodeModel;
864
+ const grokModelChanged = updates.grokModel !== undefined && normalizedUpdatedGrokModel !== agent.grokModel;
820
865
  const providerChanged = updates.provider !== undefined && updates.provider !== agent.provider;
821
866
  const classChanged = updates.class !== undefined && updates.class !== agent.class;
822
867
  const codexConfigChanged = updates.codexConfig !== undefined
@@ -878,6 +923,9 @@ export async function handleUpdateAgentProperties(ctx, payload) {
878
923
  if (updates.opencodeModel !== undefined) {
879
924
  agentUpdates.opencodeModel = normalizedUpdatedOpencodeModel;
880
925
  }
926
+ if (updates.grokModel !== undefined) {
927
+ agentUpdates.grokModel = normalizedUpdatedGrokModel;
928
+ }
881
929
  if (updates.effort !== undefined) {
882
930
  agentUpdates.effort = updates.effort;
883
931
  }
@@ -920,7 +968,8 @@ export async function handleUpdateAgentProperties(ctx, payload) {
920
968
  }
921
969
  // If model changed, do a hot restart: stop process, resume with new model
922
970
  // This preserves context by using --resume with the existing sessionId
923
- if ((modelChanged || codexModelChanged || opencodeModelChanged || providerChanged || codexConfigChanged || classChanged) && sessionId) {
971
+ const modelLikeChanged = modelChanged || codexModelChanged || opencodeModelChanged || grokModelChanged || providerChanged || codexConfigChanged || classChanged;
972
+ if (modelLikeChanged && sessionId) {
924
973
  const reason = providerChanged
925
974
  ? `runtime changed to ${updates.provider}`
926
975
  : classChanged
@@ -931,7 +980,9 @@ export async function handleUpdateAgentProperties(ctx, payload) {
931
980
  ? `Codex model changed to ${updates.codexModel}`
932
981
  : opencodeModelChanged
933
982
  ? `OpenCode model changed to ${updates.opencodeModel}`
934
- : `model changed to ${updates.model}`;
983
+ : grokModelChanged
984
+ ? `Grok model changed to ${updates.grokModel}`
985
+ : `model changed to ${updates.model}`;
935
986
  log.log(`Agent ${agent.name}: ${reason}, hot restarting with --resume to preserve context`);
936
987
  try {
937
988
  await runtimeService.stopAgent(agentId);
@@ -946,7 +997,7 @@ export async function handleUpdateAgentProperties(ctx, payload) {
946
997
  log.error(`Failed to hot restart agent ${agent.name} after runtime config change:`, err);
947
998
  }
948
999
  }
949
- else if ((modelChanged || codexModelChanged || opencodeModelChanged || providerChanged || codexConfigChanged || classChanged) && !sessionId) {
1000
+ else if (modelLikeChanged && !sessionId) {
950
1001
  const reason = providerChanged
951
1002
  ? `runtime changed to ${updates.provider}`
952
1003
  : classChanged
@@ -957,12 +1008,14 @@ export async function handleUpdateAgentProperties(ctx, payload) {
957
1008
  ? `Codex model changed to ${updates.codexModel}`
958
1009
  : opencodeModelChanged
959
1010
  ? `OpenCode model changed to ${updates.opencodeModel}`
960
- : `model changed to ${updates.model}`;
1011
+ : grokModelChanged
1012
+ ? `Grok model changed to ${updates.grokModel}`
1013
+ : `model changed to ${updates.model}`;
961
1014
  log.log(`Agent ${agent.name}: ${reason}, will apply on next session start`);
962
1015
  }
963
1016
  // If Chrome flag changed, do a hot restart to add/remove --chrome flag
964
1017
  // Only restart if model didn't already trigger a restart
965
- if (useChromeChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !providerChanged && !codexConfigChanged && sessionId) {
1018
+ if (useChromeChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !grokModelChanged && !providerChanged && !codexConfigChanged && sessionId) {
966
1019
  const chromeStatus = updates.useChrome ? 'enabled' : 'disabled';
967
1020
  log.log(`Agent ${agent.name}: Chrome ${chromeStatus}, hot restarting with --resume to apply change`);
968
1021
  try {
@@ -982,13 +1035,13 @@ export async function handleUpdateAgentProperties(ctx, payload) {
982
1035
  log.error(`Failed to hot restart agent ${agent.name} after Chrome change:`, err);
983
1036
  }
984
1037
  }
985
- else if (useChromeChanged && !providerChanged && !codexConfigChanged && !codexModelChanged && !opencodeModelChanged && !sessionId) {
1038
+ else if (useChromeChanged && !providerChanged && !codexConfigChanged && !codexModelChanged && !opencodeModelChanged && !grokModelChanged && !sessionId) {
986
1039
  // No existing session, chrome flag will apply on next start
987
1040
  const chromeStatus = updates.useChrome ? 'enabled' : 'disabled';
988
1041
  log.log(`Agent ${agent.name}: Chrome ${chromeStatus}, will apply on next session start`);
989
1042
  }
990
1043
  // If effort changed, hot restart to apply new --effort flag
991
- if (effortChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !providerChanged && !codexConfigChanged && !classChanged && !useChromeChanged && sessionId) {
1044
+ if (effortChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !grokModelChanged && !providerChanged && !codexConfigChanged && !classChanged && !useChromeChanged && sessionId) {
992
1045
  const effortLabel = updates.effort || 'default';
993
1046
  log.log(`Agent ${agent.name}: effort changed to ${effortLabel}, hot restarting with --resume to apply change`);
994
1047
  try {
@@ -1052,7 +1105,7 @@ export async function handleUpdateAgentProperties(ctx, payload) {
1052
1105
  }
1053
1106
  // If skills changed and we didn't already hot restart for model/chrome/cwd change, do it now
1054
1107
  // Skills are injected into the system prompt, so we need to restart to apply them
1055
- if (skillsChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !providerChanged && !codexConfigChanged && !classChanged && !useChromeChanged && !cwdChanged && sessionId) {
1108
+ if (skillsChanged && !modelChanged && !codexModelChanged && !opencodeModelChanged && !grokModelChanged && !providerChanged && !codexConfigChanged && !classChanged && !useChromeChanged && !cwdChanged && sessionId) {
1056
1109
  log.log(`Agent ${agent.name}: Skills changed, hot restarting with --resume to apply new system prompt`);
1057
1110
  try {
1058
1111
  // Stop the current Claude process