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.
- package/CHANGELOG.md +33 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
- package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
- package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
- package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
- package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/index-Ay7dKyeR.js +1 -0
- package/dist/assets/index-BMw7AAAK.js +5 -0
- package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
- package/dist/assets/index-CIon0DLf.js +2 -0
- package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
- package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
- package/dist/assets/index-DR6_WeF_.js +1 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
- package/dist/assets/main-D5sqeuwB.js +286 -0
- package/dist/assets/main-JBkwGXfE.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +3 -1
- package/dist/locales/en/config.json +2 -0
- package/dist/locales/en/terminal.json +6 -0
- package/dist/locales/en/tools.json +4 -2
- package/dist/locales/es/config.json +2 -0
- package/dist/locales/fr/config.json +3 -1
- package/dist/locales/hi/config.json +3 -1
- package/dist/locales/it/config.json +3 -1
- package/dist/locales/ja/config.json +3 -1
- package/dist/locales/pt/config.json +3 -1
- package/dist/locales/ru/config.json +3 -1
- package/dist/locales/zh-CN/config.json +3 -1
- package/dist/src/packages/server/claude/backend.js +114 -10
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +295 -0
- package/dist/src/packages/server/codex/json-event-parser.js +149 -4
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +235 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +153 -0
- package/dist/src/packages/server/grok/session-watcher.js +493 -0
- package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
- package/dist/src/packages/server/routes/agents.js +19 -11
- 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/grok-usage-service.js +374 -0
- 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 +73 -14
- 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-DOzFBkSH.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
|
@@ -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
|
-
:
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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:
|