tide-commander 1.148.0 → 1.150.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -3
  3. package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
  4. package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
  5. package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
  10. package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
  12. package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
  13. package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
  15. package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
  16. package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
  17. package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
  18. package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
  20. package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
  21. package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
  25. package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
  26. package/dist/assets/grok.ico +0 -0
  27. package/dist/assets/grok.png +0 -0
  28. package/dist/assets/index-Ay7dKyeR.js +1 -0
  29. package/dist/assets/index-BMw7AAAK.js +5 -0
  30. package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
  31. package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
  32. package/dist/assets/index-CIon0DLf.js +2 -0
  33. package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
  34. package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
  35. package/dist/assets/index-DR6_WeF_.js +1 -0
  36. package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
  37. package/dist/assets/main-D5sqeuwB.js +286 -0
  38. package/dist/assets/main-JBkwGXfE.css +1 -0
  39. package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
  40. package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
  41. package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/de/config.json +3 -1
  44. package/dist/locales/en/config.json +2 -0
  45. package/dist/locales/en/terminal.json +6 -0
  46. package/dist/locales/en/tools.json +4 -2
  47. package/dist/locales/es/config.json +2 -0
  48. package/dist/locales/fr/config.json +3 -1
  49. package/dist/locales/hi/config.json +3 -1
  50. package/dist/locales/it/config.json +3 -1
  51. package/dist/locales/ja/config.json +3 -1
  52. package/dist/locales/pt/config.json +3 -1
  53. package/dist/locales/ru/config.json +3 -1
  54. package/dist/locales/zh-CN/config.json +3 -1
  55. package/dist/src/packages/server/claude/backend.js +114 -10
  56. package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
  57. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
  58. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  59. package/dist/src/packages/server/claude/session-loader.js +295 -0
  60. package/dist/src/packages/server/codex/json-event-parser.js +149 -4
  61. package/dist/src/packages/server/data/index.js +1 -0
  62. package/dist/src/packages/server/grok/backend.js +235 -0
  63. package/dist/src/packages/server/grok/index.js +4 -0
  64. package/dist/src/packages/server/grok/json-event-parser.js +153 -0
  65. package/dist/src/packages/server/grok/session-watcher.js +493 -0
  66. package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
  67. package/dist/src/packages/server/routes/agents.js +19 -11
  68. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  69. package/dist/src/packages/server/runtime/index.js +1 -0
  70. package/dist/src/packages/server/services/agent-service.js +26 -5
  71. package/dist/src/packages/server/services/grok-usage-service.js +374 -0
  72. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  73. package/dist/src/packages/server/services/runtime-events.js +46 -7
  74. package/dist/src/packages/server/services/runtime-service.js +19 -3
  75. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  76. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  77. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
  78. package/dist/src/packages/shared/agent-types.js +10 -0
  79. package/package.json +1 -1
  80. package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
  81. package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
  82. package/dist/assets/SceneManager-QqKygyaw.js +0 -104
  83. package/dist/assets/SpawnModal-BOeStihN.js +0 -1
  84. package/dist/assets/index-DOzFBkSH.js +0 -1
  85. package/dist/assets/index-DP7kd91h.js +0 -1
  86. package/dist/assets/index-DRyMARFk.js +0 -5
  87. package/dist/assets/index-DuWmOZdY.js +0 -2
  88. package/dist/assets/main-C2zgPci-.js +0 -256
  89. package/dist/assets/main-C3JO2TRI.css +0 -1
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Grok Usage Service
3
+ *
4
+ * Surfaces the same plan-limit gauges the Grok CLI's `/usage` command shows,
5
+ * using the CLI chat-proxy billing endpoints with the OAuth token in
6
+ * `~/.grok/auth.json`:
7
+ *
8
+ * - `GET /v1/billing?format=credits` — subscription usage percent +
9
+ * current weekly/monthly period (what the CLI logs as
10
+ * "billing: fetched credits config")
11
+ * - `GET /v1/billing` — calendar-month spend/credit allotment
12
+ * (`used` / `monthlyLimit`) when the credits payload is weekly-only
13
+ *
14
+ * When both are available the snapshot exposes **weekly** and **monthly**
15
+ * gauges side-by-side, matching the terminal `/usage` panel.
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ import { createLogger } from '../utils/logger.js';
21
+ const log = createLogger('GrokUsage');
22
+ const AUTH_PATH = path.join(os.homedir(), '.grok', 'auth.json');
23
+ const DEFAULT_BILLING_BASE = 'https://cli-chat-proxy.grok.com/v1/billing';
24
+ const BILLING_TIMEOUT_MS = 5_000;
25
+ // Account-wide gauges — one cache line serves every Grok agent/client poll.
26
+ const RATE_LIMIT_CACHE_TTL_MS = 60_000;
27
+ const RATE_LIMIT_429_BACKOFF_MS = 5 * 60_000;
28
+ const RATE_LIMIT_ERROR_BACKOFF_MS = 30_000;
29
+ function billingBaseUrl() {
30
+ const base = process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim();
31
+ if (base) {
32
+ return `${base.replace(/\/+$/, '')}/billing`;
33
+ }
34
+ return DEFAULT_BILLING_BASE;
35
+ }
36
+ function readGrokAccessToken() {
37
+ try {
38
+ if (!fs.existsSync(AUTH_PATH)) {
39
+ return { error: 'No Grok CLI credentials found — run `grok login`' };
40
+ }
41
+ const raw = JSON.parse(fs.readFileSync(AUTH_PATH, 'utf-8'));
42
+ // auth.json is keyed by issuer::client_id; pick the first entry with a token.
43
+ for (const entry of Object.values(raw)) {
44
+ if (!entry || typeof entry !== 'object')
45
+ continue;
46
+ const token = entry.key;
47
+ if (typeof token !== 'string' || token === '')
48
+ continue;
49
+ const expiresRaw = entry.expires_at ?? entry.expiresAt;
50
+ if (typeof expiresRaw === 'string' || typeof expiresRaw === 'number') {
51
+ const expiresMs = typeof expiresRaw === 'number' ? expiresRaw : Date.parse(expiresRaw);
52
+ if (Number.isFinite(expiresMs) && expiresMs <= Date.now()) {
53
+ return {
54
+ error: 'Grok CLI OAuth token has expired — run `grok login` or any Grok session to refresh it',
55
+ };
56
+ }
57
+ }
58
+ return { token };
59
+ }
60
+ return { error: 'Grok CLI credentials are missing an OAuth token' };
61
+ }
62
+ catch (err) {
63
+ log.warn(`Failed to read Grok credentials at ${AUTH_PATH}: ${err}`);
64
+ return { error: 'Failed to read Grok CLI credentials' };
65
+ }
66
+ }
67
+ function unwrapVal(value) {
68
+ if (typeof value === 'number' && Number.isFinite(value))
69
+ return value;
70
+ if (value && typeof value === 'object' && 'val' in value) {
71
+ const inner = value.val;
72
+ if (typeof inner === 'number' && Number.isFinite(inner))
73
+ return inner;
74
+ }
75
+ return null;
76
+ }
77
+ function parseIso(value) {
78
+ if (typeof value !== 'string' || value.trim() === '')
79
+ return null;
80
+ const ms = Date.parse(value);
81
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
82
+ }
83
+ function clampPercent(value) {
84
+ return Math.max(0, Math.min(100, value));
85
+ }
86
+ /** Map API period type strings / date span → weekly | monthly | period. */
87
+ export function classifyPeriodType(typeRaw, startIso, endIso) {
88
+ if (typeof typeRaw === 'string') {
89
+ const t = typeRaw.toUpperCase();
90
+ if (t.includes('WEEK'))
91
+ return 'weekly';
92
+ if (t.includes('MONTH'))
93
+ return 'monthly';
94
+ }
95
+ if (!startIso || !endIso)
96
+ return 'period';
97
+ const start = Date.parse(startIso);
98
+ const end = Date.parse(endIso);
99
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
100
+ return 'period';
101
+ const days = (end - start) / (24 * 60 * 60 * 1000);
102
+ if (days >= 5 && days <= 10)
103
+ return 'weekly';
104
+ if (days >= 25 && days <= 35)
105
+ return 'monthly';
106
+ return 'period';
107
+ }
108
+ /** @deprecated use classifyPeriodType — kept for existing tests. */
109
+ export function classifyBillingPeriod(startIso, endIso) {
110
+ return classifyPeriodType(null, startIso, endIso);
111
+ }
112
+ function parseRetryAfterMs(headerValue) {
113
+ if (!headerValue)
114
+ return undefined;
115
+ const seconds = Number(headerValue);
116
+ if (Number.isFinite(seconds))
117
+ return Math.max(0, seconds * 1000);
118
+ const dateMs = Date.parse(headerValue);
119
+ if (Number.isFinite(dateMs))
120
+ return Math.max(0, dateMs - Date.now());
121
+ return undefined;
122
+ }
123
+ async function fetchBillingJson(url, token) {
124
+ const response = await fetch(url, {
125
+ headers: {
126
+ Authorization: `Bearer ${token}`,
127
+ Accept: 'application/json',
128
+ 'x-grok-client-version': '0.2.93',
129
+ },
130
+ signal: AbortSignal.timeout(BILLING_TIMEOUT_MS),
131
+ });
132
+ if (!response.ok) {
133
+ return {
134
+ ok: false,
135
+ status: response.status,
136
+ error: `Grok billing endpoint returned ${response.status}`,
137
+ retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
138
+ };
139
+ }
140
+ const body = (await response.json());
141
+ return {
142
+ ok: true,
143
+ status: response.status,
144
+ config: body?.config ?? {},
145
+ retryAfterMs: parseRetryAfterMs(response.headers.get('retry-after')),
146
+ };
147
+ }
148
+ /**
149
+ * Parse `/billing?format=credits` — the payload the CLI uses for the
150
+ * Weekly/Monthly percent gauges in `/usage`.
151
+ */
152
+ export function parseCreditsConfig(config) {
153
+ let weekly = null;
154
+ let monthly = null;
155
+ let onDemand = null;
156
+ const percentRaw = config.creditUsagePercent;
157
+ const percent = typeof percentRaw === 'number' && Number.isFinite(percentRaw)
158
+ ? clampPercent(percentRaw)
159
+ : null;
160
+ const currentPeriod = config.currentPeriod && typeof config.currentPeriod === 'object'
161
+ ? config.currentPeriod
162
+ : null;
163
+ const periodStart = parseIso(currentPeriod?.start) ?? parseIso(config.billingPeriodStart);
164
+ const periodEnd = parseIso(currentPeriod?.end) ?? parseIso(config.billingPeriodEnd);
165
+ const kind = classifyPeriodType(currentPeriod?.type, periodStart, periodEnd);
166
+ if (percent != null && periodEnd) {
167
+ const window = {
168
+ utilization: percent,
169
+ resetsAt: periodEnd,
170
+ };
171
+ if (kind === 'weekly')
172
+ weekly = window;
173
+ else if (kind === 'monthly')
174
+ monthly = window;
175
+ else {
176
+ // Unknown period type — prefer weekly label when span is short-ish.
177
+ if (kind === 'period' && periodStart && periodEnd) {
178
+ const days = (Date.parse(periodEnd) - Date.parse(periodStart)) / (24 * 60 * 60 * 1000);
179
+ if (days <= 14)
180
+ weekly = window;
181
+ else
182
+ monthly = window;
183
+ }
184
+ else {
185
+ weekly = window;
186
+ }
187
+ }
188
+ }
189
+ // productUsage[] can carry per-product percents (e.g. GrokBuild) — prefer the
190
+ // primary creditUsagePercent above, but fall back if it was missing.
191
+ if (percent == null && Array.isArray(config.productUsage)) {
192
+ for (const entry of config.productUsage) {
193
+ if (!entry || typeof entry !== 'object')
194
+ continue;
195
+ const p = entry.usagePercent;
196
+ if (typeof p === 'number' && Number.isFinite(p) && periodEnd) {
197
+ const window = {
198
+ utilization: clampPercent(p),
199
+ resetsAt: periodEnd,
200
+ };
201
+ if (kind === 'monthly')
202
+ monthly = window;
203
+ else
204
+ weekly = window;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ const onDemandCap = unwrapVal(config.onDemandCap);
210
+ const onDemandUsed = unwrapVal(config.onDemandUsed) ?? 0;
211
+ if (onDemandCap != null && onDemandCap > 0) {
212
+ onDemand = {
213
+ utilization: clampPercent((onDemandUsed / onDemandCap) * 100),
214
+ resetsAt: periodEnd ?? parseIso(config.billingPeriodEnd) ?? new Date().toISOString(),
215
+ used: onDemandUsed,
216
+ limit: onDemandCap,
217
+ };
218
+ }
219
+ return { weekly, monthly, onDemand };
220
+ }
221
+ /**
222
+ * Parse default `/billing` — absolute used/monthlyLimit spend-style cap.
223
+ * Used to fill the monthly gauge when the credits payload only has weekly.
224
+ */
225
+ export function parseSpendConfig(config) {
226
+ const used = unwrapVal(config.used) ?? unwrapVal(config.includedUsed);
227
+ const limit = unwrapVal(config.monthlyLimit);
228
+ const periodEnd = parseIso(config.billingPeriodEnd);
229
+ if (used == null || limit == null || limit <= 0 || !periodEnd)
230
+ return null;
231
+ return {
232
+ utilization: clampPercent((used / limit) * 100),
233
+ resetsAt: periodEnd,
234
+ used,
235
+ limit,
236
+ };
237
+ }
238
+ /**
239
+ * Raw network calls to the CLI chat-proxy billing endpoints. Prefer
240
+ * `getGrokRateLimits()` which caches + single-flights.
241
+ */
242
+ async function fetchGrokBillingFromApi() {
243
+ const creds = readGrokAccessToken();
244
+ if ('error' in creds) {
245
+ return { rateLimits: null, error: creds.error };
246
+ }
247
+ const base = billingBaseUrl();
248
+ try {
249
+ // Parallel: credits format (CLI /usage) + default spend allotment.
250
+ const [creditsResult, spendResult] = await Promise.all([
251
+ fetchBillingJson(`${base}?format=credits`, creds.token),
252
+ fetchBillingJson(base, creds.token),
253
+ ]);
254
+ // Prefer status/retry from credits (primary); fall back to spend.
255
+ if (!creditsResult.ok && !spendResult.ok) {
256
+ return {
257
+ rateLimits: null,
258
+ error: creditsResult.error,
259
+ status: creditsResult.status,
260
+ retryAfterMs: creditsResult.retryAfterMs ?? spendResult.retryAfterMs,
261
+ };
262
+ }
263
+ let weekly = null;
264
+ let monthly = null;
265
+ let onDemand = null;
266
+ if (creditsResult.ok) {
267
+ const parsed = parseCreditsConfig(creditsResult.config);
268
+ weekly = parsed.weekly;
269
+ monthly = parsed.monthly;
270
+ onDemand = parsed.onDemand;
271
+ }
272
+ // Fill monthly from spend allotment when credits only gave weekly (the
273
+ // SuperGrok dual-gauge layout in the CLI /usage panel).
274
+ if (spendResult.ok) {
275
+ const spendMonthly = parseSpendConfig(spendResult.config);
276
+ if (spendMonthly && !monthly) {
277
+ monthly = spendMonthly;
278
+ }
279
+ // On-demand may only appear on the default payload.
280
+ if (!onDemand) {
281
+ const onDemandCap = unwrapVal(spendResult.config.onDemandCap);
282
+ const onDemandUsed = unwrapVal(spendResult.config.onDemandUsed) ?? 0;
283
+ const periodEnd = parseIso(spendResult.config.billingPeriodEnd);
284
+ if (onDemandCap != null && onDemandCap > 0 && periodEnd) {
285
+ onDemand = {
286
+ utilization: clampPercent((onDemandUsed / onDemandCap) * 100),
287
+ resetsAt: periodEnd,
288
+ used: onDemandUsed,
289
+ limit: onDemandCap,
290
+ };
291
+ }
292
+ }
293
+ }
294
+ if (!weekly && !monthly && !onDemand) {
295
+ return {
296
+ rateLimits: null,
297
+ error: creditsResult.ok
298
+ ? 'Grok billing response had no usable usage limits'
299
+ : creditsResult.error,
300
+ status: creditsResult.ok ? creditsResult.status : creditsResult.status,
301
+ };
302
+ }
303
+ return {
304
+ rateLimits: { weekly, monthly, onDemand },
305
+ error: null,
306
+ status: creditsResult.ok ? creditsResult.status : spendResult.status,
307
+ };
308
+ }
309
+ catch (err) {
310
+ log.warn(`Failed to fetch Grok billing: ${err}`);
311
+ const reason = err?.name === 'TimeoutError' ? 'request timed out' : (err?.message ?? 'request failed');
312
+ return { rateLimits: null, error: `Could not reach Grok billing endpoint (${reason})` };
313
+ }
314
+ }
315
+ let rateLimitCache = null;
316
+ let lastGoodRateLimits = null;
317
+ let rateLimitInFlight = null;
318
+ async function getGrokRateLimits() {
319
+ const now = Date.now();
320
+ if (rateLimitCache && now < rateLimitCache.validUntil) {
321
+ return { rateLimits: rateLimitCache.rateLimits, error: rateLimitCache.error };
322
+ }
323
+ if (rateLimitInFlight)
324
+ return rateLimitInFlight;
325
+ rateLimitInFlight = (async () => {
326
+ const result = await fetchGrokBillingFromApi();
327
+ let ttl = RATE_LIMIT_CACHE_TTL_MS;
328
+ if (result.rateLimits) {
329
+ lastGoodRateLimits = result.rateLimits;
330
+ }
331
+ else if (result.status === 429) {
332
+ ttl = Math.max(result.retryAfterMs ?? RATE_LIMIT_429_BACKOFF_MS, RATE_LIMIT_CACHE_TTL_MS);
333
+ }
334
+ else {
335
+ ttl = RATE_LIMIT_ERROR_BACKOFF_MS;
336
+ }
337
+ const entry = {
338
+ rateLimits: result.rateLimits ?? lastGoodRateLimits,
339
+ error: result.error,
340
+ validUntil: Date.now() + ttl,
341
+ };
342
+ rateLimitCache = entry;
343
+ return { rateLimits: entry.rateLimits, error: entry.error };
344
+ })();
345
+ try {
346
+ return await rateLimitInFlight;
347
+ }
348
+ finally {
349
+ rateLimitInFlight = null;
350
+ }
351
+ }
352
+ /** Test helper: drop the cached billing result and backoff state. */
353
+ export function resetGrokRateLimitCache() {
354
+ rateLimitCache = null;
355
+ lastGoodRateLimits = null;
356
+ rateLimitInFlight = null;
357
+ }
358
+ export async function buildGrokUsageSnapshot(agent) {
359
+ const { rateLimits, error: rateLimitsError } = await getGrokRateLimits();
360
+ return {
361
+ provider: 'grok',
362
+ fetchedAt: Date.now(),
363
+ session: {
364
+ tokensUsed: agent.tokensUsed ?? 0,
365
+ contextUsed: agent.contextUsed ?? 0,
366
+ contextLimit: agent.contextLimit ?? 500_000,
367
+ taskCount: agent.taskCount ?? 0,
368
+ lastActivity: agent.lastActivity ?? 0,
369
+ },
370
+ rateLimits,
371
+ rateLimitsError,
372
+ cliHint: "Run /usage inside this agent's terminal to see live weekly and monthly usage limits.",
373
+ };
374
+ }
@@ -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: