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 +21 -0
- package/dist/bin.js +41 -10
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +18 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +268 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +34 -3
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +93 -5
- package/dist/ingest.js.map +1 -1
- package/dist/render.d.ts +18 -10
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +180 -24
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +36 -3
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +106 -14
- package/dist/serve.js.map +1 -1
- package/dist/smoke.d.ts +107 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +327 -0
- package/dist/smoke.js.map +1 -0
- package/dist/statusline-wrapper.d.ts +32 -0
- package/dist/statusline-wrapper.d.ts.map +1 -0
- package/dist/statusline-wrapper.js +121 -0
- package/dist/statusline-wrapper.js.map +1 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +3 -0
- package/dist/statusline.js.map +1 -1
- package/package.json +8 -6
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
process.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;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
367
|
+
payload,
|
|
293
368
|
now,
|
|
294
369
|
environment: dependencies.environment
|
|
295
370
|
});
|
|
296
371
|
meters = result.ok ? result.meters : null;
|
|
297
372
|
}
|
|
298
|
-
|
|
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
|
|
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);
|