openlimiter 1.2.0 → 1.3.1

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 (55) hide show
  1. package/README.md +45 -16
  2. package/dist/bin.js +53 -4
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.d.ts +110 -11
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +1064 -64
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codex-device-login.d.ts +87 -0
  9. package/dist/codex-device-login.d.ts.map +1 -0
  10. package/dist/codex-device-login.js +291 -0
  11. package/dist/codex-device-login.js.map +1 -0
  12. package/dist/config.d.ts +44 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +129 -7
  15. package/dist/config.js.map +1 -1
  16. package/dist/hub-auth.d.ts +137 -0
  17. package/dist/hub-auth.d.ts.map +1 -0
  18. package/dist/hub-auth.js +280 -0
  19. package/dist/hub-auth.js.map +1 -0
  20. package/dist/hub-sync.d.ts +144 -0
  21. package/dist/hub-sync.d.ts.map +1 -0
  22. package/dist/hub-sync.js +370 -0
  23. package/dist/hub-sync.js.map +1 -0
  24. package/dist/hub.d.ts +60 -0
  25. package/dist/hub.d.ts.map +1 -0
  26. package/dist/hub.js +178 -0
  27. package/dist/hub.js.map +1 -0
  28. package/dist/index.d.ts +6 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/ingest.d.ts +26 -0
  33. package/dist/ingest.d.ts.map +1 -1
  34. package/dist/ingest.js +116 -1
  35. package/dist/ingest.js.map +1 -1
  36. package/dist/qr.d.ts.map +1 -1
  37. package/dist/qr.js +1 -0
  38. package/dist/qr.js.map +1 -1
  39. package/dist/session.d.ts +46 -0
  40. package/dist/session.d.ts.map +1 -0
  41. package/dist/session.js +121 -0
  42. package/dist/session.js.map +1 -0
  43. package/dist/statusline.d.ts +14 -0
  44. package/dist/statusline.d.ts.map +1 -1
  45. package/dist/statusline.js +258 -19
  46. package/dist/statusline.js.map +1 -1
  47. package/dist/terminal.d.ts +109 -0
  48. package/dist/terminal.d.ts.map +1 -0
  49. package/dist/terminal.js +974 -0
  50. package/dist/terminal.js.map +1 -0
  51. package/package.json +4 -4
  52. package/dist/serve.d.ts +0 -170
  53. package/dist/serve.d.ts.map +0 -1
  54. package/dist/serve.js +0 -533
  55. package/dist/serve.js.map +0 -1
@@ -0,0 +1,974 @@
1
+ import { readFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { connectors } from "@openlimiter/connectors";
4
+ import { writeFileAtomically } from "@openlimiter/core";
5
+ import { DEFAULT_PROVIDERS, DEFAULT_STATUSLINE, readConfig, writeConfig } from "./config.js";
6
+ import { decodeWrappedStatuslineCommand, encodeWrappedStatuslineCommand } from "./statusline-wrapper.js";
7
+ export const TERMINAL_HOST_NAMES = [
8
+ "claude",
9
+ "antigravity",
10
+ "grok",
11
+ "codex",
12
+ "shell"
13
+ ];
14
+ export const UNSUPPORTED_HOST_ALTERNATIVE = "No status line support, use the shell prompt";
15
+ export const STATUS_WIRED = "Wired";
16
+ /**
17
+ * The host already draws a status line, and it is not ours.
18
+ *
19
+ * Told apart from `STATUS_NOT_WIRED` on purpose: a host with somebody else's
20
+ * command already in the slot is not a host with nothing there, and install
21
+ * behaves differently in each case, wrapping the first and writing the second
22
+ * fresh. Saying "Not installed" for both, as this used to, told a person
23
+ * nothing about which one they were looking at.
24
+ */
25
+ export const STATUS_OWN_LINE_FOUND = "Your own status line found, install wraps it";
26
+ export const STATUS_NOT_WIRED = "Not wired";
27
+ export const CONNECT_FIRST_SENTENCE = "Connect it first";
28
+ function claudeSettingsPath(home) {
29
+ return path.join(home, ".claude", "settings.json");
30
+ }
31
+ function antigravitySettingsPath(home) {
32
+ return path.join(home, ".gemini", "antigravity-cli", "settings.json");
33
+ }
34
+ function grokConfigPath(home) {
35
+ return path.join(home, ".grok", "config.toml");
36
+ }
37
+ function codexConfigPath(home) {
38
+ return path.join(home, ".codex", "config.toml");
39
+ }
40
+ /** Used only once nothing could ask the shell itself where its profile lives. */
41
+ function powerShellProfilePath(home, platform) {
42
+ if (platform === "win32") {
43
+ return path.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
44
+ }
45
+ return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
46
+ }
47
+ const POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS = 5_000;
48
+ /**
49
+ * Ask a shell where `$PROFILE` actually is, rather than guessing.
50
+ *
51
+ * The hardcoded guess above is Windows PowerShell 5.1's own default, and only
52
+ * that: PowerShell 7 keeps its profile under a `PowerShell` folder, not
53
+ * `WindowsPowerShell`, and either one moves the moment Documents itself is
54
+ * redirected, which OneDrive's Known Folder Move does on its own with nobody
55
+ * asking. A profile snippet written to the wrong path is a snippet nobody's
56
+ * shell ever loads.
57
+ *
58
+ * `pwsh`, the PowerShell somebody installed on purpose and the one most
59
+ * people who have it actually run, is asked first, on every platform it ships
60
+ * for. `powershell.exe`, Windows only and present on every Windows machine by
61
+ * default, is the fallback there. Neither answering, including no runner
62
+ * being injected at all, falls back to the hardcoded guess: a feature that
63
+ * degrades to its old behaviour rather than one that breaks outright.
64
+ */
65
+ async function resolvePowerShellProfilePath(home, platform, runner) {
66
+ const fallback = powerShellProfilePath(home, platform);
67
+ if (runner === undefined)
68
+ return fallback;
69
+ const candidates = platform === "win32" ? ["pwsh.exe", "powershell.exe"] : ["pwsh"];
70
+ for (const executable of candidates) {
71
+ let result;
72
+ try {
73
+ result = await runner(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ if (!result.ok)
79
+ continue;
80
+ const trimmed = result.stdout.trim();
81
+ if (trimmed.length > 0)
82
+ return trimmed;
83
+ }
84
+ return fallback;
85
+ }
86
+ export async function readJsonConfig(filePath) {
87
+ try {
88
+ const text = await readFile(filePath, "utf8");
89
+ try {
90
+ const parsed = JSON.parse(text);
91
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
92
+ return { kind: "ok", data: parsed };
93
+ }
94
+ return { kind: "parse_error", error: new Error("Not a JSON object") };
95
+ }
96
+ catch (err) {
97
+ return { kind: "parse_error", error: err };
98
+ }
99
+ }
100
+ catch (err) {
101
+ if (typeof err === "object" &&
102
+ err !== null &&
103
+ "code" in err &&
104
+ err.code === "ENOENT") {
105
+ return { kind: "missing" };
106
+ }
107
+ return { kind: "parse_error", error: err };
108
+ }
109
+ }
110
+ export function validateToml(text) {
111
+ const lines = text.split(/\r?\n/);
112
+ let inSingleTriple = false;
113
+ let inDoubleTriple = false;
114
+ let bracketDepth = 0;
115
+ let braceDepth = 0;
116
+ for (const line of lines) {
117
+ let i = 0;
118
+ let inSingle = false;
119
+ let inDouble = false;
120
+ let cleanedLine = "";
121
+ const wasInsideGroup = bracketDepth > 0 || braceDepth > 0 || inSingleTriple || inDoubleTriple;
122
+ while (i < line.length) {
123
+ if (inDoubleTriple) {
124
+ if (line.slice(i, i + 3) === '"""') {
125
+ inDoubleTriple = false;
126
+ i += 3;
127
+ continue;
128
+ }
129
+ i++;
130
+ continue;
131
+ }
132
+ if (inSingleTriple) {
133
+ if (line.slice(i, i + 3) === "'''") {
134
+ inSingleTriple = false;
135
+ i += 3;
136
+ continue;
137
+ }
138
+ i++;
139
+ continue;
140
+ }
141
+ if (inDouble) {
142
+ if (line[i] === "\\" && i + 1 < line.length) {
143
+ i += 2;
144
+ continue;
145
+ }
146
+ if (line[i] === '"') {
147
+ inDouble = false;
148
+ }
149
+ i++;
150
+ continue;
151
+ }
152
+ if (inSingle) {
153
+ if (line[i] === "'") {
154
+ inSingle = false;
155
+ }
156
+ i++;
157
+ continue;
158
+ }
159
+ if (line.slice(i, i + 3) === '"""') {
160
+ inDoubleTriple = true;
161
+ i += 3;
162
+ continue;
163
+ }
164
+ if (line.slice(i, i + 3) === "'''") {
165
+ inSingleTriple = true;
166
+ i += 3;
167
+ continue;
168
+ }
169
+ if (line[i] === '"') {
170
+ inDouble = true;
171
+ i++;
172
+ continue;
173
+ }
174
+ if (line[i] === "'") {
175
+ inSingle = true;
176
+ i++;
177
+ continue;
178
+ }
179
+ if (line[i] === "#") {
180
+ break;
181
+ }
182
+ const ch = line[i];
183
+ cleanedLine += ch;
184
+ if (ch === "[")
185
+ bracketDepth++;
186
+ else if (ch === "]") {
187
+ bracketDepth--;
188
+ if (bracketDepth < 0)
189
+ return false;
190
+ }
191
+ else if (ch === "{")
192
+ braceDepth++;
193
+ else if (ch === "}") {
194
+ braceDepth--;
195
+ if (braceDepth < 0)
196
+ return false;
197
+ }
198
+ i++;
199
+ }
200
+ if (inSingle || inDouble) {
201
+ return false;
202
+ }
203
+ if (!wasInsideGroup && !inSingleTriple && !inDoubleTriple && bracketDepth === 0 && braceDepth === 0) {
204
+ const trimmed = cleanedLine.trim();
205
+ if (trimmed.length > 0) {
206
+ if (!(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
207
+ if (!trimmed.includes("=")) {
208
+ return false;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ if (inSingleTriple || inDoubleTriple)
215
+ return false;
216
+ if (bracketDepth !== 0 || braceDepth !== 0)
217
+ return false;
218
+ return true;
219
+ }
220
+ export async function readTomlConfig(filePath) {
221
+ try {
222
+ const text = await readFile(filePath, "utf8");
223
+ if (!validateToml(text)) {
224
+ return { kind: "parse_error", error: new Error("Invalid TOML") };
225
+ }
226
+ return { kind: "ok", data: text };
227
+ }
228
+ catch (err) {
229
+ if (typeof err === "object" &&
230
+ err !== null &&
231
+ "code" in err &&
232
+ err.code === "ENOENT") {
233
+ return { kind: "missing" };
234
+ }
235
+ return { kind: "parse_error", error: err };
236
+ }
237
+ }
238
+ async function readJsonFile(filePath) {
239
+ const res = await readJsonConfig(filePath);
240
+ return res.kind === "ok" ? res.data : null;
241
+ }
242
+ async function writeJsonFile(filePath, data) {
243
+ await mkdir(path.dirname(filePath), { recursive: true });
244
+ await writeFileAtomically(filePath, JSON.stringify(data, null, 2) + "\n");
245
+ }
246
+ async function readTextFile(filePath) {
247
+ try {
248
+ return await readFile(filePath, "utf8");
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ async function writeTextFile(filePath, text) {
255
+ await mkdir(path.dirname(filePath), { recursive: true });
256
+ await writeFileAtomically(filePath, text);
257
+ }
258
+ /**
259
+ * Install status line into Claude Code ~/.claude/settings.json
260
+ */
261
+ export async function installClaude(context) {
262
+ const settingsFile = claudeSettingsPath(context.homeDirectory);
263
+ const readRes = await readJsonConfig(settingsFile);
264
+ if (readRes.kind === "parse_error") {
265
+ return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
266
+ }
267
+ const existing = readRes.kind === "ok" ? readRes.data : {};
268
+ const currentStatusLine = existing["statusLine"];
269
+ let existingCommand = null;
270
+ if (typeof currentStatusLine === "object" && currentStatusLine !== null) {
271
+ const cmd = currentStatusLine["command"];
272
+ if (typeof cmd === "string")
273
+ existingCommand = cmd;
274
+ }
275
+ else if (typeof currentStatusLine === "string") {
276
+ existingCommand = currentStatusLine;
277
+ }
278
+ /*
279
+ * A command that already runs us, wrapped or not, is left exactly as it
280
+ * is: re-running install must never re-wrap an already wrapped command,
281
+ * and must never fall back to the bare default and silently drop the
282
+ * user's original status line the wrap exists to protect.
283
+ */
284
+ let commandToSet = "openlimiter statusline --host claude";
285
+ if (existingCommand !== null) {
286
+ commandToSet = existingCommand.includes("openlimiter statusline")
287
+ ? existingCommand
288
+ : `openlimiter statusline --host claude --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
289
+ }
290
+ existing["openlimiter managed"] = true;
291
+ existing["statusLine"] = {
292
+ type: "command",
293
+ command: commandToSet
294
+ };
295
+ try {
296
+ await writeJsonFile(settingsFile, existing);
297
+ return { ok: true, message: "Wired Claude Code status line." };
298
+ }
299
+ catch {
300
+ return { ok: false, message: "Could not write Claude Code settings." };
301
+ }
302
+ }
303
+ export async function uninstallClaude(context) {
304
+ const settingsFile = claudeSettingsPath(context.homeDirectory);
305
+ const readRes = await readJsonConfig(settingsFile);
306
+ if (readRes.kind === "parse_error") {
307
+ return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
308
+ }
309
+ if (readRes.kind === "missing" || !readRes.data["statusLine"]) {
310
+ return { ok: true, message: "Claude Code status line is not installed." };
311
+ }
312
+ const existing = readRes.data;
313
+ const current = existing["statusLine"];
314
+ let cmd = null;
315
+ if (typeof current === "object" && current !== null) {
316
+ const c = current["command"];
317
+ if (typeof c === "string")
318
+ cmd = c;
319
+ }
320
+ else if (typeof current === "string") {
321
+ cmd = current;
322
+ }
323
+ if (cmd !== null && cmd.includes("--wrap")) {
324
+ const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
325
+ const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
326
+ if (restored !== null) {
327
+ existing["statusLine"] = {
328
+ type: "command",
329
+ command: restored
330
+ };
331
+ delete existing["openlimiter managed"];
332
+ await writeJsonFile(settingsFile, existing);
333
+ return { ok: true, message: "Restored original Claude Code status line." };
334
+ }
335
+ }
336
+ delete existing["statusLine"];
337
+ delete existing["openlimiter managed"];
338
+ try {
339
+ await writeJsonFile(settingsFile, existing);
340
+ return { ok: true, message: "Uninstalled Claude Code status line." };
341
+ }
342
+ catch {
343
+ return { ok: false, message: "Could not update Claude Code settings." };
344
+ }
345
+ }
346
+ /**
347
+ * Install status line into Antigravity ~/.gemini/antigravity-cli/settings.json
348
+ */
349
+ export async function installAntigravity(context) {
350
+ const settingsFile = antigravitySettingsPath(context.homeDirectory);
351
+ const readRes = await readJsonConfig(settingsFile);
352
+ if (readRes.kind === "parse_error") {
353
+ return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
354
+ }
355
+ const existing = readRes.kind === "ok" ? readRes.data : {};
356
+ const current = existing["statusLine"];
357
+ let existingCommand = null;
358
+ if (typeof current === "string") {
359
+ existingCommand = current;
360
+ }
361
+ else if (typeof current === "object" && current !== null) {
362
+ const cmd = current["command"];
363
+ if (typeof cmd === "string")
364
+ existingCommand = cmd;
365
+ }
366
+ /* See installClaude: an already installed command, wrapped or not, is
367
+ left exactly as it is on a second install. */
368
+ let commandToSet = "openlimiter statusline --host antigravity";
369
+ if (existingCommand !== null) {
370
+ commandToSet = existingCommand.includes("openlimiter statusline")
371
+ ? existingCommand
372
+ : `openlimiter statusline --host antigravity --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
373
+ }
374
+ existing["openlimiter managed"] = true;
375
+ existing["statusLine"] = commandToSet;
376
+ try {
377
+ await writeJsonFile(settingsFile, existing);
378
+ return { ok: true, message: "Wired Antigravity CLI status line." };
379
+ }
380
+ catch {
381
+ return { ok: false, message: "Could not write Antigravity settings." };
382
+ }
383
+ }
384
+ export async function uninstallAntigravity(context) {
385
+ const settingsFile = antigravitySettingsPath(context.homeDirectory);
386
+ const readRes = await readJsonConfig(settingsFile);
387
+ if (readRes.kind === "parse_error") {
388
+ return { ok: false, message: `Could not read ${settingsFile}, fix it or move it aside` };
389
+ }
390
+ if (readRes.kind === "missing" || !readRes.data["statusLine"]) {
391
+ return { ok: true, message: "Antigravity status line is not installed." };
392
+ }
393
+ const existing = readRes.data;
394
+ const current = existing["statusLine"];
395
+ let cmd = null;
396
+ if (typeof current === "string") {
397
+ cmd = current;
398
+ }
399
+ else if (typeof current === "object" && current !== null) {
400
+ const c = current["command"];
401
+ if (typeof c === "string")
402
+ cmd = c;
403
+ }
404
+ if (cmd !== null && cmd.includes("--wrap")) {
405
+ const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
406
+ const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
407
+ if (restored !== null) {
408
+ existing["statusLine"] = restored;
409
+ delete existing["openlimiter managed"];
410
+ await writeJsonFile(settingsFile, existing);
411
+ return { ok: true, message: "Restored original Antigravity status line." };
412
+ }
413
+ }
414
+ delete existing["statusLine"];
415
+ delete existing["openlimiter managed"];
416
+ try {
417
+ await writeJsonFile(settingsFile, existing);
418
+ return { ok: true, message: "Uninstalled Antigravity status line." };
419
+ }
420
+ catch {
421
+ return { ok: false, message: "Could not update Antigravity settings." };
422
+ }
423
+ }
424
+ /**
425
+ * Install status line into Grok Build ~/.grok/config.toml
426
+ */
427
+ export async function installGrok(context) {
428
+ const configFile = grokConfigPath(context.homeDirectory);
429
+ const readRes = await readTomlConfig(configFile);
430
+ if (readRes.kind === "parse_error") {
431
+ return { ok: false, message: `Could not read ${configFile}, fix it or move it aside` };
432
+ }
433
+ const text = readRes.kind === "ok" ? readRes.data : "";
434
+ const sectionMatch = /(?:^|\n)(\[ui\.status_line\][\s\S]*?)(?=\n\[|$)/.exec(text);
435
+ let existingCommand = null;
436
+ const rawSection = sectionMatch?.[1];
437
+ if (sectionMatch && rawSection !== undefined) {
438
+ const cmdMatch = /command\s*=\s*"([^"]+)"/.exec(rawSection);
439
+ if (cmdMatch && cmdMatch[1]) {
440
+ existingCommand = cmdMatch[1];
441
+ }
442
+ }
443
+ /* See installClaude: an already installed command, wrapped or not, is
444
+ left exactly as it is on a second install. */
445
+ let commandToSet = "openlimiter statusline --host grok";
446
+ if (existingCommand !== null) {
447
+ commandToSet = existingCommand.includes("openlimiter statusline")
448
+ ? existingCommand
449
+ : `openlimiter statusline --host grok --wrap ${encodeWrappedStatuslineCommand(existingCommand)}`;
450
+ }
451
+ let updated;
452
+ if (sectionMatch && rawSection !== undefined) {
453
+ let section = rawSection
454
+ .replace(/[ \t]*# openlimiter managed\r?\n?/g, "")
455
+ .replace(/[ \t]*type\s*=\s*"[^"]*"\r?\n?/g, "")
456
+ .replace(/[ \t]*command\s*=\s*"[^"]*"\r?\n?/g, "");
457
+ const lines = section.split(/\r?\n/);
458
+ const header = lines[0] ?? "";
459
+ const rest = lines.slice(1).filter((l) => l.trim().length > 0);
460
+ const managedLines = [
461
+ "# openlimiter managed",
462
+ 'type = "command"',
463
+ `command = "${commandToSet}"`
464
+ ];
465
+ const newSection = [header, ...managedLines, ...rest].join("\n");
466
+ const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
467
+ updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
468
+ }
469
+ else {
470
+ const managedSection = [
471
+ "[ui.status_line]",
472
+ "# openlimiter managed",
473
+ 'type = "command"',
474
+ `command = "${commandToSet}"`
475
+ ].join("\n");
476
+ updated = text ? text.trimEnd() + "\n\n" + managedSection + "\n" : managedSection + "\n";
477
+ }
478
+ if (!updated.endsWith("\n")) {
479
+ updated += "\n";
480
+ }
481
+ try {
482
+ await writeTextFile(configFile, updated);
483
+ return { ok: true, message: "Wired Grok Build status line." };
484
+ }
485
+ catch {
486
+ return { ok: false, message: "Could not write Grok config." };
487
+ }
488
+ }
489
+ export async function uninstallGrok(context) {
490
+ const configFile = grokConfigPath(context.homeDirectory);
491
+ const readRes = await readTomlConfig(configFile);
492
+ if (readRes.kind === "parse_error") {
493
+ return { ok: false, message: `Could not read ${configFile}, fix it or move it aside` };
494
+ }
495
+ if (readRes.kind === "missing") {
496
+ return { ok: true, message: "Grok status line is not installed." };
497
+ }
498
+ const text = readRes.data;
499
+ const sectionMatch = /(?:^|\n)(\[ui\.status_line\][\s\S]*?)(?=\n\[|$)/.exec(text);
500
+ if (!sectionMatch || sectionMatch[1] === undefined) {
501
+ return { ok: true, message: "Grok status line is not installed." };
502
+ }
503
+ const section = sectionMatch[1];
504
+ const cmdMatch = /command\s*=\s*"([^"]+)"/.exec(section);
505
+ const cmd = cmdMatch?.[1] ?? null;
506
+ const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
507
+ if (cmd !== null && cmd.includes("--wrap")) {
508
+ const match = /--wrap\s+([A-Za-z0-9_-]+)/.exec(cmd);
509
+ const restored = match?.[1] ? decodeWrappedStatuslineCommand(match[1]) : null;
510
+ if (restored !== null) {
511
+ let updatedSection = section.replace(/[ \t]*# openlimiter managed\r?\n?/g, "");
512
+ updatedSection = updatedSection.replace(/command\s*=\s*"[^"]*"/, `command = "${restored}"`);
513
+ const updated = text.slice(0, startIndex) + updatedSection + text.slice(sectionMatch.index + sectionMatch[0].length);
514
+ try {
515
+ await writeTextFile(configFile, updated);
516
+ return { ok: true, message: "Restored original Grok status line." };
517
+ }
518
+ catch {
519
+ return { ok: false, message: "Could not update Grok config." };
520
+ }
521
+ }
522
+ }
523
+ let updatedSection = section
524
+ .replace(/[ \t]*# openlimiter managed\r?\n?/g, "")
525
+ .replace(/[ \t]*type\s*=\s*"[^"]*"\r?\n?/g, "")
526
+ .replace(/[ \t]*command\s*=\s*"[^"]*"\r?\n?/g, "");
527
+ const lines = updatedSection.split(/\r?\n/);
528
+ const rest = lines.slice(1).filter((l) => l.trim().length > 0);
529
+ let updated;
530
+ if (rest.length === 0) {
531
+ const before = text.slice(0, startIndex).trimEnd();
532
+ const after = text.slice(sectionMatch.index + sectionMatch[0].length).trimStart();
533
+ updated = before && after ? before + "\n\n" + after : (before || after ? (before || after) + "\n" : "");
534
+ }
535
+ else {
536
+ const newSection = [lines[0] ?? "", ...rest].join("\n");
537
+ updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
538
+ }
539
+ try {
540
+ await writeTextFile(configFile, updated);
541
+ return { ok: true, message: "Uninstalled Grok status line." };
542
+ }
543
+ catch {
544
+ return { ok: false, message: "Could not update Grok config." };
545
+ }
546
+ }
547
+ /**
548
+ * Install built in status line items into Codex ~/.codex/config.toml
549
+ */
550
+ export async function installCodex(context) {
551
+ const configFile = codexConfigPath(context.homeDirectory);
552
+ const readRes = await readTomlConfig(configFile);
553
+ if (readRes.kind === "parse_error") {
554
+ return { ok: false, message: `Could not read ${configFile}, fix it or move it aside` };
555
+ }
556
+ const text = readRes.kind === "ok" ? readRes.data : "";
557
+ const codexManagedLines = [
558
+ "# openlimiter managed",
559
+ 'status_line = ["five-hour-limit", "weekly-limit", "context-used", "model-with-reasoning", "current-dir"]',
560
+ "status_line_use_colors = true"
561
+ ];
562
+ const sectionMatch = /(?:^|\n)(\[tui\][\s\S]*?)(?=\n\[|$)/.exec(text);
563
+ let updated;
564
+ const rawSection = sectionMatch?.[1];
565
+ if (sectionMatch && rawSection !== undefined) {
566
+ let section = rawSection
567
+ .replace(/[ \t]*# openlimiter managed\r?\n?/g, "")
568
+ .replace(/[ \t]*status_line\s*=[\s\S]*?\]\r?\n?/g, "")
569
+ .replace(/[ \t]*status_line_use_colors\s*=\s*(?:true|false)\r?\n?/g, "");
570
+ const lines = section.split(/\r?\n/);
571
+ const header = lines[0] ?? "";
572
+ const rest = lines.slice(1).filter((l) => l.trim().length > 0);
573
+ const newSection = [header, ...codexManagedLines, ...rest].join("\n");
574
+ const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
575
+ updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
576
+ }
577
+ else {
578
+ const newSection = ["[tui]", ...codexManagedLines].join("\n");
579
+ updated = text ? text.trimEnd() + "\n\n" + newSection + "\n" : newSection + "\n";
580
+ }
581
+ if (!updated.endsWith("\n")) {
582
+ updated += "\n";
583
+ }
584
+ try {
585
+ await writeTextFile(configFile, updated);
586
+ return { ok: true, message: "Wired Codex status line." };
587
+ }
588
+ catch {
589
+ return { ok: false, message: "Could not write Codex config." };
590
+ }
591
+ }
592
+ export async function uninstallCodex(context) {
593
+ const configFile = codexConfigPath(context.homeDirectory);
594
+ const readRes = await readTomlConfig(configFile);
595
+ if (readRes.kind === "parse_error") {
596
+ return { ok: false, message: `Could not read ${configFile}, fix it or move it aside` };
597
+ }
598
+ if (readRes.kind === "missing") {
599
+ return { ok: true, message: "Codex status line is not installed." };
600
+ }
601
+ const text = readRes.data;
602
+ const sectionMatch = /(?:^|\n)(\[tui\][\s\S]*?)(?=\n\[|$)/.exec(text);
603
+ if (!sectionMatch || sectionMatch[1] === undefined) {
604
+ return { ok: true, message: "Codex status line is not installed." };
605
+ }
606
+ let section = sectionMatch[1];
607
+ section = section
608
+ .replace(/[ \t]*# openlimiter managed\r?\n?/g, "")
609
+ .replace(/[ \t]*status_line\s*=[\s\S]*?\]\r?\n?/g, "")
610
+ .replace(/[ \t]*status_line_use_colors\s*=\s*(?:true|false)\r?\n?/g, "");
611
+ const lines = section.split(/\r?\n/);
612
+ const rest = lines.slice(1).filter((l) => l.trim().length > 0);
613
+ let updated;
614
+ const startIndex = sectionMatch.index + (text[sectionMatch.index] === "\n" ? 1 : 0);
615
+ if (rest.length === 0) {
616
+ const before = text.slice(0, startIndex).trimEnd();
617
+ const after = text.slice(sectionMatch.index + sectionMatch[0].length).trimStart();
618
+ updated = before && after ? before + "\n\n" + after : (before || after ? (before || after) + "\n" : "");
619
+ }
620
+ else {
621
+ const newSection = [lines[0] ?? "", ...rest].join("\n");
622
+ updated = text.slice(0, startIndex) + newSection + text.slice(sectionMatch.index + sectionMatch[0].length);
623
+ }
624
+ try {
625
+ await writeTextFile(configFile, updated);
626
+ return { ok: true, message: "Uninstalled Codex status line." };
627
+ }
628
+ catch {
629
+ return { ok: false, message: "Could not update Codex config." };
630
+ }
631
+ }
632
+ export const SHELL_SNIPPETS = {
633
+ starship: [
634
+ "[custom.openlimiter]",
635
+ 'command = "openlimiter statusline --host shell"',
636
+ 'when = "true"',
637
+ 'shell = ["bash", "--noprofile", "--norc"]',
638
+ 'format = "[$output]($style) "'
639
+ ].join("\n"),
640
+ ohMyPosh: [
641
+ "{",
642
+ ' "type": "command",',
643
+ ' "properties": {',
644
+ ' "command": "openlimiter statusline --host shell",',
645
+ ' "cache": { "duration": "60s" }',
646
+ " }",
647
+ "}"
648
+ ].join("\n"),
649
+ tmux: "set -g status-right '#(openlimiter statusline --host shell)'\nset -g status-interval 60",
650
+ powerShell: [
651
+ "# OpenLimiter status line snippet",
652
+ "function prompt {",
653
+ " $bar = openlimiter statusline --host shell",
654
+ " if ($bar) { Write-Host $bar }",
655
+ " \"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) \"",
656
+ "}"
657
+ ].join("\n")
658
+ };
659
+ /**
660
+ * Install shell prompt integration
661
+ */
662
+ export async function installShell(context) {
663
+ const profileFile = await resolvePowerShellProfilePath(context.homeDirectory, context.platform, context.shellRunner);
664
+ const existing = (await readTextFile(profileFile)) ?? "";
665
+ if (!existing.includes("openlimiter statusline")) {
666
+ const toAppend = "\n\n" + SHELL_SNIPPETS.powerShell + "\n";
667
+ try {
668
+ await writeTextFile(profileFile, existing + toAppend);
669
+ }
670
+ catch {
671
+ // Writing profile is optional, snippets are still printed
672
+ }
673
+ }
674
+ const message = [
675
+ "Wired shell prompt integration.",
676
+ "",
677
+ "Starship snippet (~/.config/starship.toml):",
678
+ SHELL_SNIPPETS.starship,
679
+ "",
680
+ "tmux snippet (~/.tmux.conf):",
681
+ SHELL_SNIPPETS.tmux,
682
+ "",
683
+ "Oh My Posh segment:",
684
+ SHELL_SNIPPETS.ohMyPosh
685
+ ].join("\n");
686
+ return { ok: true, message };
687
+ }
688
+ export async function uninstallShell(context) {
689
+ const profileFile = await resolvePowerShellProfilePath(context.homeDirectory, context.platform, context.shellRunner);
690
+ const existing = await readTextFile(profileFile);
691
+ if (existing && existing.includes("openlimiter statusline")) {
692
+ const updated = existing
693
+ .replace(/# OpenLimiter status line snippet[\s\S]*?^}/m, "")
694
+ .trimEnd() + "\n";
695
+ try {
696
+ await writeTextFile(profileFile, updated);
697
+ }
698
+ catch {
699
+ // Ignored
700
+ }
701
+ }
702
+ return { ok: true, message: "Uninstalled shell prompt integration." };
703
+ }
704
+ /**
705
+ * The command a Claude Code or Antigravity style status line field carries,
706
+ * whichever of the two shapes it was written in.
707
+ *
708
+ * `null` means the field is absent, which is the only case that counts as
709
+ * nothing being wired at all.
710
+ */
711
+ function claudeLikeStatusLineCommand(value) {
712
+ if (typeof value === "string")
713
+ return value;
714
+ if (typeof value === "object" && value !== null) {
715
+ const command = value["command"];
716
+ if (typeof command === "string")
717
+ return command;
718
+ }
719
+ return null;
720
+ }
721
+ /** Wired, somebody else's, or nothing there, from the command alone. */
722
+ function classifyCommand(command) {
723
+ if (command === null)
724
+ return STATUS_NOT_WIRED;
725
+ return command.includes("openlimiter statusline") ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
726
+ }
727
+ /** The command inside Grok's `[ui.status_line]` table, or nothing found. */
728
+ function grokStatusLineCommand(text) {
729
+ if (text === null)
730
+ return null;
731
+ const sectionMatch = /(?:^|\n)(\[ui\.status_line\][\s\S]*?)(?=\n\[|$)/.exec(text);
732
+ const section = sectionMatch?.[1];
733
+ if (section === undefined)
734
+ return null;
735
+ const commandMatch = /command\s*=\s*"([^"]+)"/.exec(section);
736
+ /* The table exists whether or not it carries a command line this build can
737
+ read, and an existing table with no readable command is still somebody
738
+ else's, not nothing. An empty string is never confused with absence. */
739
+ return commandMatch?.[1] ?? "";
740
+ }
741
+ /**
742
+ * Wired, somebody else's, or nothing there, for Codex's `[tui]` table.
743
+ *
744
+ * Codex has no free form command to inspect: the table names a fixed list of
745
+ * built in items instead. So this reads the comment install leaves behind
746
+ * rather than a command string, and falls back to the presence of the
747
+ * `status_line` key itself for a table this build did not write.
748
+ */
749
+ function classifyCodexSection(text) {
750
+ if (text === null)
751
+ return STATUS_NOT_WIRED;
752
+ const sectionMatch = /(?:^|\n)(\[tui\][\s\S]*?)(?=\n\[|$)/.exec(text);
753
+ const section = sectionMatch?.[1];
754
+ if (section === undefined)
755
+ return STATUS_NOT_WIRED;
756
+ if (section.includes("# openlimiter managed"))
757
+ return STATUS_WIRED;
758
+ return /status_line\s*=/.test(section) ? STATUS_OWN_LINE_FOUND : STATUS_NOT_WIRED;
759
+ }
760
+ /**
761
+ * Check wiring status for a host.
762
+ *
763
+ * Three states for every host that can carry somebody else's status line:
764
+ * ours is wired, somebody else's is already there and install would wrap it
765
+ * rather than overwrite it, or nothing is there at all. The shell host has no
766
+ * wrap to offer, install only ever appends our own snippet, so it stays a
767
+ * plain wired or not.
768
+ */
769
+ export async function hostStatus(host, context) {
770
+ const h = host.toLowerCase();
771
+ if (h === "gemini" || h === "opencode" || h === "kimi") {
772
+ return UNSUPPORTED_HOST_ALTERNATIVE;
773
+ }
774
+ if (h === "claude") {
775
+ const settings = await readJsonFile(claudeSettingsPath(context.homeDirectory));
776
+ return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
777
+ }
778
+ if (h === "antigravity") {
779
+ const settings = await readJsonFile(antigravitySettingsPath(context.homeDirectory));
780
+ return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
781
+ }
782
+ if (h === "grok") {
783
+ const text = await readTextFile(grokConfigPath(context.homeDirectory));
784
+ return classifyCommand(grokStatusLineCommand(text));
785
+ }
786
+ if (h === "codex") {
787
+ const text = await readTextFile(codexConfigPath(context.homeDirectory));
788
+ return classifyCodexSection(text);
789
+ }
790
+ if (h === "shell") {
791
+ const profileFile = await resolvePowerShellProfilePath(context.homeDirectory, context.platform, context.shellRunner);
792
+ const text = await readTextFile(profileFile);
793
+ if (text && text.includes("openlimiter statusline")) {
794
+ return STATUS_WIRED;
795
+ }
796
+ return STATUS_NOT_WIRED;
797
+ }
798
+ return STATUS_NOT_WIRED;
799
+ }
800
+ /**
801
+ * Dispatch installer for a host
802
+ */
803
+ export async function installHost(host, context) {
804
+ const h = host.toLowerCase();
805
+ if (h === "claude")
806
+ return await installClaude(context);
807
+ if (h === "antigravity")
808
+ return await installAntigravity(context);
809
+ if (h === "grok")
810
+ return await installGrok(context);
811
+ if (h === "codex")
812
+ return await installCodex(context);
813
+ if (h === "shell")
814
+ return await installShell(context);
815
+ return {
816
+ ok: false,
817
+ message: "Unknown host. Supported hosts: " + TERMINAL_HOST_NAMES.join(", ") + "."
818
+ };
819
+ }
820
+ /**
821
+ * Dispatch uninstaller for a host
822
+ */
823
+ export async function uninstallHost(host, context) {
824
+ const h = host.toLowerCase();
825
+ if (h === "claude")
826
+ return await uninstallClaude(context);
827
+ if (h === "antigravity")
828
+ return await uninstallAntigravity(context);
829
+ if (h === "grok")
830
+ return await uninstallGrok(context);
831
+ if (h === "codex")
832
+ return await uninstallCodex(context);
833
+ if (h === "shell")
834
+ return await uninstallShell(context);
835
+ return {
836
+ ok: false,
837
+ message: "Unknown host. Supported hosts: " + TERMINAL_HOST_NAMES.join(", ") + "."
838
+ };
839
+ }
840
+ /**
841
+ * Terminal status table
842
+ */
843
+ export async function terminalStatusTable(context) {
844
+ const displayHosts = [
845
+ "Claude",
846
+ "Antigravity",
847
+ "Grok",
848
+ "Codex",
849
+ "Gemini",
850
+ "OpenCode",
851
+ "Kimi",
852
+ "Shell"
853
+ ];
854
+ const rows = [];
855
+ for (const name of displayHosts) {
856
+ const status = await hostStatus(name, context);
857
+ rows.push(`${name}: ${status}`);
858
+ }
859
+ return rows.join("\n");
860
+ }
861
+ /**
862
+ * The stored configuration, or the defaults, and never a refusal.
863
+ *
864
+ * No configuration file yet is the ordinary state of a machine that has
865
+ * never run `init` or `config set`, not a failure: `terminal show` and
866
+ * `terminal hide` have to work on it the same way every other command in
867
+ * this file reads its configuration (config.ts, `readStatuslineConfig`).
868
+ * `detectedProviders` on the context is what the CLI dispatcher always
869
+ * supplies from a fresh environment scan, so an empty connector list here
870
+ * costs nothing in the one path that matters.
871
+ */
872
+ async function loadTerminalConfig(context) {
873
+ const readRes = await readConfig(context.stateDirectory);
874
+ if (readRes.ok)
875
+ return readRes.config;
876
+ return {
877
+ version: 1,
878
+ connectors: [],
879
+ statusline: DEFAULT_STATUSLINE,
880
+ providers: DEFAULT_PROVIDERS
881
+ };
882
+ }
883
+ /**
884
+ * Toggle terminal show providers
885
+ */
886
+ export async function terminalShow(providerIds, context) {
887
+ const config = await loadTerminalConfig(context);
888
+ const detected = new Set((context.detectedProviders ?? config.connectors.filter((c) => c.detected).map((c) => c.id)).map((id) => id.toLowerCase()));
889
+ const invalid = [];
890
+ for (const id of providerIds) {
891
+ const lower = id.toLowerCase();
892
+ if (!detected.has(lower)) {
893
+ invalid.push(lower);
894
+ }
895
+ }
896
+ if (invalid.length > 0) {
897
+ return {
898
+ ok: false,
899
+ message: CONNECT_FIRST_SENTENCE
900
+ };
901
+ }
902
+ const currentShow = [...config.statusline.show];
903
+ for (const id of providerIds) {
904
+ const lower = id.toLowerCase();
905
+ if (!currentShow.includes(lower)) {
906
+ currentShow.push(lower);
907
+ }
908
+ }
909
+ const updatedConfig = {
910
+ ...config,
911
+ statusline: {
912
+ ...config.statusline,
913
+ show: currentShow
914
+ }
915
+ };
916
+ try {
917
+ await writeConfig(updatedConfig, context.stateDirectory);
918
+ return {
919
+ ok: true,
920
+ message: "Showing in terminal: " + currentShow.join(", ") + "."
921
+ };
922
+ }
923
+ catch {
924
+ return { ok: false, message: "Could not write configuration." };
925
+ }
926
+ }
927
+ /**
928
+ * Toggle terminal hide providers
929
+ */
930
+ export async function terminalHide(providerIds, context) {
931
+ const config = await loadTerminalConfig(context);
932
+ const detected = new Set((context.detectedProviders ?? config.connectors.filter((c) => c.detected).map((c) => c.id)).map((id) => id.toLowerCase()));
933
+ const invalid = [];
934
+ for (const id of providerIds) {
935
+ const lower = id.toLowerCase();
936
+ if (!detected.has(lower)) {
937
+ invalid.push(lower);
938
+ }
939
+ }
940
+ if (invalid.length > 0) {
941
+ return {
942
+ ok: false,
943
+ message: CONNECT_FIRST_SENTENCE
944
+ };
945
+ }
946
+ let currentShow;
947
+ if (config.statusline.show.length === 0) {
948
+ const allProviders = connectors.map((c) => c.id);
949
+ currentShow = allProviders.filter((id) => !providerIds.map((p) => p.toLowerCase()).includes(id.toLowerCase()));
950
+ }
951
+ else {
952
+ currentShow = config.statusline.show.filter((id) => !providerIds.map((p) => p.toLowerCase()).includes(id.toLowerCase()));
953
+ }
954
+ const updatedConfig = {
955
+ ...config,
956
+ statusline: {
957
+ ...config.statusline,
958
+ show: currentShow
959
+ }
960
+ };
961
+ try {
962
+ await writeConfig(updatedConfig, context.stateDirectory);
963
+ return {
964
+ ok: true,
965
+ message: currentShow.length === 0
966
+ ? "No providers shown in terminal."
967
+ : "Showing in terminal: " + currentShow.join(", ") + "."
968
+ };
969
+ }
970
+ catch {
971
+ return { ok: false, message: "Could not write configuration." };
972
+ }
973
+ }
974
+ //# sourceMappingURL=terminal.js.map