openlimiter 0.2.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import { type ConnectionStatus, type ProviderCode } from "@openlimiter/core";
2
+ /** The environment variable that turns this on. Nothing else does. */
3
+ export declare const LIVE_ENVIRONMENT_FLAG = "OPENLIMITER_LIVE";
4
+ /** What the command prints when the flag is absent. */
5
+ export declare const SKIPPED_NOTE: string;
6
+ /**
7
+ * Where a person points this at a payload they captured themselves.
8
+ *
9
+ * `OPENLIMITER_SMOKE_PAYLOAD_CLAUDE=/path/to/statusline.json` hands that file
10
+ * to the Claude reader. This is how a live response reaches the real parser
11
+ * without this module ever making the call: the person makes it, with their own
12
+ * credential, in their own shell, and hands over the result.
13
+ */
14
+ export declare const PAYLOAD_ENVIRONMENT_PREFIX = "OPENLIMITER_SMOKE_PAYLOAD_";
15
+ /** Where the evidence goes, when a person wants it somewhere else. */
16
+ export declare const EVIDENCE_DIRECTORY_ENVIRONMENT = "OPENLIMITER_EVIDENCE_DIR";
17
+ /** Largest payload file this harness will read. */
18
+ export declare const MAX_PAYLOAD_BYTES = 262144;
19
+ /** The first thing that must not be here, or null when the text is clean. */
20
+ export declare function containsSecret(text: string): string | null;
21
+ /**
22
+ * The directory the evidence is written into.
23
+ *
24
+ * The mission workspace, `launch-2026-09-01/providers`, which sits beside the
25
+ * checkout rather than inside it. It is found by walking up from the repository
26
+ * root, because a worktree may be checked out at `launch-2026-09-01/wt/<lane>`
27
+ * or beside the workspace, and a fixed number of parent steps is right in one
28
+ * of those layouts and silently wrong in the other. The environment variable
29
+ * wins over both, for a person who wants the file somewhere else entirely.
30
+ */
31
+ export declare function evidenceDirectory(repositoryRoot: string, environment?: Readonly<Record<string, string | undefined>>): string;
32
+ /** The evidence file for one provider on one day. */
33
+ export declare function evidenceFileName(provider: ProviderCode, day: string): string;
34
+ /** The calendar day an instant falls on, in UTC, so a file name never drifts. */
35
+ export declare function evidenceDay(now: string): string;
36
+ export interface SmokeMeter {
37
+ readonly meter: string;
38
+ readonly value: number;
39
+ readonly unit: string;
40
+ readonly resetAt: string | null;
41
+ readonly state: "fresh" | "stale" | "unknown";
42
+ readonly source: string;
43
+ readonly precision: string;
44
+ }
45
+ export interface SmokeProvider {
46
+ readonly provider: ProviderCode;
47
+ readonly connector: string;
48
+ readonly maturity: "stable" | "beta";
49
+ readonly detected: boolean;
50
+ /** Where the reading came from: a payload file, the local cache, or nothing. */
51
+ readonly via: "payload_file" | "snapshot_cache" | "none";
52
+ readonly connection: ConnectionStatus | null;
53
+ readonly meters: readonly SmokeMeter[];
54
+ }
55
+ /**
56
+ * One printed line per provider per meter, in the shape a human scans.
57
+ *
58
+ * Percent and reset, because those are the two things a person is checking, and
59
+ * nothing else, because every other field on a snapshot is either a constant or
60
+ * a thing that could carry an identity.
61
+ */
62
+ export declare function smokeLine(entry: SmokeProvider, meter: SmokeMeter): string;
63
+ /** The line for a provider that produced nothing, which is still a result. */
64
+ export declare function silentLine(entry: SmokeProvider): string;
65
+ /**
66
+ * The evidence document, with nothing in it that could name a person.
67
+ *
68
+ * Built by naming every field that goes IN rather than by deleting fields that
69
+ * must stay out. An allow list cannot be defeated by a provider adding a field,
70
+ * and a deny list can, which is the whole argument.
71
+ */
72
+ export declare function sanitizeEvidence(entry: SmokeProvider, now: string): Record<string, unknown>;
73
+ export interface SmokeDependencies {
74
+ readonly environment: Readonly<Record<string, string | undefined>>;
75
+ readonly stateDirectory: string;
76
+ readonly now: string;
77
+ }
78
+ /**
79
+ * Read every provider this machine has, through the real readers.
80
+ *
81
+ * A payload file wins when one was supplied, because that is a reading taken
82
+ * today. Otherwise the local snapshot cache stands in: those rows were written
83
+ * by the real collectors on this machine, so they still answer the question
84
+ * this harness exists for, and the document says which of the two it was.
85
+ */
86
+ export declare function collectSmoke(dependencies: SmokeDependencies): Promise<readonly SmokeProvider[]>;
87
+ export interface SmokeOutcome {
88
+ readonly lines: readonly string[];
89
+ readonly written: readonly string[];
90
+ readonly refused: readonly string[];
91
+ }
92
+ /**
93
+ * Run the harness and write one evidence file per provider that reported.
94
+ *
95
+ * A provider with no reading writes no file: an evidence file saying nothing
96
+ * happened is a file somebody will one day cite as proof that something did.
97
+ */
98
+ export declare function runSmoke(dependencies: SmokeDependencies, directory: string): Promise<SmokeOutcome>;
99
+ /**
100
+ * The command, including the half of it that does nothing.
101
+ *
102
+ * Returns the exit code rather than calling `process.exit`, so the whole thing
103
+ * stays testable and the skipped path can be asserted to be exactly what it
104
+ * claims: no reads, no writes, exit zero.
105
+ */
106
+ export declare function smokeMain(repositoryRoot: string, environment?: Readonly<Record<string, string | undefined>>, write?: (line: string) => void): Promise<number>;
107
+ //# sourceMappingURL=smoke.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../src/smoke.ts"],"names":[],"mappings":"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"}
package/dist/smoke.js ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * The live smoke harness: read this machine's real providers, once, on purpose.
3
+ *
4
+ * Every other test in this repository runs against a frozen fixture, which is
5
+ * the right default and has one blind spot it can never close: a fixture proves
6
+ * the parser reads the shape it was given, and says nothing about whether that
7
+ * shape is still what a provider sends today. This is the one path that answers
8
+ * the other question, and the reason it is a separate command rather than a
9
+ * test is that it needs a real account and produces evidence a human reads.
10
+ *
11
+ * Three rules hold it in place.
12
+ *
13
+ * OFF BY DEFAULT. Nothing here runs unless `OPENLIMITER_LIVE=1` is set in the
14
+ * environment. Without it the command prints one line saying so and exits zero,
15
+ * because a smoke harness that could run by accident in CI is a smoke harness
16
+ * that will one day read somebody's account in CI.
17
+ *
18
+ * NO NETWORK, EVER, FROM HERE. This module opens no sockets. It reads what the
19
+ * real readers already wrote to this machine, and payload FILES a person points
20
+ * it at, and it runs both through the real connector contracts. Fetching would
21
+ * make this a second collection implementation, and a second implementation is
22
+ * exactly what the browser mirror and the desktop reader exist to avoid.
23
+ *
24
+ * NOTHING IDENTIFYING LEAVES. The evidence file carries provider codes, meter
25
+ * codes, numbers, instants and connection states. It carries no token, no
26
+ * email, no key, no account alias and no path. `containsSecret` below is the
27
+ * last gate: it reads the finished document back as text and refuses to write
28
+ * it if anything that looks like a credential or a machine path survived.
29
+ */
30
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
31
+ import path from "node:path";
32
+ import { connectors } from "@openlimiter/connectors";
33
+ import { connectorMaturity, freshness, normalizeMeters, readSnapshotCache, resolveStateDirectory } from "@openlimiter/core";
34
+ /** The environment variable that turns this on. Nothing else does. */
35
+ export const LIVE_ENVIRONMENT_FLAG = "OPENLIMITER_LIVE";
36
+ /** What the command prints when the flag is absent. */
37
+ export const SKIPPED_NOTE = "openlimiter smoke: skipped. Set " + LIVE_ENVIRONMENT_FLAG +
38
+ "=1 to read this machine's real providers.";
39
+ /**
40
+ * Where a person points this at a payload they captured themselves.
41
+ *
42
+ * `OPENLIMITER_SMOKE_PAYLOAD_CLAUDE=/path/to/statusline.json` hands that file
43
+ * to the Claude reader. This is how a live response reaches the real parser
44
+ * without this module ever making the call: the person makes it, with their own
45
+ * credential, in their own shell, and hands over the result.
46
+ */
47
+ export const PAYLOAD_ENVIRONMENT_PREFIX = "OPENLIMITER_SMOKE_PAYLOAD_";
48
+ /** Where the evidence goes, when a person wants it somewhere else. */
49
+ export const EVIDENCE_DIRECTORY_ENVIRONMENT = "OPENLIMITER_EVIDENCE_DIR";
50
+ /** Largest payload file this harness will read. */
51
+ export const MAX_PAYLOAD_BYTES = 262_144;
52
+ /**
53
+ * Anything that must never appear in an evidence file.
54
+ *
55
+ * Written as a refusal rather than a redaction on purpose. A redactor that
56
+ * misses a pattern writes the secret; a gate that misses a pattern writes the
57
+ * secret too, but a gate that FIRES is loud, and the failure mode of "we wrote
58
+ * nothing and said why" is the only acceptable one here.
59
+ */
60
+ const SECRET_PATTERNS = [
61
+ ["an email address", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/u],
62
+ ["a JSON web token", /\beyJ[A-Za-z0-9_-]{6,}/u],
63
+ ["a bearer token", /\bBearer\s+\S/iu],
64
+ ["an api key", /\b(?:sk|pk|xai|sess)-[A-Za-z0-9]{8,}/u],
65
+ /* Case blind: Windows paths are, so the lower case spelling names the
66
+ same directory and leaks the same name. A case sensitive pattern here
67
+ caught the tidy spelling and let the one a shell prints straight by. */
68
+ ["a windows user path", /[A-Za-z]:\\+Users\\+[^\\\\"]+/iu],
69
+ ["a unix home path", /\/(?:home|Users)\/[^/"]+/u],
70
+ ["an authorization header", /"?authorization"?\s*[:=]/iu],
71
+ ["a cookie", /\b(?:set-)?cookie\b/iu]
72
+ ];
73
+ /** The first thing that must not be here, or null when the text is clean. */
74
+ export function containsSecret(text) {
75
+ for (const [reason, pattern] of SECRET_PATTERNS) {
76
+ if (pattern.test(text))
77
+ return reason;
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * The directory the evidence is written into.
83
+ *
84
+ * The mission workspace, `launch-2026-09-01/providers`, which sits beside the
85
+ * checkout rather than inside it. It is found by walking up from the repository
86
+ * root, because a worktree may be checked out at `launch-2026-09-01/wt/<lane>`
87
+ * or beside the workspace, and a fixed number of parent steps is right in one
88
+ * of those layouts and silently wrong in the other. The environment variable
89
+ * wins over both, for a person who wants the file somewhere else entirely.
90
+ */
91
+ export function evidenceDirectory(repositoryRoot, environment = process.env) {
92
+ const override = environment[EVIDENCE_DIRECTORY_ENVIRONMENT];
93
+ if (override !== undefined && override !== "")
94
+ return path.resolve(override);
95
+ let directory = path.resolve(repositoryRoot);
96
+ for (let step = 0; step < 6; step += 1) {
97
+ const parent = path.dirname(directory);
98
+ if (parent === directory)
99
+ break;
100
+ if (path.basename(parent) === "launch-2026-09-01") {
101
+ return path.join(parent, "providers");
102
+ }
103
+ directory = parent;
104
+ }
105
+ return path.resolve(repositoryRoot, "..", "..", "launch-2026-09-01", "providers");
106
+ }
107
+ /** The evidence file for one provider on one day. */
108
+ export function evidenceFileName(provider, day) {
109
+ return provider.toLowerCase().replace(/_/gu, "-") + "-" + day + ".json";
110
+ }
111
+ /** The calendar day an instant falls on, in UTC, so a file name never drifts. */
112
+ export function evidenceDay(now) {
113
+ const parsed = Date.parse(now);
114
+ return Number.isFinite(parsed)
115
+ ? new Date(parsed).toISOString().slice(0, 10)
116
+ : "unknown";
117
+ }
118
+ /**
119
+ * One printed line per provider per meter, in the shape a human scans.
120
+ *
121
+ * Percent and reset, because those are the two things a person is checking, and
122
+ * nothing else, because every other field on a snapshot is either a constant or
123
+ * a thing that could carry an identity.
124
+ */
125
+ export function smokeLine(entry, meter) {
126
+ return [
127
+ entry.provider.padEnd(12),
128
+ meter.meter.padEnd(22),
129
+ (meter.value.toFixed(1) + "%").padStart(7),
130
+ meter.state.padEnd(8),
131
+ meter.resetAt ?? "no reset stated"
132
+ ].join(" ");
133
+ }
134
+ /** The line for a provider that produced nothing, which is still a result. */
135
+ export function silentLine(entry) {
136
+ const detail = entry.connection === null
137
+ ? entry.detected ? "detected, no reading" : "not configured"
138
+ : entry.connection.state + ", " + entry.connection.instruction;
139
+ return entry.provider.padEnd(12) + " " + detail;
140
+ }
141
+ function snapshotMeter(snapshot, now) {
142
+ return {
143
+ meter: snapshot.meter,
144
+ value: snapshot.value,
145
+ unit: snapshot.unit,
146
+ resetAt: snapshot.resetAt,
147
+ state: freshness(snapshot.observedAt, snapshot.expiresAt, now),
148
+ source: snapshot.source,
149
+ precision: snapshot.precision
150
+ };
151
+ }
152
+ /**
153
+ * The evidence document, with nothing in it that could name a person.
154
+ *
155
+ * Built by naming every field that goes IN rather than by deleting fields that
156
+ * must stay out. An allow list cannot be defeated by a provider adding a field,
157
+ * and a deny list can, which is the whole argument.
158
+ */
159
+ export function sanitizeEvidence(entry, now) {
160
+ return {
161
+ schema: "openlimiter.smoke.evidence.v1",
162
+ provider: entry.provider,
163
+ connector: entry.connector,
164
+ maturity: entry.maturity,
165
+ observedAt: now,
166
+ detected: entry.detected,
167
+ readVia: entry.via,
168
+ connection: entry.connection === null
169
+ ? null
170
+ : {
171
+ state: entry.connection.state,
172
+ reason: entry.connection.reason,
173
+ instruction: entry.connection.instruction
174
+ },
175
+ meterCount: entry.meters.length,
176
+ meters: entry.meters.map((meter) => ({
177
+ meter: meter.meter,
178
+ value: meter.value,
179
+ unit: meter.unit,
180
+ resetAt: meter.resetAt,
181
+ state: meter.state,
182
+ source: meter.source,
183
+ precision: meter.precision
184
+ }))
185
+ };
186
+ }
187
+ /** A payload file a person pointed this harness at, or null. */
188
+ async function readPayloadFile(connectorId, encoding, environment) {
189
+ const key = PAYLOAD_ENVIRONMENT_PREFIX + connectorId.toUpperCase();
190
+ const file = environment[key];
191
+ if (file === undefined || file === "")
192
+ return null;
193
+ let text;
194
+ try {
195
+ text = await readFile(path.resolve(file), "utf8");
196
+ }
197
+ catch {
198
+ return { ok: false, reason: "could not be read" };
199
+ }
200
+ if (text.length > MAX_PAYLOAD_BYTES)
201
+ return { ok: false, reason: "is too large" };
202
+ if (encoding === "text")
203
+ return { ok: true, payload: text };
204
+ try {
205
+ return { ok: true, payload: JSON.parse(text) };
206
+ }
207
+ catch {
208
+ return { ok: false, reason: "is not valid JSON" };
209
+ }
210
+ }
211
+ /**
212
+ * Read every provider this machine has, through the real readers.
213
+ *
214
+ * A payload file wins when one was supplied, because that is a reading taken
215
+ * today. Otherwise the local snapshot cache stands in: those rows were written
216
+ * by the real collectors on this machine, so they still answer the question
217
+ * this harness exists for, and the document says which of the two it was.
218
+ */
219
+ export async function collectSmoke(dependencies) {
220
+ const cached = await readSnapshotCache(dependencies.stateDirectory);
221
+ const cachedRows = cached.ok ? cached.snapshots : [];
222
+ const entries = [];
223
+ for (const connector of connectors) {
224
+ const provider = connector.id.toUpperCase();
225
+ const detected = connector.detect(dependencies.environment);
226
+ const supplied = await readPayloadFile(connector.id, connector.encoding, dependencies.environment);
227
+ let connection = null;
228
+ let meters = [];
229
+ let via = "none";
230
+ if (supplied !== null && supplied.ok) {
231
+ const result = await connector.read({
232
+ payload: supplied.payload,
233
+ now: dependencies.now,
234
+ environment: dependencies.environment
235
+ });
236
+ connection = result.connection ?? null;
237
+ if (result.ok) {
238
+ via = "payload_file";
239
+ meters = normalizeMeters(result.meters)
240
+ .map((snapshot) => snapshotMeter(snapshot, dependencies.now));
241
+ }
242
+ }
243
+ if (meters.length === 0) {
244
+ const rows = cachedRows.filter((row) => row.provider === provider);
245
+ if (rows.length > 0) {
246
+ via = "snapshot_cache";
247
+ meters = rows.map((row) => snapshotMeter(row, dependencies.now));
248
+ }
249
+ }
250
+ entries.push({
251
+ provider,
252
+ connector: connector.id,
253
+ maturity: connectorMaturity(connector),
254
+ detected,
255
+ via,
256
+ connection,
257
+ meters
258
+ });
259
+ }
260
+ return entries;
261
+ }
262
+ /**
263
+ * Run the harness and write one evidence file per provider that reported.
264
+ *
265
+ * A provider with no reading writes no file: an evidence file saying nothing
266
+ * happened is a file somebody will one day cite as proof that something did.
267
+ */
268
+ export async function runSmoke(dependencies, directory) {
269
+ const entries = await collectSmoke(dependencies);
270
+ const lines = [];
271
+ const written = [];
272
+ const refused = [];
273
+ const day = evidenceDay(dependencies.now);
274
+ let created = false;
275
+ for (const entry of entries) {
276
+ if (entry.meters.length === 0) {
277
+ lines.push(silentLine(entry));
278
+ continue;
279
+ }
280
+ for (const meter of entry.meters)
281
+ lines.push(smokeLine(entry, meter));
282
+ const document = JSON.stringify(sanitizeEvidence(entry, dependencies.now), null, 2);
283
+ const secret = containsSecret(document);
284
+ if (secret !== null) {
285
+ refused.push(entry.provider + ": evidence withheld, it contained " + secret);
286
+ continue;
287
+ }
288
+ if (!created) {
289
+ await mkdir(directory, { recursive: true });
290
+ created = true;
291
+ }
292
+ const file = path.join(directory, evidenceFileName(entry.provider, day));
293
+ await writeFile(file, document + "\n", "utf8");
294
+ written.push(evidenceFileName(entry.provider, day));
295
+ }
296
+ return { lines, written, refused };
297
+ }
298
+ /**
299
+ * The command, including the half of it that does nothing.
300
+ *
301
+ * Returns the exit code rather than calling `process.exit`, so the whole thing
302
+ * stays testable and the skipped path can be asserted to be exactly what it
303
+ * claims: no reads, no writes, exit zero.
304
+ */
305
+ export async function smokeMain(repositoryRoot, environment = process.env, write = (line) => process.stdout.write(line + "\n")) {
306
+ if (environment[LIVE_ENVIRONMENT_FLAG] !== "1") {
307
+ write(SKIPPED_NOTE);
308
+ return 0;
309
+ }
310
+ const directory = evidenceDirectory(repositoryRoot, environment);
311
+ const outcome = await runSmoke({
312
+ environment,
313
+ stateDirectory: resolveStateDirectory({ environment }),
314
+ now: new Date().toISOString()
315
+ }, directory);
316
+ write("PROVIDER METER PERCENT STATE RESETS AT");
317
+ for (const line of outcome.lines)
318
+ write(line);
319
+ for (const refusal of outcome.refused)
320
+ write("WITHHELD " + refusal);
321
+ write(outcome.written.length === 0
322
+ ? "No evidence written: no provider reported a reading."
323
+ : "Wrote " + String(outcome.written.length) + " evidence file(s): " +
324
+ outcome.written.join(", "));
325
+ return 0;
326
+ }
327
+ //# sourceMappingURL=smoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.js","sourceRoot":"","sources":["../src/smoke.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ /** A foreign status line may not hold Claude's prompt open indefinitely. */
3
+ export declare const STATUSLINE_WRAPPER_TIMEOUT_MILLISECONDS = 2000;
4
+ /** Quota capture is best effort and never gets a longer budget than the prompt. */
5
+ export declare const STATUSLINE_WRAPPER_INGEST_TIMEOUT_MILLISECONDS = 1000;
6
+ export interface StatuslineWrapperResult {
7
+ exitCode: number;
8
+ stdout: Buffer;
9
+ stderr: Buffer;
10
+ timedOut: boolean;
11
+ }
12
+ export interface StatuslineWrapperOptions {
13
+ ingest: (payload: Buffer) => Promise<unknown>;
14
+ timeoutMilliseconds?: number;
15
+ ingestTimeoutMilliseconds?: number;
16
+ spawnCommand?: StatuslineWrapperSpawn;
17
+ }
18
+ export type StatuslineWrapperSpawn = (command: string) => ChildProcessWithoutNullStreams;
19
+ /**
20
+ * The settings writer uses this alphabet because every character is inert in
21
+ * both POSIX shells and cmd.exe. The decoded text is still passed to the shell
22
+ * as one untouched command string, so this transport does not reinterpret it.
23
+ */
24
+ export declare function encodeWrappedStatuslineCommand(command: string): string;
25
+ /** Decode only canonical, unpadded base64url carrying valid UTF-8. */
26
+ export declare function decodeWrappedStatuslineCommand(encoded: string): string | null;
27
+ /**
28
+ * Capture Claude's payload and preserve the foreign status line as the primary
29
+ * operation. OpenLimiter ingestion runs beside it, is bounded, and is silent.
30
+ */
31
+ export declare function runStatuslineWrapper(payload: Buffer, originalCommand: string, options: StatuslineWrapperOptions): Promise<StatuslineWrapperResult>;
32
+ //# sourceMappingURL=statusline-wrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusline-wrapper.d.ts","sourceRoot":"","sources":["../src/statusline-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,oBAAoB,CAAC;AAE5B,4EAA4E;AAC5E,eAAO,MAAM,uCAAuC,OAAQ,CAAC;AAE7D,mFAAmF;AACnF,eAAO,MAAM,8CAA8C,OAAQ,CAAC;AAEpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,YAAY,CAAC,EAAE,sBAAsB,CAAC;CACvC;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,OAAO,EAAE,MAAM,KACZ,8BAA8B,CAAC;AAEpC;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,sEAAsE;AACtE,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW7E;AA6ED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CA0BlC"}
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ /** A foreign status line may not hold Claude's prompt open indefinitely. */
3
+ export const STATUSLINE_WRAPPER_TIMEOUT_MILLISECONDS = 2_000;
4
+ /** Quota capture is best effort and never gets a longer budget than the prompt. */
5
+ export const STATUSLINE_WRAPPER_INGEST_TIMEOUT_MILLISECONDS = 1_000;
6
+ /**
7
+ * The settings writer uses this alphabet because every character is inert in
8
+ * both POSIX shells and cmd.exe. The decoded text is still passed to the shell
9
+ * as one untouched command string, so this transport does not reinterpret it.
10
+ */
11
+ export function encodeWrappedStatuslineCommand(command) {
12
+ return Buffer.from(command, "utf8").toString("base64url");
13
+ }
14
+ /** Decode only canonical, unpadded base64url carrying valid UTF-8. */
15
+ export function decodeWrappedStatuslineCommand(encoded) {
16
+ if (encoded === "" || !/^[A-Za-z0-9_-]+$/.test(encoded))
17
+ return null;
18
+ try {
19
+ const bytes = Buffer.from(encoded, "base64url");
20
+ if (bytes.toString("base64url") !== encoded)
21
+ return null;
22
+ const command = bytes.toString("utf8");
23
+ if (command === "" || !Buffer.from(command, "utf8").equals(bytes))
24
+ return null;
25
+ return command;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function quietDeadline(promise, milliseconds) {
32
+ return new Promise((resolve) => {
33
+ let settled = false;
34
+ const finish = () => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ resolve();
40
+ };
41
+ const timer = setTimeout(finish, milliseconds);
42
+ void promise.then(finish, finish);
43
+ });
44
+ }
45
+ function runOriginalCommand(command, payload, timeoutMilliseconds, spawnCommand) {
46
+ return new Promise((resolve) => {
47
+ const stdout = [];
48
+ const stderr = [];
49
+ let settled = false;
50
+ let child;
51
+ let timer;
52
+ const finish = (exitCode, timedOut) => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ clearTimeout(timer);
57
+ resolve({
58
+ exitCode,
59
+ stdout: Buffer.concat(stdout),
60
+ stderr: Buffer.concat(stderr),
61
+ timedOut
62
+ });
63
+ };
64
+ try {
65
+ child = spawnCommand(command);
66
+ }
67
+ catch {
68
+ resolve({
69
+ exitCode: 0,
70
+ stdout: Buffer.alloc(0),
71
+ stderr: Buffer.alloc(0),
72
+ timedOut: false
73
+ });
74
+ return;
75
+ }
76
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
77
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
78
+ child.stdin.on("error", () => {
79
+ /* An original command that closes standard input early is still allowed. */
80
+ });
81
+ child.on("error", () => finish(0, false));
82
+ child.on("close", (code) => finish(code ?? 0, false));
83
+ timer = setTimeout(() => {
84
+ /* Stop waiting before attempting cleanup. Destroying the inherited pipes
85
+ prevents a grandchild from keeping this wrapper alive after its shell
86
+ has been terminated. */
87
+ child.stdin.destroy();
88
+ child.stdout.destroy();
89
+ child.stderr.destroy();
90
+ child.kill();
91
+ child.unref();
92
+ finish(0, true);
93
+ }, timeoutMilliseconds);
94
+ child.stdin.end(payload);
95
+ });
96
+ }
97
+ /**
98
+ * Capture Claude's payload and preserve the foreign status line as the primary
99
+ * operation. OpenLimiter ingestion runs beside it, is bounded, and is silent.
100
+ */
101
+ export async function runStatuslineWrapper(payload, originalCommand, options) {
102
+ const ingestion = Promise.resolve()
103
+ .then(() => options.ingest(payload))
104
+ .catch(() => undefined);
105
+ const original = runOriginalCommand(originalCommand, payload, options.timeoutMilliseconds ?? STATUSLINE_WRAPPER_TIMEOUT_MILLISECONDS,
106
+ // A statusline command is a user configured shell command string (pipes, quoting,
107
+ // env expansion), not an argv array: shell:true is required to honour it unchanged,
108
+ // exactly as encodeWrappedStatuslineCommand's contract above documents. The string
109
+ // is never built from untrusted network input; it is the user's own local setting.
110
+ // nosemgrep: javascript.lang.security.audit.spawn-shell-true.spawn-shell-true, javascript.lang.security.detect-child-process.detect-child-process
111
+ options.spawnCommand ?? ((command) => spawn(command, {
112
+ shell: true,
113
+ windowsHide: true,
114
+ stdio: ["pipe", "pipe", "pipe"]
115
+ })));
116
+ const result = await original;
117
+ await quietDeadline(ingestion, options.ingestTimeoutMilliseconds ??
118
+ STATUSLINE_WRAPPER_INGEST_TIMEOUT_MILLISECONDS);
119
+ return result;
120
+ }
121
+ //# sourceMappingURL=statusline-wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusline-wrapper.js","sourceRoot":"","sources":["../src/statusline-wrapper.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../src/statusline.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA2CrE,eAAO,MAAM,sBAAsB,EAAE,SAAS,YAAY,EAGzD,CAAC;AAIF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,SAAS,YAAY,EAAE,CAazB;AAID,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,OAAO,GACP,QAAQ,GACR,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAyCZ;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU,CAazD;AAID,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,6EAA6E;AAC7E,eAAO,MAAM,QAAQ,OAAO,CAAC;AA4C7B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,QAAQ,EAAE,EAC9B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAClC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,OAAO,GACb,SAAS,cAAc,EAAE,CAwB3B;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQrD;AA8ED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CA2B3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,EACxB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACzD,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT"}
1
+ {"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../src/statusline.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA8CrE,eAAO,MAAM,sBAAsB,EAAE,SAAS,YAAY,EAGzD,CAAC;AAIF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,SAAS,YAAY,EAAE,CAazB;AAID,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,OAAO,GACP,QAAQ,GACR,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAyCZ;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU,CAazD;AAID,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,6EAA6E;AAC7E,eAAO,MAAM,QAAQ,OAAO,CAAC;AA4C7B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,QAAQ,EAAE,EAC9B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,MAAM,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAClC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,OAAO,GACb,SAAS,cAAc,EAAE,CAwB3B;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQrD;AA8ED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CA2B3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,EACxB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACzD,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT"}
@@ -35,7 +35,10 @@ const PROVIDER_CLASS = {
35
35
  CLAUDE: "subscription",
36
36
  CODEX: "subscription",
37
37
  ANTIGRAVITY: "subscription",
38
+ GEMINI_CLI: "subscription",
38
39
  OPENCODE: "subscription",
40
+ GROK: "subscription",
41
+ KIMI: "subscription",
39
42
  MANUAL: "subscription",
40
43
  OPENROUTER: "api"
41
44
  };