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.
- package/README.md +45 -16
- package/dist/bin.js +53 -4
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +110 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1064 -64
- package/dist/cli.js.map +1 -1
- package/dist/codex-device-login.d.ts +87 -0
- package/dist/codex-device-login.d.ts.map +1 -0
- package/dist/codex-device-login.js +291 -0
- package/dist/codex-device-login.js.map +1 -0
- package/dist/config.d.ts +44 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +129 -7
- package/dist/config.js.map +1 -1
- package/dist/hub-auth.d.ts +137 -0
- package/dist/hub-auth.d.ts.map +1 -0
- package/dist/hub-auth.js +280 -0
- package/dist/hub-auth.js.map +1 -0
- package/dist/hub-sync.d.ts +144 -0
- package/dist/hub-sync.d.ts.map +1 -0
- package/dist/hub-sync.js +370 -0
- package/dist/hub-sync.js.map +1 -0
- package/dist/hub.d.ts +60 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +178 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +26 -0
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +116 -1
- package/dist/ingest.js.map +1 -1
- package/dist/qr.d.ts.map +1 -1
- package/dist/qr.js +1 -0
- package/dist/qr.js.map +1 -1
- package/dist/session.d.ts +46 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +121 -0
- package/dist/session.js.map +1 -0
- package/dist/statusline.d.ts +14 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +258 -19
- package/dist/statusline.js.map +1 -1
- package/dist/terminal.d.ts +109 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +974 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +4 -4
- package/dist/serve.d.ts +0 -170
- package/dist/serve.d.ts.map +0 -1
- package/dist/serve.js +0 -533
- package/dist/serve.js.map +0 -1
package/dist/terminal.js
ADDED
|
@@ -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
|