@particle-academy/prism-acp 0.1.0 → 0.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Particle Academy
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Particle Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -109,6 +109,64 @@ implementations of one protocol disagree without anyone noticing. So:
109
109
  line here can carry a prompt, a file or a credential, and a framing error is
110
110
  not a reason to copy it into a log.
111
111
 
112
+ ## Rate limits are a gauge, not just a breach event
113
+
114
+ ACP has no field for a rate limit, so the detail rides in `_meta` under
115
+ `particle.academy/rate_limit` alongside a human-readable notice. It is a
116
+ **declared type**, not a passthrough:
117
+
118
+ ```ts
119
+ import { parseRateLimit, type ClaudeRateLimit } from '@particle-academy/prism-acp';
120
+
121
+ const limit: ClaudeRateLimit | undefined = parseRateLimit(payload);
122
+ const fiveHour = limit?.windows.five_hour;
123
+
124
+ if (fiveHour !== undefined) {
125
+ const remaining = Math.max(0, 1 - fiveHour.utilization);
126
+ console.log(`${Math.round(remaining * 100)}% left, resets ${new Date(fiveHour.resetsAtMs)}`);
127
+ }
128
+ ```
129
+
130
+ The frame arrives **mid-turn with `status: "allowed"`**, not only once you are
131
+ limited, and `utilization` moves as work is done — so remaining headroom is a
132
+ real reading rather than a feature invented to fill a panel.
133
+
134
+ Three things a consumer needs and cannot infer:
135
+
136
+ - **`resetsAt` is epoch SECONDS on the wire.** `resetsAtMs` is this package's,
137
+ converted once. Read the provider's field as milliseconds and every reset
138
+ time lands in January 1970.
139
+ - **`utilization` can exceed 1.** The frame models overage, so a window past
140
+ its allowance is a real state; the parse does not cap it, because a capped
141
+ figure would be one this package made up. Clamp where you draw the bar, next
142
+ to `isUsingOverage`.
143
+ - **`status` and `overageStatus` are open string unions.** Every frame captured
144
+ says `"allowed"`; no breached frame has ever been captured, so the breached
145
+ spelling is unknown. Test `status !== 'allowed'`, and never match a specific
146
+ breach value.
147
+
148
+ `parseRateLimit` returns `undefined` rather than a partial, and a payload it
149
+ does not recognise gets **no `rate_limit` key at all** — the frame goes to
150
+ `particle.academy/unmapped_frame` instead, **with the field that failed named**:
151
+
152
+ ```
153
+ rate_limit: unifiedWindows.five_hour.utilization expected finite number >= 0, got null
154
+ ```
155
+
156
+ That reason is load-bearing precisely because the refusal is total: one bad
157
+ field rejects the whole payload, so this string is the only thing a human gets.
158
+ Generic would mean a bug report of "the gauge vanished" rather than "they
159
+ renamed `utilization`". `readRateLimit()` returns it to you directly
160
+ (`{ ok: true, limit } | { ok: false, reason }`) if you would rather handle the
161
+ refusal than check for `undefined`.
162
+
163
+ A string value is described as `string(10)`, never quoted. This mapper sits on
164
+ the same stream as prompts, file contents and credentials, and the type tells
165
+ you a number became a string just as well as the digits would. That is the whole reason it is a
166
+ parse and not an interface: an interface over `unknown` is a cast, so a renamed
167
+ provider field would still read as `undefined`, and a gauge renders `undefined`
168
+ as empty. An empty headroom gauge is read by a human as plenty of headroom.
169
+
112
170
  ## Using it
113
171
 
114
172
  ```ts
@@ -117,8 +175,7 @@ import { serve, ClaudeDriver } from '@particle-academy/prism-acp';
117
175
  serve({
118
176
  input: process.stdin,
119
177
  output: process.stdout,
120
- driverFactory: (options, events) =>
121
- new ClaudeDriver({ cwd: options.cwd, ...options }, events),
178
+ driverFactory: (options, events) => new ClaudeDriver(options, events),
122
179
  });
123
180
  ```
124
181
 
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The rate-limit payload the Claude CLI reports, declared and narrowed.
3
+ *
4
+ * ## Why a parse and not just an interface
5
+ *
6
+ * This shipped as `input.rate_limit_info ?? input` -- passed through verbatim,
7
+ * with nothing in the `.d.ts` naming a field. A consumer building a headroom
8
+ * gauge then has to guess the provider's field names off a captured frame, and
9
+ * the failure mode when the provider renames one is the worst available: the
10
+ * read yields `undefined`, the gauge renders empty, and a human reads an empty
11
+ * gauge as PLENTY OF HEADROOM. A wrong answer delivered confidently.
12
+ *
13
+ * An interface alone does not fix that. The payload crosses a pipe as JSON, so
14
+ * it arrives as `unknown`, and an interface over `unknown` is a cast: a rename
15
+ * still produces the same silent `undefined`, only now with a type annotation
16
+ * standing behind it. What makes a rename LOUD is {@link parseRateLimit}
17
+ * refusing the shape, so the mapper can emit no gauge at all and say what it
18
+ * actually received instead. Absent and explained beats zero and plausible.
19
+ *
20
+ * ## Where the shape came from
21
+ *
22
+ * Three independently captured turns in `test/fixtures`, which agree on every
23
+ * key. Nothing here is inferred from a schema, because no schema for this frame
24
+ * was available -- the same provenance rule as the rest of this mapper.
25
+ *
26
+ * ## What the captures do NOT tell us
27
+ *
28
+ * Every captured frame says `status: "allowed"`. **No breached frame has ever
29
+ * been captured**, so how a breach is spelled is genuinely unknown, and so are
30
+ * the `overageStatus` values beyond `"rejected"`. Those stay open string unions
31
+ * on purpose -- see {@link ClaudeRateLimit.status}.
32
+ */
33
+ /** One rate-limit window, as the provider reports it. */
34
+ export interface ClaudeRateLimitWindow {
35
+ /**
36
+ * The fraction of this window consumed -- `0.12` is 12% used.
37
+ *
38
+ * **This can exceed 1.** The frame models overage (`isUsingOverage`,
39
+ * `overageStatus`), so a window consumed past its allowance is a real state
40
+ * and not a corrupt reading. {@link parseRateLimit} therefore accepts any
41
+ * finite value `>= 0` and does NOT cap it at 1: a capped figure would be a
42
+ * number this package made up. Clamp for a progress bar if you like, but
43
+ * clamp at the point of display, where a reader can also see
44
+ * `isUsingOverage`.
45
+ */
46
+ readonly utilization: number;
47
+ /** When this window resets, in epoch MILLISECONDS. See {@link ClaudeRateLimit.resetsAtMs}. */
48
+ readonly resetsAtMs: number;
49
+ }
50
+ /** The structured rate-limit detail carried under `particle.academy/rate_limit`. */
51
+ export interface ClaudeRateLimit {
52
+ /**
53
+ * `"allowed"` in every frame captured so far.
54
+ *
55
+ * The union is OPEN (`'allowed' | (string & {})`) deliberately, which keeps
56
+ * the known literal in autocomplete while accepting any string. A closed
57
+ * union would be the same silent-failure class inverted: it would break a
58
+ * consumer's BUILD the first time a real breach arrived, which is worse than
59
+ * the problem it was guarding against. Test `status !== 'allowed'` for "not
60
+ * allowed"; never match a specific breach spelling, because nobody here has
61
+ * seen one.
62
+ */
63
+ readonly status: 'allowed' | (string & {});
64
+ /**
65
+ * When the binding window resets, in epoch MILLISECONDS.
66
+ *
67
+ * **The provider sends SECONDS**, in a field named `resetsAt` that gives no
68
+ * hint of its unit. This field is named for its unit and converted exactly
69
+ * once, here, because the alternative is every consumer deciding
70
+ * independently and one of them rendering January 1970.
71
+ *
72
+ * No seconds-vs-milliseconds heuristic is applied. A range sniff would
73
+ * silently absorb a unit change by the provider; the conversion is
74
+ * unconditional so `test/rate-limit.test.ts` fails instead -- it pins the
75
+ * captured values to their real dates.
76
+ */
77
+ readonly resetsAtMs: number;
78
+ /** Which window the provider currently treats as binding, e.g. `"five_hour"`. */
79
+ readonly rateLimitType: string;
80
+ /** Open union for the same reason as {@link ClaudeRateLimit.status}: only `"rejected"` has been seen. */
81
+ readonly overageStatus?: string;
82
+ readonly overageDisabledReason?: string;
83
+ readonly isUsingOverage?: boolean;
84
+ /** Every window the frame reported, keyed as the provider keys them (`five_hour`, `seven_day`). */
85
+ readonly windows: Readonly<Record<string, ClaudeRateLimitWindow>>;
86
+ /**
87
+ * The provider's object, verbatim.
88
+ *
89
+ * Kept ON the typed value rather than beside it, so one `_meta` key always
90
+ * carries both views. It covers the case a refusal cannot: a field the
91
+ * provider ADDS still parses, and would otherwise be dropped by a type that
92
+ * does not know about it yet. `src/meta.ts` is explicit that nothing is
93
+ * silently dropped, and a narrowing parse is exactly where that rule would
94
+ * otherwise be quietly broken.
95
+ */
96
+ readonly raw: Record<string, unknown>;
97
+ }
98
+ /**
99
+ * The result of reading a payload: the value, or WHY it was refused.
100
+ *
101
+ * The reason exists because the refusal is total. Since one bad field rejects
102
+ * the whole payload, the explanation is the ONLY thing a human gets when the
103
+ * provider changes shape -- so "not recognised" would turn a bug report into
104
+ * somebody diffing a frame by hand. Raised by prism-acp's first external
105
+ * reviewer, against this exact design.
106
+ */
107
+ export type RateLimitRead = {
108
+ readonly ok: true;
109
+ readonly limit: ClaudeRateLimit;
110
+ } | {
111
+ readonly ok: false;
112
+ readonly reason: string;
113
+ };
114
+ /**
115
+ * Narrow an unknown rate-limit payload, or refuse it.
116
+ *
117
+ * Returns `undefined` rather than a partial value. A partial is the thing worth
118
+ * refusing hardest: a gauge built from half a payload looks like a reading.
119
+ *
120
+ * **One malformed window refuses the WHOLE payload.** Dropping the bad window
121
+ * and keeping the rest would mean a consumer whose `five_hour` figure went
122
+ * malformed silently renders the `seven_day` one in its place -- a healthy
123
+ * number, off the wrong window, with nothing to indicate the substitution.
124
+ */
125
+ export declare function parseRateLimit(value: unknown): ClaudeRateLimit | undefined;
126
+ /**
127
+ * The same read, but saying WHY when it refuses.
128
+ *
129
+ * {@link parseRateLimit} is the convenience; this is what the mapper uses,
130
+ * because the mapper is what has to explain itself to a human.
131
+ */
132
+ export declare function readRateLimit(value: unknown): RateLimitRead;
133
+ /**
134
+ * The sentence a human reads, built from the figures rather than from nothing.
135
+ *
136
+ * The notice this goes on used to say only "The provider reported a rate
137
+ * limit." -- true, and actionable by no one. The structured half is for a
138
+ * client; this half is for the person watching, and it should carry the two
139
+ * numbers they would otherwise have to open a debugger to see.
140
+ */
141
+ export declare function rateLimitNotice(limit: ClaudeRateLimit): string;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The rate-limit payload the Claude CLI reports, declared and narrowed.
3
+ *
4
+ * ## Why a parse and not just an interface
5
+ *
6
+ * This shipped as `input.rate_limit_info ?? input` -- passed through verbatim,
7
+ * with nothing in the `.d.ts` naming a field. A consumer building a headroom
8
+ * gauge then has to guess the provider's field names off a captured frame, and
9
+ * the failure mode when the provider renames one is the worst available: the
10
+ * read yields `undefined`, the gauge renders empty, and a human reads an empty
11
+ * gauge as PLENTY OF HEADROOM. A wrong answer delivered confidently.
12
+ *
13
+ * An interface alone does not fix that. The payload crosses a pipe as JSON, so
14
+ * it arrives as `unknown`, and an interface over `unknown` is a cast: a rename
15
+ * still produces the same silent `undefined`, only now with a type annotation
16
+ * standing behind it. What makes a rename LOUD is {@link parseRateLimit}
17
+ * refusing the shape, so the mapper can emit no gauge at all and say what it
18
+ * actually received instead. Absent and explained beats zero and plausible.
19
+ *
20
+ * ## Where the shape came from
21
+ *
22
+ * Three independently captured turns in `test/fixtures`, which agree on every
23
+ * key. Nothing here is inferred from a schema, because no schema for this frame
24
+ * was available -- the same provenance rule as the rest of this mapper.
25
+ *
26
+ * ## What the captures do NOT tell us
27
+ *
28
+ * Every captured frame says `status: "allowed"`. **No breached frame has ever
29
+ * been captured**, so how a breach is spelled is genuinely unknown, and so are
30
+ * the `overageStatus` values beyond `"rejected"`. Those stay open string unions
31
+ * on purpose -- see {@link ClaudeRateLimit.status}.
32
+ */
33
+ function isObject(value) {
34
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
35
+ }
36
+ /** A finite number at or above `minimum`, or `undefined`. */
37
+ function finite(value, minimum) {
38
+ return typeof value === 'number' && Number.isFinite(value) && value >= minimum
39
+ ? value
40
+ : undefined;
41
+ }
42
+ function optionalString(value) {
43
+ return typeof value === 'string' ? value : undefined;
44
+ }
45
+ /**
46
+ * Describe what arrived, WITHOUT quoting it.
47
+ *
48
+ * A number, boolean, null or undefined is reported as itself -- those are the
49
+ * cases that identify a shape change and none of them can carry content. A
50
+ * STRING is reported as its type and length only, because this module sits on
51
+ * the same stream as prompts, file contents and credentials, and this package
52
+ * already refuses to put a framing error's content in a log (see
53
+ * `MAX_LINE_BYTES` in the README). "expected finite number, got string(10)"
54
+ * identifies a provider switching a number to a string just as well as the
55
+ * digits would, and cannot leak anything if a future frame puts something else
56
+ * in that field.
57
+ */
58
+ function describe(value) {
59
+ if (value === null)
60
+ return 'null';
61
+ if (value === undefined)
62
+ return 'undefined';
63
+ if (typeof value === 'string')
64
+ return `string(${value.length})`;
65
+ if (typeof value === 'number' || typeof value === 'boolean')
66
+ return String(value);
67
+ if (Array.isArray(value))
68
+ return `array(${value.length})`;
69
+ if (typeof value === 'object')
70
+ return 'object';
71
+ return typeof value;
72
+ }
73
+ function refuse(path, expected, got) {
74
+ return { ok: false, reason: `rate_limit: ${path} expected ${expected}, got ${describe(got)}` };
75
+ }
76
+ /**
77
+ * Narrow an unknown rate-limit payload, or refuse it.
78
+ *
79
+ * Returns `undefined` rather than a partial value. A partial is the thing worth
80
+ * refusing hardest: a gauge built from half a payload looks like a reading.
81
+ *
82
+ * **One malformed window refuses the WHOLE payload.** Dropping the bad window
83
+ * and keeping the rest would mean a consumer whose `five_hour` figure went
84
+ * malformed silently renders the `seven_day` one in its place -- a healthy
85
+ * number, off the wrong window, with nothing to indicate the substitution.
86
+ */
87
+ export function parseRateLimit(value) {
88
+ const read = readRateLimit(value);
89
+ return read.ok ? read.limit : undefined;
90
+ }
91
+ /**
92
+ * The same read, but saying WHY when it refuses.
93
+ *
94
+ * {@link parseRateLimit} is the convenience; this is what the mapper uses,
95
+ * because the mapper is what has to explain itself to a human.
96
+ */
97
+ export function readRateLimit(value) {
98
+ if (!isObject(value))
99
+ return refuse('payload', 'an object', value);
100
+ const status = optionalString(value.status);
101
+ if (status === undefined)
102
+ return refuse('status', 'string', value.status);
103
+ const rateLimitType = optionalString(value.rateLimitType);
104
+ if (rateLimitType === undefined)
105
+ return refuse('rateLimitType', 'string', value.rateLimitType);
106
+ const resetsAtSeconds = finite(value.resetsAt, Number.MIN_VALUE);
107
+ if (resetsAtSeconds === undefined) {
108
+ return refuse('resetsAt', 'finite number > 0 (epoch seconds)', value.resetsAt);
109
+ }
110
+ // An absent `unifiedWindows` is refused, not treated as "no windows": it is
111
+ // the only part of this payload that answers "how much is left", so a
112
+ // consumer receiving a typed value without it would have a reset time and no
113
+ // gauge, which is the shape this type exists to stop being ambiguous.
114
+ if (!isObject(value.unifiedWindows)) {
115
+ return refuse('unifiedWindows', 'an object', value.unifiedWindows);
116
+ }
117
+ const windows = {};
118
+ for (const [name, window] of Object.entries(value.unifiedWindows)) {
119
+ if (!isObject(window))
120
+ return refuse(`unifiedWindows.${name}`, 'an object', window);
121
+ const utilization = finite(window.utilization, 0);
122
+ if (utilization === undefined) {
123
+ return refuse(`unifiedWindows.${name}.utilization`, 'finite number >= 0', window.utilization);
124
+ }
125
+ const windowResetsAtSeconds = finite(window.resetsAt, Number.MIN_VALUE);
126
+ if (windowResetsAtSeconds === undefined) {
127
+ return refuse(`unifiedWindows.${name}.resetsAt`, 'finite number > 0 (epoch seconds)', window.resetsAt);
128
+ }
129
+ windows[name] = { utilization, resetsAtMs: windowResetsAtSeconds * 1000 };
130
+ }
131
+ const isUsingOverage = typeof value.isUsingOverage === 'boolean' ? value.isUsingOverage : undefined;
132
+ return {
133
+ ok: true,
134
+ limit: {
135
+ status,
136
+ resetsAtMs: resetsAtSeconds * 1000,
137
+ rateLimitType,
138
+ ...(optionalString(value.overageStatus) === undefined
139
+ ? {}
140
+ : { overageStatus: value.overageStatus }),
141
+ ...(optionalString(value.overageDisabledReason) === undefined
142
+ ? {}
143
+ : { overageDisabledReason: value.overageDisabledReason }),
144
+ ...(isUsingOverage === undefined ? {} : { isUsingOverage }),
145
+ windows,
146
+ raw: value,
147
+ },
148
+ };
149
+ }
150
+ /**
151
+ * The sentence a human reads, built from the figures rather than from nothing.
152
+ *
153
+ * The notice this goes on used to say only "The provider reported a rate
154
+ * limit." -- true, and actionable by no one. The structured half is for a
155
+ * client; this half is for the person watching, and it should carry the two
156
+ * numbers they would otherwise have to open a debugger to see.
157
+ */
158
+ export function rateLimitNotice(limit) {
159
+ const binding = limit.windows[limit.rateLimitType];
160
+ const resetsAtMs = binding?.resetsAtMs ?? limit.resetsAtMs;
161
+ const used = binding === undefined ? '' : ` at ${Math.round(binding.utilization * 100)}% used`;
162
+ return `Rate limit: ${limit.rateLimitType} window${used}, resets ${new Date(resetsAtMs).toISOString()}.`;
163
+ }
@@ -22,6 +22,7 @@
22
22
  * reasoning, or the arguments to a command about to run.
23
23
  */
24
24
  import { META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, withMeta, } from '../meta.js';
25
+ import { rateLimitNotice, readRateLimit } from './rate-limit.js';
25
26
  /**
26
27
  * ACP's ten tool kinds.
27
28
  *
@@ -273,11 +274,35 @@ export class ClaudeToAcp {
273
274
  #rateLimit(input) {
274
275
  // A notice because it is genuinely user-facing, AND _meta so a client can
275
276
  // act on the reset times rather than parse a sentence.
277
+ //
278
+ // The payload is NARROWED rather than passed through. A rename by the
279
+ // provider used to reach the consumer as a field it could not read, which
280
+ // a gauge renders as empty -- and an empty headroom gauge reads as plenty
281
+ // of headroom. So an unrecognised payload now emits no rate-limit value at
282
+ // all and keeps the frame under `unmapped_frame` instead: absent and
283
+ // explained, rather than zero and plausible.
284
+ //
285
+ // The refusal NAMES THE FIELD, because the refusal is total: one bad field
286
+ // rejects the whole payload, so this reason is the only thing a human gets.
287
+ // A generic "not recognised" would turn a bug report from "they renamed
288
+ // utilization" into "the gauge vanished", and someone diffing a frame by
289
+ // hand to tell the difference.
290
+ const read = readRateLimit(input.rate_limit_info ?? input);
291
+ if (!read.ok) {
292
+ return [
293
+ withMeta({
294
+ sessionUpdate: 'notice',
295
+ notice: { level: 'warning', message: 'The provider reported a rate limit.' },
296
+ }, {
297
+ [META_UNMAPPED_FRAME]: { reason: read.reason, frame: input },
298
+ }),
299
+ ];
300
+ }
276
301
  return [
277
302
  withMeta({
278
303
  sessionUpdate: 'notice',
279
- notice: { level: 'warning', message: 'The provider reported a rate limit.' },
280
- }, { [META_RATE_LIMIT]: input.rate_limit_info ?? input }),
304
+ notice: { level: 'warning', message: rateLimitNotice(read.limit) },
305
+ }, { [META_RATE_LIMIT]: read.limit }),
281
306
  ];
282
307
  }
283
308
  /**
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export type { JsonRpcPeerOptions, NotificationHandler, RequestHandler, RpcId, }
7
7
  export { META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
8
8
  export { ClaudeToAcp } from './claude/to-acp.js';
9
9
  export type { AcpUpdate, ToolStatus } from './claude/to-acp.js';
10
+ export { parseRateLimit, rateLimitNotice, readRateLimit } from './claude/rate-limit.js';
11
+ export type { ClaudeRateLimit, ClaudeRateLimitWindow, RateLimitRead } from './claude/rate-limit.js';
10
12
  export { ClaudeDriver, claudeArgs, promptLine, updatesFromFrames } from './claude/driver.js';
11
13
  export type { ClaudeDriverEvents, ClaudeDriverOptions, ClaudePermissionMode, } from './claude/driver.js';
12
14
  export { AcpAgent, PROTOCOL_VERSION } from './acp/agent.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { BASE_ALLOW, OUTRANKING_CREDENTIALS, childEnv } from './env.js';
3
3
  export { JsonRpcPeer, RPC_INTERNAL_ERROR, RPC_INVALID_PARAMS, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, RpcError, } from './jsonrpc.js';
4
4
  export { META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
5
5
  export { ClaudeToAcp } from './claude/to-acp.js';
6
+ export { parseRateLimit, rateLimitNotice, readRateLimit } from './claude/rate-limit.js';
6
7
  export { ClaudeDriver, claudeArgs, promptLine, updatesFromFrames } from './claude/driver.js';
7
8
  export { AcpAgent, PROTOCOL_VERSION } from './acp/agent.js';
8
9
  export { serve } from './acp/stdio.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/prism-acp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Speak the Agent Client Protocol to a coding-agent CLI the user has already authenticated. No API key, no third-party adapter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=20"
22
+ "node": ">=22"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.json",