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.
package/README.md CHANGED
@@ -16,6 +16,27 @@ openlimiter demo
16
16
 
17
17
  The demo uses synthetic data and does not read a provider account.
18
18
 
19
+ ## Agent context hooks
20
+
21
+ Install or remove a user scoped hook explicitly:
22
+
23
+ ```bash
24
+ openlimiter hooks install codex
25
+ openlimiter hooks uninstall codex
26
+ openlimiter hooks status codex
27
+ openlimiter hooks repair codex
28
+ ```
29
+
30
+ Installation preserves unrelated agent configuration, writes a recoverable backup, performs no network request, and refuses a version below the tested minimum or an unsafe configuration path. A newer version remains enabled and `hooks status` prints one warning line. Hook execution reads only the short lived local snapshot and the protected desktop trust bridge, then fails open with no context on any invalid or missing input.
31
+
32
+ Antigravity CLI injection is excluded because `agy 1.1.23` did not fire the IDE hook shapes in a real prompt fixture. Kimi Code CLI installation stays gated because `1.50.0` fired `UserPromptSubmit` but discarded successful hook stdout instead of adding it to model context.
33
+
34
+ Grok Build does not currently expose a documented dynamic context surface. Its `UserPromptSubmit` hook discards successful stdout, so OpenLimiter does not install a Grok hook or claim injection support. Use the explicit command instead:
35
+
36
+ ```bash
37
+ openlimiter status --agent-context
38
+ ```
39
+
19
40
  ## Provider inputs
20
41
 
21
42
  | Provider | How data reaches OpenLimiter |
package/dist/bin.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from "node:readline/promises";
3
+ import { HOOK_INPUT_MAX_BYTES } from "@openlimiter/adapters";
3
4
  import { runCli } from "./cli.js";
4
- import { readStandardInputText } from "./ingest.js";
5
+ import { readStandardInputBuffer, readStandardInputText } from "./ingest.js";
6
+ import { decodeWrappedStatuslineCommand, runStatuslineWrapper } from "./statusline-wrapper.js";
5
7
  async function promptForSecret() {
6
8
  if (!process.stdin.isTTY || !process.stdout.isTTY)
7
9
  return "";
@@ -16,13 +18,42 @@ async function promptForSecret() {
16
18
  interfaceHandle.close();
17
19
  }
18
20
  }
19
- const result = await runCli(process.argv.slice(2), {
20
- promptForSecret,
21
- readStandardInput: () => readStandardInputText()
22
- });
23
- if (result.stdout !== "")
24
- process.stdout.write(result.stdout + "\n");
25
- if (result.stderr !== "")
26
- process.stderr.write(result.stderr + "\n");
27
- process.exitCode = result.exitCode;
21
+ const argumentsList = process.argv.slice(2);
22
+ const wrapperRequested = argumentsList[0] === "statusline" && argumentsList[1] === "--wrap";
23
+ const wrapped = wrapperRequested
24
+ ? decodeWrappedStatuslineCommand(argumentsList[2] ?? "")
25
+ : null;
26
+ if (wrapperRequested && wrapped === null) {
27
+ /* A damaged wrapper command must fail invisibly, never fall through to the
28
+ OpenLimiter renderer and replace the user's visible status line. */
29
+ process.exitCode = 0;
30
+ }
31
+ else if (wrapped !== null) {
32
+ const payload = await readStandardInputBuffer();
33
+ const result = await runStatuslineWrapper(payload, wrapped, {
34
+ ingest: async (buffer) => {
35
+ await runCli(["statusline"], {
36
+ readStandardInput: async () => buffer.toString("utf8")
37
+ });
38
+ }
39
+ });
40
+ if (result.stdout.length > 0)
41
+ process.stdout.write(result.stdout);
42
+ if (result.stderr.length > 0)
43
+ process.stderr.write(result.stderr);
44
+ process.exitCode = result.exitCode;
45
+ }
46
+ else {
47
+ const result = await runCli(argumentsList, {
48
+ promptForSecret,
49
+ readStandardInput: (signal) => argumentsList[0] === "hook"
50
+ ? readStandardInputText(process.stdin, HOOK_INPUT_MAX_BYTES, undefined, signal)
51
+ : readStandardInputText()
52
+ });
53
+ if (result.stdout !== "")
54
+ process.stdout.write(result.stdout + "\n");
55
+ if (result.stderr !== "")
56
+ process.stderr.write(result.stderr + "\n");
57
+ process.exitCode = result.exitCode;
58
+ }
28
59
  //# sourceMappingURL=bin.js.map
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,KAAK,UAAU,eAAe;IAC5B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAC7D,MAAM,eAAe,GAAG,eAAe,CAAC;QACtC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,IAAI,CAAC;QACH,OAAO,MAAM,eAAe,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAC5D,CAAC;YAAS,CAAC;QACT,eAAe,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACjD,eAAe;IACf,iBAAiB,EAAE,GAAG,EAAE,CAAC,qBAAqB,EAAE;CACjD,CAAC,CAAC;AACH,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE;IAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AACrE,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE;IAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AACrE,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC"}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type ProviderFailure, type Snapshot } from "@openlimiter/core";
2
+ import { type AgentId, type AgentInstallation, type HostedContextTrust, type HostedTrustLoadOptions } from "@openlimiter/adapters";
2
3
  import { type CredentialStore } from "./credentials.js";
3
4
  import { type QuotaServerHandle } from "./serve.js";
4
5
  export interface CliDependencies {
@@ -15,7 +16,7 @@ export interface CliDependencies {
15
16
  * reader, which keeps every command deterministic when it is called in
16
17
  * process by a test or another tool.
17
18
  */
18
- readStandardInput: () => Promise<string | null>;
19
+ readStandardInput: (signal?: AbortSignal) => Promise<string | null>;
19
20
  /**
20
21
  * Whether output may carry terminal colour.
21
22
  *
@@ -26,6 +27,22 @@ export interface CliDependencies {
26
27
  * capture redirected into a file carries no control characters at all.
27
28
  */
28
29
  colorOutput: boolean;
30
+ homeDirectory: string;
31
+ openLimiterScript: string;
32
+ nodeExecutable: string;
33
+ platform: NodeJS.Platform;
34
+ detectedAgentInstallations: Readonly<Partial<Record<AgentId, AgentInstallation | null>>>;
35
+ hostedContextTrust?: HostedContextTrust;
36
+ hostedContextPublicKeys?: HostedTrustLoadOptions["pinnedPublicKeys"];
37
+ hostedTrustConfigRoot?: string;
38
+ /**
39
+ * How the Windows owner and permissions of the trust file are read.
40
+ *
41
+ * The library default asks the operating system. A test injects a recorded
42
+ * answer instead, so a hostile ownership case can be proved without touching
43
+ * the security of a real file.
44
+ */
45
+ hostedTrustWindowsSecurity?: HostedTrustLoadOptions["windowsSecurity"];
29
46
  /**
30
47
  * Called once the serve command is listening.
31
48
  *
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAcL,KAAK,eAAe,EAEpB,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAuC3B,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AAYpB,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,iBAAiB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChD;;;;;;;;OAQG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,YAAY,IAAI,CAAC;AA6C9B,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,qEAAqE;IACrE,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CACtC;AAqgBD,wBAAsB,MAAM,CAC1B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,SAAS,GAAE,OAAO,CAAC,eAAe,CAAM,GACvC,OAAO,CAAC,SAAS,CAAC,CAiDpB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAcL,KAAK,eAAe,EAEpB,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAoB3B,OAAO,EAWL,KAAK,OAAO,EACZ,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAG5B,MAAM,uBAAuB,CAAC;AA0B/B,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AAYpB,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,iBAAiB,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACpE;;;;;;;;OAQG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,0BAA0B,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACzF,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,uBAAuB,CAAC,EAAE,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IACrE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;IACvE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,YAAY,IAAI,CAAC;AA2E9B,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,qEAAqE;IACrE,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CACtC;AAoxBD,wBAAsB,MAAM,CAC1B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,SAAS,GAAE,OAAO,CAAC,eAAe,CAAM,GACvC,OAAO,CAAC,SAAS,CAAC,CAiDpB"}
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { PROVIDER_CODES, buildAdvice, canonicalJson, dedupeFailures, failureFromConnectorReason, freshness, mergeSnapshots, normalizeMeters, normalizeMetersReport, readSnapshotCache } from "@openlimiter/core";
2
- import { antigravityFixture, claudeFixture, codexFixture, connectors, manualFixture, opencodeFixture, openrouterFixture, parseAntigravityPayload, parseClaudePayload, parseCodexPayload, parseManualPayload, parseOpencodePayload, parseOpenrouterPayload } from "@openlimiter/connectors";
3
- import { agentContextFromCache, renderClaudeStatusline } from "@openlimiter/adapters";
2
+ import { antigravityFixture, claudeFixture, codexFixture, grokFixture, kimiFixture, connectors, manualFixture, opencodeFixture, openrouterFixture, parseAntigravityPayload, parseClaudePayload, parseCodexPayload, parseGrokPayload, parseKimiPayload, parseManualPayload, parseOpencodePayload, parseOpenrouterPayload } from "@openlimiter/connectors";
3
+ import { AGENT_COMPATIBILITY, agentContextFromCache, agentContextSpillFromCache, agentVersionCompatibility, changeAgentHook, detectAgentInstallation, readAgentHookStatus, runAgentHook, validateAgentExecutableStamp, writeAgentContextSnapshot, loadHostedContextTrust, renderClaudeStatusline } from "@openlimiter/adapters";
4
+ import { homedir } from "node:os";
4
5
  import { STATUSLINE_KEYS, defaultConfig, initialize, isStatuslineKey, readConfig, readStatuslineConfig, setStatuslineValue, statuslineValueText, writeConfig } from "./config.js";
5
- import { environmentWithLocalMarkers, parseJsonText, persistSnapshots, readManualDocument } from "./ingest.js";
6
+ import { INGEST_PROVENANCE, MANUAL_PROVENANCE, STATUSLINE_PROVENANCE, environmentWithLocalMarkers, STDIN_BYTE_LIMIT, parseJsonText, persistSnapshots, readManualDocument, withProvenance } from "./ingest.js";
6
7
  import { UnavailableCredentialStore } from "./credentials.js";
7
8
  import { DEFAULT_SERVE_PORT, serveBanner, startQuotaServer } from "./serve.js";
8
9
  import { failureLine, failureLines, renderTable, supportsColor } from "./render.js";
@@ -20,12 +21,37 @@ function defaults() {
20
21
  now: () => new Date().toISOString(),
21
22
  payloads: {},
22
23
  readStandardInput: async () => null,
23
- colorOutput: supportsColor(process.env, process.stdout.isTTY === true)
24
+ colorOutput: supportsColor(process.env, process.stdout.isTTY === true),
25
+ homeDirectory: homedir(),
26
+ openLimiterScript: process.argv[1] ?? "",
27
+ nodeExecutable: process.execPath,
28
+ platform: process.platform,
29
+ detectedAgentInstallations: {}
24
30
  };
25
31
  }
26
32
  function succeed(stdout) {
27
33
  return { exitCode: EXIT_OK, stdout, stderr: "" };
28
34
  }
35
+ async function resolvedHostedTrust(dependencies, now, signal) {
36
+ if (dependencies.hostedContextTrust !== undefined) {
37
+ return dependencies.hostedContextTrust;
38
+ }
39
+ return await loadHostedContextTrust({
40
+ homeDirectory: dependencies.homeDirectory,
41
+ platform: dependencies.platform,
42
+ now,
43
+ ...(signal === undefined ? {} : { signal }),
44
+ ...(dependencies.hostedContextPublicKeys === undefined
45
+ ? {}
46
+ : { pinnedPublicKeys: dependencies.hostedContextPublicKeys }),
47
+ ...(dependencies.hostedTrustConfigRoot === undefined
48
+ ? {}
49
+ : { trustedPlatformConfigRoot: dependencies.hostedTrustConfigRoot }),
50
+ ...(dependencies.hostedTrustWindowsSecurity === undefined
51
+ ? {}
52
+ : { windowsSecurity: dependencies.hostedTrustWindowsSecurity })
53
+ });
54
+ }
29
55
  function fail(exitCode, message, stdout = "") {
30
56
  return { exitCode, stdout, stderr: message };
31
57
  }
@@ -62,6 +88,12 @@ async function refresh(dependencies, now) {
62
88
  const manual = supplied === undefined
63
89
  ? await readManualDocument(dependencies.stateDirectory)
64
90
  : supplied;
91
+ /*
92
+ * The manual connector is the only one this command reads from disk on its
93
+ * own, so it is the only one whose provenance is the document. Everything
94
+ * else here was handed in by a caller, which is an import by any other name.
95
+ */
96
+ const manualFromDisk = supplied === undefined;
65
97
  const environment = await environmentWithLocalMarkers(dependencies.environment, dependencies.stateDirectory);
66
98
  const raw = [];
67
99
  const failures = [];
@@ -71,7 +103,10 @@ async function refresh(dependencies, now) {
71
103
  : dependencies.payloads[connector.id];
72
104
  const result = await connector.read({ payload, now, environment });
73
105
  if (result.ok) {
74
- raw.push(...result.meters);
106
+ const provenance = connector.id === "manual" && manualFromDisk
107
+ ? MANUAL_PROVENANCE
108
+ : INGEST_PROVENANCE;
109
+ raw.push(...withProvenance(result.meters, provenance));
75
110
  continue;
76
111
  }
77
112
  if (payload === undefined)
@@ -87,7 +122,7 @@ async function refresh(dependencies, now) {
87
122
  }
88
123
  if (report.snapshots.length === 0)
89
124
  return { snapshots: [], failures };
90
- const persisted = await persistSnapshots(report.snapshots, dependencies.stateDirectory);
125
+ const persisted = await persistSnapshots(report.snapshots, dependencies.stateDirectory, now);
91
126
  return { snapshots: persisted.merged, failures };
92
127
  }
93
128
  function demoSnapshots(now) {
@@ -95,11 +130,13 @@ function demoSnapshots(now) {
95
130
  ...(parseClaudePayload(claudeFixture(now), now) ?? []),
96
131
  ...(parseOpenrouterPayload(openrouterFixture(), now) ?? []),
97
132
  ...(parseCodexPayload(codexFixture(now), now) ?? []),
133
+ ...(parseGrokPayload(grokFixture(now), now) ?? []),
134
+ ...(parseKimiPayload(kimiFixture(now), now) ?? []),
98
135
  ...(parseAntigravityPayload(antigravityFixture(now), now) ?? []),
99
136
  ...(parseOpencodePayload(opencodeFixture(now), now) ?? []),
100
137
  ...(parseManualPayload(manualFixture(now), now) ?? [])
101
138
  ];
102
- return normalizeMeters(raw);
139
+ return normalizeMeters(withProvenance(raw, INGEST_PROVENANCE));
103
140
  }
104
141
  function doctorRows(snapshots, environment, now) {
105
142
  const lines = ["CONNECTOR DETECTED FRESHNESS DRIFT"];
@@ -127,6 +164,11 @@ const help = [
127
164
  "openlimiter snapshot [--refresh]",
128
165
  "openlimiter statusline",
129
166
  "openlimiter hook [--dry-run]",
167
+ "openlimiter hooks install <agent>",
168
+ "openlimiter hooks uninstall <agent>",
169
+ "openlimiter hooks status <agent>",
170
+ "openlimiter hooks repair <agent>",
171
+ "openlimiter status --agent-context",
130
172
  "openlimiter ingest [--provider <id>] [--payload <json>]",
131
173
  "openlimiter config get statusline[.<key>]",
132
174
  "openlimiter config set statusline.<key> <value>",
@@ -157,11 +199,13 @@ async function ingestStandardInput(dependencies, now) {
157
199
  const meters = parseClaudePayload(document.value, now);
158
200
  if (meters === null)
159
201
  return null;
160
- const incoming = normalizeMeters(meters);
202
+ /* Claude Code wrote this to our standard input in this session. It is the
203
+ one live reading the product currently has, and it says so. */
204
+ const incoming = normalizeMeters(withProvenance(meters, STATUSLINE_PROVENANCE));
161
205
  if (incoming.length === 0)
162
206
  return null;
163
207
  try {
164
- return (await persistSnapshots(incoming, dependencies.stateDirectory)).merged;
208
+ return (await persistSnapshots(incoming, dependencies.stateDirectory, now)).merged;
165
209
  }
166
210
  catch {
167
211
  const existing = await cachedSnapshots(dependencies.stateDirectory);
@@ -198,9 +242,16 @@ async function snapshotCommand(dependencies, argumentsList, now) {
198
242
  }
199
243
  const cached = await readSnapshotCache(dependencies.stateDirectory);
200
244
  if (!cached.ok && cached.reason !== "missing") {
245
+ await writeAgentContextSnapshot([], dependencies.stateDirectory, now, PROVIDER_CODES).catch(() => undefined);
201
246
  return fail(EXIT_FAILURE, "openlimiter snapshot: quota state could not be read.");
202
247
  }
203
248
  const snapshots = cached.ok ? cached.snapshots : [];
249
+ try {
250
+ await writeAgentContextSnapshot(snapshots, dependencies.stateDirectory, now, PROVIDER_CODES);
251
+ }
252
+ catch {
253
+ return fail(EXIT_FAILURE, "openlimiter snapshot: agent context could not be written.");
254
+ }
204
255
  const stdout = withFailures(renderTable(snapshots, now, dependencies.colorOutput), failures, dependencies.colorOutput);
205
256
  return snapshots.length === 0
206
257
  ? fail(EXIT_NO_DATA, "openlimiter snapshot: no bounded quota data is available.", stdout)
@@ -273,14 +324,24 @@ async function ingestCommand(dependencies, argumentsList, now) {
273
324
  return fail(EXIT_USAGE, "openlimiter ingest: the payload flag needs a value.");
274
325
  }
275
326
  const text = inline ?? await dependencies.readStandardInput();
276
- const document = parseJsonText(text);
277
- if (!document.ok) {
278
- return text === null || text.trim() === ""
279
- ? fail(EXIT_USAGE, "openlimiter ingest: no input was supplied on standard input.")
280
- : fail(EXIT_FAILURE, "openlimiter ingest: input is not valid JSON.");
281
- }
327
+ if (text === null || text.trim() === "") {
328
+ return fail(EXIT_USAGE, "openlimiter ingest: no input was supplied on standard input.");
329
+ }
330
+ /*
331
+ * The connector says what its payload IS, and this boundary obeys it.
332
+ *
333
+ * Assuming JSON here made the OpenCode reader unreachable from this command
334
+ * entirely: its payload is a logged in HTML page, so every real capture died
335
+ * at JSON.parse with "input is not valid JSON" and no page could ever be
336
+ * ingested. A text connector is handed the raw text, bounded exactly as the
337
+ * JSON path is bounded, and nothing is parsed on its behalf.
338
+ */
282
339
  let meters;
283
340
  if (provider === undefined) {
341
+ const document = parseJsonText(text);
342
+ if (!document.ok) {
343
+ return fail(EXIT_FAILURE, "openlimiter ingest: input is not valid JSON.");
344
+ }
284
345
  meters = parseManualPayload(document.value, now);
285
346
  }
286
347
  else {
@@ -288,19 +349,40 @@ async function ingestCommand(dependencies, argumentsList, now) {
288
349
  if (connector === undefined) {
289
350
  return fail(EXIT_USAGE, "openlimiter ingest: unknown provider.");
290
351
  }
352
+ let payload;
353
+ if (connector.encoding === "text") {
354
+ if (text.length > STDIN_BYTE_LIMIT) {
355
+ return fail(EXIT_FAILURE, "openlimiter ingest: input is larger than accepted.");
356
+ }
357
+ payload = text;
358
+ }
359
+ else {
360
+ const document = parseJsonText(text);
361
+ if (!document.ok) {
362
+ return fail(EXIT_FAILURE, "openlimiter ingest: input is not valid JSON.");
363
+ }
364
+ payload = document.value;
365
+ }
291
366
  const result = await connector.read({
292
- payload: document.value,
367
+ payload,
293
368
  now,
294
369
  environment: dependencies.environment
295
370
  });
296
371
  meters = result.ok ? result.meters : null;
297
372
  }
298
- const incoming = meters === null ? [] : normalizeMeters([...meters]);
373
+ /*
374
+ * However this document reached the command, a person handed it over. It is
375
+ * an import, not a live reading, whichever connector parsed it, and a card
376
+ * that showed it as live would be the exact claim this wave exists to stop.
377
+ */
378
+ const incoming = meters === null
379
+ ? []
380
+ : normalizeMeters(withProvenance(meters, INGEST_PROVENANCE));
299
381
  if (incoming.length === 0) {
300
382
  return fail(EXIT_FAILURE, "openlimiter ingest: no bounded meter survived validation.");
301
383
  }
302
384
  try {
303
- const persisted = await persistSnapshots(incoming, dependencies.stateDirectory);
385
+ const persisted = await persistSnapshots(incoming, dependencies.stateDirectory, now);
304
386
  return succeed("Ingested " + String(incoming.length) +
305
387
  " bounded meters. Cached meters: " + String(persisted.merged.length) + ".");
306
388
  }
@@ -323,6 +405,9 @@ async function ingestCommand(dependencies, argumentsList, now) {
323
405
  async function statuslineCommand(dependencies, now) {
324
406
  const ingested = await ingestStandardInput(dependencies, now);
325
407
  const snapshots = ingested ?? await cachedSnapshots(dependencies.stateDirectory);
408
+ if (ingested === null) {
409
+ await writeAgentContextSnapshot(snapshots, dependencies.stateDirectory, now, PROVIDER_CODES).catch(() => undefined);
410
+ }
326
411
  const advice = buildAdvice(snapshots, now, PROVIDER_CODES);
327
412
  const config = await readStatuslineConfig(dependencies.stateDirectory);
328
413
  if (!config.bars)
@@ -335,6 +420,165 @@ async function statuslineCommand(dependencies, now) {
335
420
  color: statuslineColor(config.color, dependencies.environment, dependencies.colorOutput)
336
421
  }));
337
422
  }
423
+ const agentAliases = {
424
+ agy: "antigravity",
425
+ antigravity: "antigravity",
426
+ claude: "claude",
427
+ codex: "codex",
428
+ gemini: "gemini",
429
+ grok: "grok",
430
+ "grok-build": "grok",
431
+ kimi: "kimi",
432
+ opencode: "opencode"
433
+ };
434
+ function agentArgument(value) {
435
+ return value === undefined ? null : agentAliases[value.toLowerCase()] ?? null;
436
+ }
437
+ async function hookProtocolCommand(dependencies, argumentsList, now) {
438
+ const agentFlag = flagValue(argumentsList, "--agent");
439
+ if (agentFlag === undefined) {
440
+ const hostedTrust = await resolvedHostedTrust(dependencies, now);
441
+ return succeed(await agentContextFromCache(dependencies.stateDirectory, now, PROVIDER_CODES, hostedTrust === undefined ? {} : { hostedTrust }));
442
+ }
443
+ const agent = agentArgument(agentFlag);
444
+ const hostVersion = flagValue(argumentsList, "--host-version");
445
+ if (agent === null || hostVersion === undefined)
446
+ return succeed("");
447
+ if (agent === "opencode" &&
448
+ dependencies.environment["OPENLIMITER_EXPERIMENTAL_OPENCODE"] !== "1")
449
+ return succeed("");
450
+ const fallback = () => succeed(runAgentHook({
451
+ agent,
452
+ hostVersion,
453
+ rawInput: null,
454
+ context: ""
455
+ }).stdout);
456
+ /*
457
+ * The deadline both chooses the answer and cancels the work behind it.
458
+ * Racing alone left the loser running, so a slow read could still reach the
459
+ * context cache and rewrite a spill file long after the host had been given
460
+ * its reply. Every step below sees the same signal and stops at it.
461
+ */
462
+ const deadline = new AbortController();
463
+ let timer;
464
+ const timeout = new Promise((resolve) => {
465
+ timer = setTimeout(() => {
466
+ deadline.abort();
467
+ resolve(fallback());
468
+ }, 450);
469
+ });
470
+ const work = (async () => {
471
+ if (flagValue(argumentsList, "--managed-hook") === "openlimiter-v1") {
472
+ const executable = flagValue(argumentsList, "--agent-executable");
473
+ const fileSize = Number(flagValue(argumentsList, "--agent-file-size"));
474
+ const mtime = Number(flagValue(argumentsList, "--agent-mtime-ms"));
475
+ if (executable === undefined ||
476
+ !(await validateAgentExecutableStamp(executable, fileSize, mtime)))
477
+ return fallback();
478
+ }
479
+ const rawInput = await dependencies.readStandardInput(deadline.signal);
480
+ if (deadline.signal.aborted)
481
+ return fallback();
482
+ const hostedTrust = await resolvedHostedTrust(dependencies, now, deadline.signal);
483
+ if (deadline.signal.aborted)
484
+ return fallback();
485
+ const context = await agentContextFromCache(dependencies.stateDirectory, now, PROVIDER_CODES, {
486
+ ...(hostedTrust === undefined ? {} : { hostedTrust }),
487
+ signal: deadline.signal
488
+ });
489
+ return succeed(runAgentHook({ agent, hostVersion, rawInput, context }).stdout);
490
+ })();
491
+ /* The race still sees a failure that arrives in time. This second handler
492
+ only keeps a failure that arrives too late from becoming an unhandled
493
+ rejection in the host process. */
494
+ void work.catch(() => undefined);
495
+ try {
496
+ return await Promise.race([work, timeout]);
497
+ }
498
+ finally {
499
+ if (timer !== undefined)
500
+ clearTimeout(timer);
501
+ deadline.abort();
502
+ }
503
+ }
504
+ async function hooksCommand(dependencies, argumentsList) {
505
+ const requestedAction = argumentsList[1];
506
+ const action = requestedAction === "repair" ? "install" : requestedAction;
507
+ const agent = agentArgument(argumentsList[2]);
508
+ if ((action !== "install" && action !== "uninstall" && action !== "status") ||
509
+ agent === null) {
510
+ return fail(EXIT_USAGE, "openlimiter hooks: use install, uninstall, status, or repair with a known agent.");
511
+ }
512
+ if (action === "status") {
513
+ const status = await readAgentHookStatus(agent, {
514
+ homeDirectory: dependencies.homeDirectory
515
+ });
516
+ const hasKnownInstallation = Object.prototype.hasOwnProperty.call(dependencies.detectedAgentInstallations, agent);
517
+ const installation = hasKnownInstallation
518
+ ? dependencies.detectedAgentInstallations[agent] ?? null
519
+ : await detectAgentInstallation(agent, {
520
+ environment: dependencies.environment,
521
+ platform: dependencies.platform
522
+ });
523
+ const gate = AGENT_COMPATIBILITY[agent];
524
+ const compatibility = installation === null
525
+ ? null
526
+ : agentVersionCompatibility(agent, installation.version);
527
+ const location = status.configPath ?? "NONE";
528
+ return succeed([
529
+ "agent=" + agent,
530
+ "installed=" + (status.installed ? "yes" : "no"),
531
+ "config=" + location,
532
+ ...(compatibility === "newer" && gate.minimumTestedVersion !== null
533
+ ? [
534
+ "warning=detected version " + installation.version +
535
+ " is newer than minimum tested " + gate.minimumTestedVersion
536
+ ]
537
+ : [])
538
+ ].join("\n"));
539
+ }
540
+ const knownInstallation = Object.prototype.hasOwnProperty.call(dependencies.detectedAgentInstallations, agent) ? dependencies.detectedAgentInstallations[agent] : undefined;
541
+ const result = await changeAgentHook(agent, action, {
542
+ homeDirectory: dependencies.homeDirectory,
543
+ openLimiterScript: dependencies.openLimiterScript,
544
+ nodeExecutable: dependencies.nodeExecutable,
545
+ environment: dependencies.environment,
546
+ platform: dependencies.platform,
547
+ ...(knownInstallation === undefined
548
+ ? {}
549
+ : knownInstallation === null
550
+ ? { detectedVersion: null }
551
+ : {
552
+ detectedVersion: knownInstallation.version,
553
+ agentExecutable: knownInstallation.executable,
554
+ agentFileSize: knownInstallation.fileSize,
555
+ agentMtimeMilliseconds: knownInstallation.mtimeMilliseconds
556
+ })
557
+ });
558
+ const preview = [
559
+ "agent=" + result.agent,
560
+ "action=" + result.action,
561
+ "changed=" + (result.changed ? "yes" : "no"),
562
+ "config=" + (result.configPath ?? "NONE"),
563
+ "backup=" + (result.backupPath ?? "NONE"),
564
+ "version=" + (result.version ?? "NONE")
565
+ ].join("\n");
566
+ return result.supported
567
+ ? succeed(preview)
568
+ : fail(EXIT_FAILURE, "openlimiter hooks: " + result.message, preview);
569
+ }
570
+ async function explicitStatusCommand(dependencies, argumentsList, now) {
571
+ if (!argumentsList.includes("--agent-context")) {
572
+ return fail(EXIT_USAGE, "openlimiter status: use --agent-context.");
573
+ }
574
+ const hostedTrust = await resolvedHostedTrust(dependencies, now);
575
+ const context = await agentContextFromCache(dependencies.stateDirectory, now, PROVIDER_CODES, hostedTrust === undefined ? {} : { hostedTrust });
576
+ const spill = await agentContextSpillFromCache(dependencies.stateDirectory, now);
577
+ const stdout = [context, spill].filter((value) => value !== "").join("\n");
578
+ return stdout === ""
579
+ ? fail(EXIT_NO_DATA, "openlimiter status: no current agent context is available.")
580
+ : succeed(stdout);
581
+ }
338
582
  /* --------------------------------------------------------------- config */
339
583
  /** The one section this command reads and writes. */
340
584
  const CONFIG_SECTION = "statusline";
@@ -472,7 +716,12 @@ export async function runCli(argumentsList, overrides = {}) {
472
716
  return await configCommand(dependencies, argumentsList);
473
717
  }
474
718
  if (command === "hook") {
475
- return succeed(await agentContextFromCache(dependencies.stateDirectory, now, PROVIDER_CODES));
719
+ return await hookProtocolCommand(dependencies, argumentsList, now);
720
+ }
721
+ if (command === "hooks")
722
+ return await hooksCommand(dependencies, argumentsList);
723
+ if (command === "status") {
724
+ return await explicitStatusCommand(dependencies, argumentsList, now);
476
725
  }
477
726
  if (command === "doctor")
478
727
  return await doctorCommand(dependencies, now);