openlimiter 1.3.1 → 1.3.3

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 (52) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +130 -78
  4. package/dist/cli.js.map +1 -1
  5. package/dist/codex-device-login.d.ts +2 -1
  6. package/dist/codex-device-login.d.ts.map +1 -1
  7. package/dist/codex-device-login.js +6 -4
  8. package/dist/codex-device-login.js.map +1 -1
  9. package/dist/config.d.ts +1 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +3 -2
  12. package/dist/config.js.map +1 -1
  13. package/dist/hub-auth.d.ts +12 -14
  14. package/dist/hub-auth.d.ts.map +1 -1
  15. package/dist/hub-auth.js +232 -47
  16. package/dist/hub-auth.js.map +1 -1
  17. package/dist/hub-sync.d.ts +14 -13
  18. package/dist/hub-sync.d.ts.map +1 -1
  19. package/dist/hub-sync.js +139 -18
  20. package/dist/hub-sync.js.map +1 -1
  21. package/dist/hub.d.ts +6 -3
  22. package/dist/hub.d.ts.map +1 -1
  23. package/dist/hub.js +47 -11
  24. package/dist/hub.js.map +1 -1
  25. package/dist/ingest.d.ts.map +1 -1
  26. package/dist/ingest.js +16 -21
  27. package/dist/ingest.js.map +1 -1
  28. package/dist/session.d.ts +16 -8
  29. package/dist/session.d.ts.map +1 -1
  30. package/dist/session.js +144 -31
  31. package/dist/session.js.map +1 -1
  32. package/dist/statusline.d.ts +1 -1
  33. package/dist/statusline.d.ts.map +1 -1
  34. package/dist/statusline.js +10 -8
  35. package/dist/statusline.js.map +1 -1
  36. package/dist/terminal-backup.d.ts +5 -0
  37. package/dist/terminal-backup.d.ts.map +1 -0
  38. package/dist/terminal-backup.js +88 -0
  39. package/dist/terminal-backup.js.map +1 -0
  40. package/dist/terminal-launcher.d.ts +12 -0
  41. package/dist/terminal-launcher.d.ts.map +1 -0
  42. package/dist/terminal-launcher.js +208 -0
  43. package/dist/terminal-launcher.js.map +1 -0
  44. package/dist/terminal-toml.d.ts +38 -0
  45. package/dist/terminal-toml.d.ts.map +1 -0
  46. package/dist/terminal-toml.js +357 -0
  47. package/dist/terminal-toml.js.map +1 -0
  48. package/dist/terminal.d.ts +10 -37
  49. package/dist/terminal.d.ts.map +1 -1
  50. package/dist/terminal.js +390 -619
  51. package/dist/terminal.js.map +1 -1
  52. package/package.json +4 -4
package/dist/terminal.js CHANGED
@@ -1,9 +1,12 @@
1
- import { readFile, mkdir } from "node:fs/promises";
1
+ import { lstat, readFile, mkdir, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { connectors } from "@openlimiter/connectors";
4
4
  import { writeFileAtomically } from "@openlimiter/core";
5
5
  import { DEFAULT_PROVIDERS, DEFAULT_STATUSLINE, readConfig, writeConfig } from "./config.js";
6
6
  import { decodeWrappedStatuslineCommand, encodeWrappedStatuslineCommand } from "./statusline-wrapper.js";
7
+ import { parseToml, editToml, tomlValue } from "./terminal-toml.js";
8
+ import { installLauncher, launcherCommand } from "./terminal-launcher.js";
9
+ import { isOwned, readOptional, restoreOwned, writeOwned } from "./terminal-backup.js";
7
10
  export const TERMINAL_HOST_NAMES = [
8
11
  "claude",
9
12
  "antigravity",
@@ -25,63 +28,73 @@ export const STATUS_WIRED = "Wired";
25
28
  export const STATUS_OWN_LINE_FOUND = "Your own status line found, install wraps it";
26
29
  export const STATUS_NOT_WIRED = "Not wired";
27
30
  export const CONNECT_FIRST_SENTENCE = "Connect it first";
28
- function claudeSettingsPath(home) {
29
- return path.join(home, ".claude", "settings.json");
31
+ async function canonicalOverrideRoot(value, fallback, variable) {
32
+ if (value === undefined || value === "")
33
+ return fallback;
34
+ if (!path.isAbsolute(value))
35
+ throw new Error(`${variable} must be an absolute path.`);
36
+ let existing = null;
37
+ try {
38
+ existing = await lstat(value);
39
+ }
40
+ catch (error) {
41
+ if (error.code !== "ENOENT")
42
+ throw error;
43
+ }
44
+ if (existing?.isSymbolicLink())
45
+ return await realpath(value);
46
+ try {
47
+ return await realpath(value);
48
+ }
49
+ catch (error) {
50
+ if (error.code !== "ENOENT")
51
+ throw error;
52
+ const parent = await realpath(path.dirname(value));
53
+ return path.join(parent, path.basename(value));
54
+ }
55
+ }
56
+ function isWithinDirectory(root, target) {
57
+ const relative = path.relative(path.resolve(root), path.resolve(target));
58
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
59
+ }
60
+ async function canonicalConfigFile(root, name, variable) {
61
+ const candidate = path.join(root, name);
62
+ try {
63
+ const target = await realpath(candidate);
64
+ if (!isWithinDirectory(root, target))
65
+ throw new Error(`${variable} must point to a file under its root.`);
66
+ return target;
67
+ }
68
+ catch (error) {
69
+ if (error.code !== "ENOENT")
70
+ throw error;
71
+ return candidate;
72
+ }
30
73
  }
31
- function antigravitySettingsPath(home) {
32
- return path.join(home, ".gemini", "antigravity-cli", "settings.json");
74
+ async function claudeSettingsPath(context) {
75
+ const root = await canonicalOverrideRoot(context.environment?.["CLAUDE_CONFIG_DIR"], path.join(context.homeDirectory, ".claude"), "CLAUDE_CONFIG_DIR");
76
+ return await canonicalConfigFile(root, "settings.json", "CLAUDE_CONFIG_DIR");
33
77
  }
34
- function grokConfigPath(home) {
35
- return path.join(home, ".grok", "config.toml");
78
+ function antigravitySettingsPath(context) {
79
+ return path.join(context.homeDirectory, ".gemini", "antigravity-cli", "settings.json");
36
80
  }
37
- function codexConfigPath(home) {
38
- return path.join(home, ".codex", "config.toml");
81
+ function grokConfigPath(context) {
82
+ return path.join(context.homeDirectory, ".grok", "config.toml");
39
83
  }
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");
84
+ async function codexConfigPath(context) {
85
+ const root = await canonicalOverrideRoot(context.environment?.["CODEX_HOME"], path.join(context.homeDirectory, ".codex"), "CODEX_HOME");
86
+ return await canonicalConfigFile(root, "config.toml", "CODEX_HOME");
46
87
  }
47
88
  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;
89
+ /** Ask the selected PowerShell, including redirected Documents and PS 7 layouts.
90
+ * A shell that cannot identify its active profile is not a verified target. */
91
+ async function resolvePowerShellProfilePath(executable, runner) {
92
+ if (!runner)
93
+ throw new Error("Unsupported shell");
94
+ const result = await runner(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PROFILE"], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
95
+ if (!result.ok || !path.isAbsolute(result.stdout.trim()))
96
+ throw new Error("Unsupported shell");
97
+ return result.stdout.trim();
85
98
  }
86
99
  export async function readJsonConfig(filePath) {
87
100
  try {
@@ -108,114 +121,13 @@ export async function readJsonConfig(filePath) {
108
121
  }
109
122
  }
110
123
  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
- }
124
+ try {
125
+ parseToml(text);
126
+ return true;
213
127
  }
214
- if (inSingleTriple || inDoubleTriple)
215
- return false;
216
- if (bracketDepth !== 0 || braceDepth !== 0)
128
+ catch {
217
129
  return false;
218
- return true;
130
+ }
219
131
  }
220
132
  export async function readTomlConfig(filePath) {
221
133
  try {
@@ -239,10 +151,6 @@ async function readJsonFile(filePath) {
239
151
  const res = await readJsonConfig(filePath);
240
152
  return res.kind === "ok" ? res.data : null;
241
153
  }
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
154
  async function readTextFile(filePath) {
247
155
  try {
248
156
  return await readFile(filePath, "utf8");
@@ -251,455 +159,289 @@ async function readTextFile(filePath) {
251
159
  return null;
252
160
  }
253
161
  }
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"];
162
+ const HOST_CONFIG = {
163
+ claude: { path: claudeSettingsPath, label: "Claude Code", file: "Claude Code settings", json: true, wired: "Wired Claude Code status line." },
164
+ antigravity: { path: antigravitySettingsPath, label: "Antigravity", file: "Antigravity settings", json: true, wired: "Wired Antigravity CLI status line." },
165
+ grok: { path: grokConfigPath, label: "Grok", file: "Grok config", json: false, wired: "Wired Grok Build status line." },
166
+ codex: { path: codexConfigPath, label: "Codex", file: "Codex config", json: false, wired: "Wired Codex status line." }
167
+ };
168
+ const CODEX_ITEMS = ["five-hour-limit", "weekly-limit", "context-used", "model-with-reasoning", "current-dir"];
169
+ async function durableCommand(context, shell) {
170
+ const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
171
+ return launcherCommand(runtime, shell);
172
+ }
173
+ function ownedMarker(text, json) {
174
+ return json ? JSON.parse(text)["openlimiter managed"] === true
175
+ : text.split(/\r?\n/).some(line => line.trim() === "# openlimiter managed");
176
+ }
177
+ async function changeConfigHost(host, context, install) {
178
+ const spec = HOST_CONFIG[host];
179
+ let file;
338
180
  try {
339
- await writeJsonFile(settingsFile, existing);
340
- return { ok: true, message: "Uninstalled Claude Code status line." };
181
+ file = await spec.path(context);
341
182
  }
342
- catch {
343
- return { ok: false, message: "Could not update Claude Code settings." };
183
+ catch (error) {
184
+ return {
185
+ ok: false,
186
+ message: error instanceof Error ? error.message : `Could not resolve ${spec.file}.`
187
+ };
344
188
  }
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;
189
+ const read = spec.json ? await readJsonConfig(file) : await readTomlConfig(file);
190
+ if (read.kind === "parse_error")
191
+ return { ok: false, message: `Could not read ${file}, fix it or move it aside` };
376
192
  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." };
193
+ const original = await readOptional(file);
194
+ const text = original ?? (spec.json ? "{}" : "");
195
+ const marker = ownedMarker(text, spec.json);
196
+ if (!install) {
197
+ const restored = original !== null && await restoreOwned(file, original, marker);
198
+ if (marker && !restored)
199
+ return { ok: false, message: `Could not update ${file}.` };
200
+ return { ok: true, message: restored ? `Uninstalled ${spec.label} status line.` : `${spec.label} status line is not installed.` };
412
201
  }
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];
202
+ const previous = spec.json
203
+ ? claudeLikeStatusLineCommand(JSON.parse(text)["statusLine"])
204
+ : host === "grok"
205
+ ? (() => {
206
+ const value = tomlValue(text, ["ui", "status_line", "command"]);
207
+ return typeof value === "string" ? value : null;
208
+ })()
209
+ : null;
210
+ const legacyOpenLimiter = previous !== null && isOpenLimiterStatuslineCommand(previous);
211
+ const owned = original !== null && await isOwned(file, original, marker);
212
+ if (marker && !owned && !legacyOpenLimiter)
213
+ return { ok: false, message: `Could not write ${file}.` };
214
+ let updated;
215
+ if (spec.json) {
216
+ const data = JSON.parse(text);
217
+ const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
218
+ const userCommand = previous === null
219
+ ? null
220
+ : legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
221
+ const command = `${base} statusline --host ${host}` +
222
+ (userCommand === null ? "" : ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}`);
223
+ data["openlimiter managed"] = true;
224
+ data["statusLine"] = host === "claude" ? { type: "command", command } : command;
225
+ updated = JSON.stringify(data, null, 2) + "\n";
226
+ JSON.parse(updated);
441
227
  }
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
- }
228
+ else if (host === "codex") {
229
+ updated = editToml(text, ["tui"], { status_line: CODEX_ITEMS, status_line_use_colors: true });
521
230
  }
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." };
231
+ else {
232
+ const base = await durableCommand(context, context.platform === "win32" ? "cmd" : "posix");
233
+ const userCommand = previous === null
234
+ ? null
235
+ : legacyOpenLimiter ? unwrapOpenLimiterStatuslineCommand(previous) : previous;
236
+ const command = `${base} statusline --host grok` +
237
+ (typeof userCommand === "string" ? ` --wrap ${encodeWrappedStatuslineCommand(userCommand)}` : "");
238
+ updated = editToml(text, ["ui", "status_line"], { type: "command", command });
239
+ }
240
+ if (owned && updated === text)
241
+ return { ok: true, message: spec.wired };
242
+ await mkdir(path.dirname(file), { recursive: true });
243
+ await writeOwned(file, original, updated);
244
+ return { ok: true, message: spec.wired };
542
245
  }
543
246
  catch {
544
- return { ok: false, message: "Could not update Grok config." };
545
- }
247
+ return { ok: false, message: `Could not ${install ? "write" : "update"} ${file}.` };
248
+ }
249
+ }
250
+ export const installClaude = (context) => changeConfigHost("claude", context, true);
251
+ export const uninstallClaude = (context) => changeConfigHost("claude", context, false);
252
+ export const installAntigravity = (context) => changeConfigHost("antigravity", context, true);
253
+ export const uninstallAntigravity = (context) => changeConfigHost("antigravity", context, false);
254
+ export const installGrok = (context) => changeConfigHost("grok", context, true);
255
+ export const uninstallGrok = (context) => changeConfigHost("grok", context, false);
256
+ export const installCodex = (context) => changeConfigHost("codex", context, true);
257
+ export const uninstallCodex = (context) => changeConfigHost("codex", context, false);
258
+ function shellSnippets(posixCommand, powerShellCommand) {
259
+ const command = posixCommand + " statusline --host shell";
260
+ return {
261
+ starship: [
262
+ "[custom.openlimiter]", "command = " + JSON.stringify(command),
263
+ 'when = "true"', 'shell = ["bash", "--noprofile", "--norc"]',
264
+ 'format = "[$output]($style) "'
265
+ ].join("\n"),
266
+ tmux: "set -g status-right " + JSON.stringify("#(" + command + ")") + "\nset -g status-interval 60",
267
+ ohMyPosh: JSON.stringify({ type: "command", properties: {
268
+ command: powerShellCommand + " statusline --host shell", shell: "powershell", cache: { duration: "60s" }
269
+ } }, null, 2)
270
+ };
546
271
  }
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" }',
272
+ class UnsupportedShellError extends Error {
273
+ shell;
274
+ constructor(shell) {
275
+ super("Unsupported shell");
276
+ this.shell = shell;
277
+ }
278
+ }
279
+ async function shellTarget(context) {
280
+ const env = context.environment ?? process.env;
281
+ let executable = env["SHELL"] || "";
282
+ if (!executable && context.platform === "win32" && context.shellRunner) {
283
+ // npm can sit between Node and the interactive shell. Walk ancestors until
284
+ // the actual PowerShell or Unix shell is found, rather than trusting COMSPEC
285
+ // (which remains cmd.exe even inside PowerShell).
286
+ const query = "$ancestorId = " + process.ppid + "; for ($n = 0; $n -lt 12 -and $ancestorId; $n++) { " +
287
+ "$p = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $ancestorId); " +
288
+ "if ($p.Name -match '^(pwsh|powershell|bash|zsh)(\\.exe)?$') { $p.ExecutablePath; break }; " +
289
+ "$ancestorId = $p.ParentProcessId }";
290
+ const found = await context.shellRunner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query], POWERSHELL_PROFILE_TIMEOUT_MILLISECONDS);
291
+ if (found.ok)
292
+ executable = found.stdout.trim();
293
+ }
294
+ const name = path.basename(executable).toLowerCase();
295
+ if (name === "bash" || name === "bash.exe") {
296
+ return { kind: "bash", file: path.join(context.homeDirectory, ".bashrc") };
297
+ }
298
+ if (name === "zsh") {
299
+ return { kind: "zsh", file: path.join(env["ZDOTDIR"] || context.homeDirectory, ".zshrc") };
300
+ }
301
+ if (name === "pwsh" || name === "pwsh.exe" || name === "powershell" || name === "powershell.exe") {
302
+ return { kind: "powershell", file: await resolvePowerShellProfilePath(executable, context.shellRunner) };
303
+ }
304
+ if (name !== "")
305
+ throw new UnsupportedShellError(name);
306
+ throw new Error("Unsupported shell");
307
+ }
308
+ function escapedRegExp(text) {
309
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
310
+ }
311
+ function userFunctionExists(text, name, powershell) {
312
+ const escaped = escapedRegExp(name);
313
+ return powershell
314
+ ? new RegExp(`(?:^|\\r?\\n)\\s*function\\s+(?:global:)?${escaped}\\b`, "i").test(text)
315
+ : new RegExp(`(?:^|\\r?\\n)\\s*(?:function\\s+)?${escaped}\\s*(?:\\(\\s*\\))?\\s*\\{`).test(text);
316
+ }
317
+ function uniqueShellName(text, base, powershell) {
318
+ for (let suffix = 0;; suffix += 1) {
319
+ const candidate = suffix === 0 ? base : `${base}_${suffix + 1}`;
320
+ if (!userFunctionExists(text, candidate, powershell))
321
+ return candidate;
322
+ }
323
+ }
324
+ const MANAGED_SHELL_BLOCK = /(^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/g;
325
+ function hasManagedShellBlock(text) {
326
+ return /(?:^|\r?\n)# openlimiter managed\r?\n[\s\S]*?\r?\n# end openlimiter managed/.test(text);
327
+ }
328
+ function withoutManagedShellBlock(text) {
329
+ return text.replace(MANAGED_SHELL_BLOCK, "$1");
330
+ }
331
+ function replaceManagedShellBlock(text, snippet) {
332
+ return text.replace(MANAGED_SHELL_BLOCK, `$1${snippet}`);
333
+ }
334
+ function shellSnippet(kind, command, source = "") {
335
+ const begin = "# openlimiter managed";
336
+ const end = "# end openlimiter managed";
337
+ const hook = uniqueShellName(source, kind === "powershell" ? "__openlimiter_prompt_hook" : "__openlimiter_statusline", kind === "powershell");
338
+ if (kind === "bash")
339
+ return [
340
+ begin,
341
+ `if ! declare -F ${hook} >/dev/null 2>&1; then`,
342
+ ` ${hook}() { ${command} statusline --host shell; }`,
343
+ "fi",
344
+ 'if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then',
345
+ ` [[ " \${PROMPT_COMMAND[*]} " == *" ${hook} "* ]] || PROMPT_COMMAND+=(${hook})`,
346
+ `elif [[ ";\${PROMPT_COMMAND};" != *";${hook};"* ]]; then`,
347
+ ` PROMPT_COMMAND="\${PROMPT_COMMAND:+\${PROMPT_COMMAND};}${hook}"`,
348
+ "fi", end
349
+ ].join("\n");
350
+ if (kind === "zsh")
351
+ return [
352
+ begin,
353
+ `if (( ! $+functions[${hook}] )); then`,
354
+ ` ${hook}() { ${command} statusline --host shell; }`,
355
+ "fi",
356
+ "autoload -Uz add-zsh-hook",
357
+ `if (( \${precmd_functions[(I)${hook}]} == 0 )); then add-zsh-hook precmd ${hook}; fi`, end
358
+ ].join("\n");
359
+ const originalPrompt = uniqueShellName(source, "__openlimiter_original_prompt", true);
360
+ const guard = uniqueShellName(source, "__openlimiter_prompt_guard", true);
361
+ return [
362
+ begin,
363
+ `if (-not (Test-Path Function:global:${originalPrompt})) {`,
364
+ ` $function:global:${originalPrompt} = $function:prompt`,
365
+ "}",
366
+ `if (-not (Test-Path Function:global:${hook})) {`,
367
+ ` function global:${hook} {`,
368
+ ` $bar = ${command} statusline --host shell`,
369
+ " if ($bar) { Write-Host $bar }",
370
+ ` & $function:global:${originalPrompt}`,
646
371
  " }",
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
- */
372
+ "}",
373
+ `if (-not (Test-Path Function:global:${guard})) {`,
374
+ ` function global:${guard} { & $function:global:${hook} }`,
375
+ "}",
376
+ "function global:prompt {",
377
+ ` & $function:global:${guard}`,
378
+ "}", end
379
+ ].join("\n");
380
+ }
381
+ async function manualShellSnippetPath(context) {
382
+ const directory = context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter");
383
+ const file = path.join(directory, "shell-snippet.txt");
384
+ await mkdir(directory, { recursive: true }).then(() => writeFileAtomically(file, "openlimiter statusline --host shell\n")).catch(() => undefined);
385
+ return file;
386
+ }
662
387
  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);
388
+ try {
389
+ const target = await shellTarget(context);
390
+ const original = await readOptional(target.file);
391
+ const runtime = await installLauncher(context.stateDirectory ?? path.join(context.homeDirectory, ".openlimiter"));
392
+ const posixCommand = launcherCommand(runtime, "posix");
393
+ const powerShellCommand = launcherCommand(runtime, "powershell");
394
+ const command = target.kind === "powershell" ? powerShellCommand : posixCommand;
395
+ const snippets = shellSnippets(posixCommand, powerShellCommand);
396
+ const alreadyOwned = original !== null && await isOwned(target.file, original, ownedMarker(original, false));
397
+ const source = withoutManagedShellBlock(original ?? "");
398
+ const snippet = shellSnippet(target.kind, command, source);
399
+ const marker = original !== null && ownedMarker(original, false);
400
+ if (alreadyOwned || (marker && hasManagedShellBlock(original ?? ""))) {
401
+ const updated = replaceManagedShellBlock(original, snippet);
402
+ if (updated !== original) {
403
+ await writeOwned(target.file, original, updated);
404
+ }
669
405
  }
670
- catch {
671
- // Writing profile is optional, snippets are still printed
406
+ else if (marker) {
407
+ return { ok: false, message: `Could not write ${target.file}.` };
408
+ }
409
+ else {
410
+ const updated = (original ?? "") + "\n\n" + snippet + "\n";
411
+ await mkdir(path.dirname(target.file), { recursive: true });
412
+ await writeOwned(target.file, original, updated);
413
+ }
414
+ const message = [
415
+ "Wired shell prompt integration.", "",
416
+ "Starship snippet (~/.config/starship.toml):", snippets.starship, "",
417
+ "tmux snippet (~/.tmux.conf):", snippets.tmux, "",
418
+ "Oh My Posh segment:", snippets.ohMyPosh
419
+ ].join("\n");
420
+ return { ok: true, message };
421
+ }
422
+ catch (error) {
423
+ if (error instanceof UnsupportedShellError) {
424
+ const file = await manualShellSnippetPath(context);
425
+ return { ok: true, message: `Skipped ${error.shell} shell. See ${file} for the manual snippet.` };
672
426
  }
427
+ return { ok: false, message: "Could not write configuration." };
673
428
  }
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
429
  }
688
430
  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
431
+ try {
432
+ const target = await shellTarget(context);
433
+ const original = await readOptional(target.file);
434
+ if (original !== null) {
435
+ const marker = ownedMarker(original, false);
436
+ const restored = await restoreOwned(target.file, original, marker);
437
+ if (marker && !restored)
438
+ return { ok: false, message: `Could not update ${target.file}.` };
700
439
  }
440
+ return { ok: true, message: "Uninstalled shell prompt integration." };
441
+ }
442
+ catch {
443
+ return { ok: false, message: "Could not write configuration." };
701
444
  }
702
- return { ok: true, message: "Uninstalled shell prompt integration." };
703
445
  }
704
446
  /**
705
447
  * The command a Claude Code or Antigravity style status line field carries,
@@ -718,25 +460,35 @@ function claudeLikeStatusLineCommand(value) {
718
460
  }
719
461
  return null;
720
462
  }
463
+ function isOpenLimiterStatuslineCommand(command) {
464
+ return /(?:^|[\\/"'\s])openlimiter(?:\.cjs|\.cmd|\.exe)?(?=$|[\\/"'\s])/i.test(command) &&
465
+ /\bstatusline\s+--host\s+[A-Za-z0-9_-]+\b/i.test(command);
466
+ }
467
+ function unwrapOpenLimiterStatuslineCommand(command) {
468
+ if (!isOpenLimiterStatuslineCommand(command))
469
+ return command;
470
+ const encoded = /\s--wrap\s+([A-Za-z0-9_-]+)/i.exec(command)?.[1];
471
+ return encoded === undefined ? null : decodeWrappedStatuslineCommand(encoded);
472
+ }
721
473
  /** Wired, somebody else's, or nothing there, from the command alone. */
722
474
  function classifyCommand(command) {
723
475
  if (command === null)
724
476
  return STATUS_NOT_WIRED;
725
- return command.includes("openlimiter statusline") ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
477
+ return isOpenLimiterStatuslineCommand(command) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
726
478
  }
727
479
  /** The command inside Grok's `[ui.status_line]` table, or nothing found. */
728
480
  function grokStatusLineCommand(text) {
729
481
  if (text === null)
730
482
  return null;
731
- const sectionMatch = /(?:^|\n)(\[ui\.status_line\][\s\S]*?)(?=\n\[|$)/.exec(text);
732
- const section = sectionMatch?.[1];
733
- if (section === undefined)
483
+ try {
484
+ const parsed = parseToml(text);
485
+ const command = tomlValue(text, ["ui", "status_line", "command"]);
486
+ return typeof command === "string" ? command
487
+ : parsed.tables.some(t => JSON.stringify(t.keys) === '["ui","status_line"]') ? "" : null;
488
+ }
489
+ catch {
734
490
  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] ?? "";
491
+ }
740
492
  }
741
493
  /**
742
494
  * Wired, somebody else's, or nothing there, for Codex's `[tui]` table.
@@ -749,13 +501,14 @@ function grokStatusLineCommand(text) {
749
501
  function classifyCodexSection(text) {
750
502
  if (text === null)
751
503
  return STATUS_NOT_WIRED;
752
- const sectionMatch = /(?:^|\n)(\[tui\][\s\S]*?)(?=\n\[|$)/.exec(text);
753
- const section = sectionMatch?.[1];
754
- if (section === undefined)
504
+ try {
505
+ if (tomlValue(text, ["tui", "status_line"]) === undefined)
506
+ return STATUS_NOT_WIRED;
507
+ return ownedMarker(text, false) ? STATUS_WIRED : STATUS_OWN_LINE_FOUND;
508
+ }
509
+ catch {
755
510
  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;
511
+ }
759
512
  }
760
513
  /**
761
514
  * Check wiring status for a host.
@@ -772,28 +525,44 @@ export async function hostStatus(host, context) {
772
525
  return UNSUPPORTED_HOST_ALTERNATIVE;
773
526
  }
774
527
  if (h === "claude") {
775
- const settings = await readJsonFile(claudeSettingsPath(context.homeDirectory));
528
+ let settingsPath;
529
+ try {
530
+ settingsPath = await claudeSettingsPath(context);
531
+ }
532
+ catch {
533
+ return STATUS_NOT_WIRED;
534
+ }
535
+ const settings = await readJsonFile(settingsPath);
776
536
  return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
777
537
  }
778
538
  if (h === "antigravity") {
779
- const settings = await readJsonFile(antigravitySettingsPath(context.homeDirectory));
539
+ const settings = await readJsonFile(antigravitySettingsPath(context));
780
540
  return classifyCommand(settings ? claudeLikeStatusLineCommand(settings["statusLine"]) : null);
781
541
  }
782
542
  if (h === "grok") {
783
- const text = await readTextFile(grokConfigPath(context.homeDirectory));
543
+ const text = await readTextFile(grokConfigPath(context));
784
544
  return classifyCommand(grokStatusLineCommand(text));
785
545
  }
786
546
  if (h === "codex") {
787
- const text = await readTextFile(codexConfigPath(context.homeDirectory));
547
+ let configPath;
548
+ try {
549
+ configPath = await codexConfigPath(context);
550
+ }
551
+ catch {
552
+ return STATUS_NOT_WIRED;
553
+ }
554
+ const text = await readTextFile(configPath);
788
555
  return classifyCodexSection(text);
789
556
  }
790
557
  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;
558
+ try {
559
+ const target = await shellTarget(context);
560
+ const text = await readOptional(target.file);
561
+ return text !== null && await isOwned(target.file, text, ownedMarker(text, false)) ? STATUS_WIRED : STATUS_NOT_WIRED;
562
+ }
563
+ catch {
564
+ return STATUS_NOT_WIRED;
795
565
  }
796
- return STATUS_NOT_WIRED;
797
566
  }
798
567
  return STATUS_NOT_WIRED;
799
568
  }
@@ -910,7 +679,8 @@ export async function terminalShow(providerIds, context) {
910
679
  ...config,
911
680
  statusline: {
912
681
  ...config.statusline,
913
- show: currentShow
682
+ show: currentShow,
683
+ showMode: "explicit"
914
684
  }
915
685
  };
916
686
  try {
@@ -944,7 +714,7 @@ export async function terminalHide(providerIds, context) {
944
714
  };
945
715
  }
946
716
  let currentShow;
947
- if (config.statusline.show.length === 0) {
717
+ if (config.statusline.show.length === 0 && config.statusline.showMode !== "explicit") {
948
718
  const allProviders = connectors.map((c) => c.id);
949
719
  currentShow = allProviders.filter((id) => !providerIds.map((p) => p.toLowerCase()).includes(id.toLowerCase()));
950
720
  }
@@ -955,7 +725,8 @@ export async function terminalHide(providerIds, context) {
955
725
  ...config,
956
726
  statusline: {
957
727
  ...config.statusline,
958
- show: currentShow
728
+ show: currentShow,
729
+ showMode: "explicit"
959
730
  }
960
731
  };
961
732
  try {