@lucascouts/claude-agent-acp-plus 0.11.1 → 0.13.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 @@
1
+ {"version":3,"file":"ultracode.d.ts","sourceRoot":"","sources":["../src/ultracode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE5E;;uEAEuE;AACvE,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,cAAc,CAAC;AAEjD;;;iDAGiD;AACjD,eAAO,MAAM,sBAAsB,EAAE,WAAqB,CAAC;AAE3D;;;kEAGkE;AAClE,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,qBAAqB,EAAE,SAAS,MAAM,EAAE,EACxC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,GAAG,SAAS,GACvD,OAAO,CAGT;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG;IAChE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B,CAQA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,oBAAoB,GAAG,SAAS,GAC1C,MAAM,GAAG,SAAS,CAGpB"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The "Ultracode" entry of the Effort picker.
3
+ *
4
+ * **Ultracode is not a sixth effort level, and reading it as one produces the
5
+ * wrong control.** Claude Code's VS Code extension models it as a boolean that
6
+ * turns on two things at once — `xhigh` effort *and* standing dynamic-workflow
7
+ * orchestration — and its own tooltip says so: `"Ultracode - xhigh +
8
+ * workflows"`. Its five real levels are `low` `medium` `high` `xhigh` `max`;
9
+ * Ultracode is drawn as one extra notch past the last of them, styled
10
+ * differently, and selecting any ordinary level clears it.
11
+ *
12
+ * ACP has no compound control to carry that, so the adapter projects it the way
13
+ * the extension's UI reads rather than the way its state is stored: one more
14
+ * entry in the `effort` select. The compound stays here, in one module, instead
15
+ * of being spread across the three places `acp-agent.ts` applies effort.
16
+ *
17
+ * The SDK carries both halves and names the channel: `Settings.ultracode` is
18
+ * *"xhigh effort plus standing dynamic-workflow orchestration. Session-scoped —
19
+ * typically provided via --settings or the apply_flag_settings control
20
+ * request"*. `apply_flag_settings` is already how the effort option is applied,
21
+ * so no new transport is involved.
22
+ *
23
+ * Kept self-contained — no import from `acp-agent.ts` — for the reason
24
+ * `thinking-option.ts` and `rewind-command.ts` state: that file is ~9,900 lines
25
+ * and moves upstream almost daily, so every coupling to it is paid again at
26
+ * each sync.
27
+ */
28
+ /** Value of the synthetic "Ultracode" entry in the effort picker. It is a
29
+ * reserved sentinel: no `EffortLevel` is spelled this way, so it cannot
30
+ * collide with a level the SDK reports in `supportedEffortLevels`. */
31
+ export const ULTRACODE_OPTION_VALUE = "ultracode";
32
+ /** Display name of that entry, matching the extension's own label. */
33
+ export const ULTRACODE_OPTION_NAME = "Ultracode";
34
+ /** The effort level Ultracode implies. Not configurable: the extension hardcodes
35
+ * `xhigh` in `enableUltracode()`, and the SDK's own description of the setting
36
+ * names the same level. A different level here would silently mean something
37
+ * else than it does in the reference client. */
38
+ export const ULTRACODE_EFFORT_LEVEL = "xhigh";
39
+ /**
40
+ * Whether the Ultracode entry may be offered, under the same two conditions the
41
+ * extension checks in its own `ultracodeAvailable`:
42
+ *
43
+ * 1. workflows are not disabled — Ultracode *is* the workflow half, so offering
44
+ * it where `disableWorkflows` is set would advertise something that cannot
45
+ * happen;
46
+ * 2. the current model supports `xhigh` — Ultracode has no meaning on a model
47
+ * that cannot reach the level it implies.
48
+ *
49
+ * Both are read rather than assumed: an unknown settings shape leaves
50
+ * `disableWorkflows` undefined, which is *not* `true`, so the gate opens — the
51
+ * same direction the extension's `=== !0` comparison takes.
52
+ */
53
+ export function isUltracodeAvailable(supportedEffortLevels, settings) {
54
+ if (settings?.disableWorkflows === true)
55
+ return false;
56
+ return supportedEffortLevels.includes(ULTRACODE_EFFORT_LEVEL);
57
+ }
58
+ /** Whether a picker value denotes Ultracode rather than a plain effort level. */
59
+ export function isUltracodeValue(value) {
60
+ return value === ULTRACODE_OPTION_VALUE;
61
+ }
62
+ /**
63
+ * The `apply_flag_settings` payload for an effort selection, Ultracode included.
64
+ *
65
+ * **The `null` is the load-bearing part.** The SDK shallow-merges flag settings
66
+ * and only clears a key when an explicit `null` arrives — `undefined` is dropped
67
+ * by JSON transport and leaves the previous value standing. So every selection
68
+ * has to say something about `ultracode`, not just the one that turns it on:
69
+ * without the clear, picking `High` after Ultracode would lower the effort and
70
+ * leave workflow orchestration running, which is a state no picker entry names.
71
+ * The same reasoning applies to `effortLevel` — this function absorbed the older
72
+ * `toSdkEffortLevel` helper for exactly that reason: both keys are decided in
73
+ * one place, so neither can be forgotten on a path the other takes.
74
+ *
75
+ * @param value the picker value — `"default"`, an `EffortLevel`, or the
76
+ * Ultracode sentinel. `undefined` means the option is absent for this model.
77
+ */
78
+ export function ultracodeFlagSettings(value) {
79
+ if (isUltracodeValue(value)) {
80
+ return { effortLevel: ULTRACODE_EFFORT_LEVEL, ultracode: true };
81
+ }
82
+ return {
83
+ effortLevel: value === undefined || value === "default" ? null : value,
84
+ ultracode: null,
85
+ };
86
+ }
87
+ /**
88
+ * The picker value to display, given the persisted effort level and whether the
89
+ * Ultracode flag is on.
90
+ *
91
+ * The flag wins over the level, and only when it is *available*: a session that
92
+ * carries `ultracode: true` from settings while the current model cannot reach
93
+ * `xhigh` must not show an entry the picker is not rendering. The extension
94
+ * reaches the same state through `adoptAppliedEffort`, which reads
95
+ * `applied.ultracode` first and falls back to inferring it from `xhigh`; we do
96
+ * not infer, because `xhigh` alone is a legitimate selection that is not
97
+ * Ultracode, and guessing would silently turn workflows on in the label.
98
+ */
99
+ export function effortOptionValue(effortLevel, ultracode) {
100
+ if (ultracode?.available && ultracode.enabled)
101
+ return ULTRACODE_OPTION_VALUE;
102
+ return effortLevel;
103
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `/usage` rendered as Markdown, and the bounded wait that decides whether it
3
+ * renders at all (story 011, R2.3/R2.4, design D5).
4
+ *
5
+ * The command ALWAYS runs through Claude Code. What this module produces is a
6
+ * best-effort overlay on top of an answer the user already has, so every
7
+ * failure here costs the overlay and nothing else — the caller forwards Claude
8
+ * Code's own text byte-for-byte instead. Three ways out, all of them ending in
9
+ * `null` from {@link structuredUsageMarkdown}:
10
+ *
11
+ * 1. unavailable — the control request rejects, or the method is gone
12
+ * 2. incompatible — the response does not match the shape the renderer reads
13
+ * 3. slow — the response does not arrive inside the bound below
14
+ *
15
+ * THE BOUND IS LOAD-BEARING, NOT DEFENSIVE. Control requests on a fresh session
16
+ * are not serviced until the first turn runs (SDK issues #886/#880), and
17
+ * `/usage` is frequently that first turn — so an unbounded wait would withhold
18
+ * an answer the CLI had already printed, forever. That is why the race below
19
+ * has three legs and not one: the report, a timeout, and the turn's abort
20
+ * signal.
21
+ *
22
+ * ONE READER FOR THE REPORT'S NUMBERS. `account-usage.ts` already maps this same
23
+ * `SDKControlGetUsageResponse` onto the client's rate-limit `_meta`, and it owns
24
+ * the two scalar readings the report needs: `normalizeUtilization` (0..100 ->
25
+ * 0..1, clamped) and `toEpochSeconds` (ISO 8601 -> Unix seconds). Both are
26
+ * imported here rather than re-derived, so the bar this module draws and the bar
27
+ * the client's usage panel draws cannot disagree about what `utilization: 3`
28
+ * means. What is NOT shared is validation: `account-usage.ts` reads defensively
29
+ * and never rejects a report, because a missing field there costs one window,
30
+ * while here an unreadable report has to become a hard "no" so way out 2 can
31
+ * fire. Different questions, so different code — but the same answers to the
32
+ * two questions both of them ask.
33
+ *
34
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
35
+ * `thinking-option.ts` and `context-compaction.ts`. The reason is merge cost:
36
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
37
+ * every coupling to it is paid again at each sync. `acp-agent.ts` wires this in
38
+ * at the `/usage` turn: it decides which turn owns a structured render, and
39
+ * publishes the result at most once across the message shapes the SDK can
40
+ * deliver one local command through.
41
+ */
42
+ import type { Query, SDKControlGetUsageResponse } from "@anthropic-ai/claude-agent-sdk";
43
+ /**
44
+ * How long the structured report may take before the original output wins.
45
+ *
46
+ * Not a guess at network latency: the request may never be serviced at all (see
47
+ * the header), so this is the ceiling on how long a user waits for a decoration
48
+ * on an answer that is already sitting in the buffer.
49
+ */
50
+ export declare const STRUCTURED_USAGE_TIMEOUT_MS = 5000;
51
+ /**
52
+ * Validate the experimental SDK response at the runtime boundary — way out 2.
53
+ * Null means "this is not a report this renderer can read", which the caller
54
+ * turns into Claude Code's original text.
55
+ */
56
+ export declare function parseUsageResponse(value: unknown): SDKControlGetUsageResponse | null;
57
+ /**
58
+ * Whether a prompt is exactly the local `/usage` command.
59
+ *
60
+ * Exact, not a prefix: `/usage now` is an argument form this renderer has no
61
+ * mapping for, and prose that merely mentions the word is a model turn.
62
+ */
63
+ export declare function isUsageCommandText(text: string): boolean;
64
+ /** Render the SDK's structured `/usage` response as Markdown (R2.3). */
65
+ export declare function formatUsageResponse(usage: SDKControlGetUsageResponse): string;
66
+ /**
67
+ * The one SDK surface this module needs.
68
+ *
69
+ * `Pick` on the literal method name is the compile-time tripwire the ToDo asks
70
+ * for: when the SDK stabilises this API and renames the method, the key stops
71
+ * being a `keyof Query` and the BUILD fails here. Without it, a rename would
72
+ * only ever surface as `/usage` degrading to way out 1 forever — a silent loss
73
+ * of the whole feature, indistinguishable from a session where the report is
74
+ * genuinely unavailable.
75
+ */
76
+ export type UsageReportQuery = Pick<Query, "usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET">;
77
+ /** The slice of the adapter's logger this module uses. */
78
+ export type UsageMarkdownLogger = {
79
+ error: (message: string) => void;
80
+ };
81
+ /**
82
+ * The structured `/usage` render for one turn, or null when any of the three
83
+ * ways out fired (R2.4). Never throws and never resolves late: the caller is
84
+ * holding a completed local command's output while it awaits this.
85
+ *
86
+ * Raced against BOTH a timeout and the turn's abort signal. The timeout covers
87
+ * a request that is never serviced (see the header); the signal covers a turn
88
+ * that ended — cancelled, or settled — while the request was still in flight,
89
+ * so a dead turn's overlay is abandoned rather than merely ignored.
90
+ */
91
+ export declare function structuredUsageMarkdown(query: UsageReportQuery, signal: AbortSignal, logger: UsageMarkdownLogger): Promise<string | null>;
92
+ //# sourceMappingURL=usage-markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-markdown.d.ts","sourceRoot":"","sources":["../src/usage-markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAKxF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AA6DjD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,GAAG,IAAI,CAOpF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AA0OD,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,0BAA0B,GAAG,MAAM,CAS7E;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,KAAK,EACL,2DAA2D,CAC5D,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,mBAAmB,GAAG;IAAE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CAAC;AAQvE;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwDxB"}
@@ -0,0 +1,373 @@
1
+ /**
2
+ * `/usage` rendered as Markdown, and the bounded wait that decides whether it
3
+ * renders at all (story 011, R2.3/R2.4, design D5).
4
+ *
5
+ * The command ALWAYS runs through Claude Code. What this module produces is a
6
+ * best-effort overlay on top of an answer the user already has, so every
7
+ * failure here costs the overlay and nothing else — the caller forwards Claude
8
+ * Code's own text byte-for-byte instead. Three ways out, all of them ending in
9
+ * `null` from {@link structuredUsageMarkdown}:
10
+ *
11
+ * 1. unavailable — the control request rejects, or the method is gone
12
+ * 2. incompatible — the response does not match the shape the renderer reads
13
+ * 3. slow — the response does not arrive inside the bound below
14
+ *
15
+ * THE BOUND IS LOAD-BEARING, NOT DEFENSIVE. Control requests on a fresh session
16
+ * are not serviced until the first turn runs (SDK issues #886/#880), and
17
+ * `/usage` is frequently that first turn — so an unbounded wait would withhold
18
+ * an answer the CLI had already printed, forever. That is why the race below
19
+ * has three legs and not one: the report, a timeout, and the turn's abort
20
+ * signal.
21
+ *
22
+ * ONE READER FOR THE REPORT'S NUMBERS. `account-usage.ts` already maps this same
23
+ * `SDKControlGetUsageResponse` onto the client's rate-limit `_meta`, and it owns
24
+ * the two scalar readings the report needs: `normalizeUtilization` (0..100 ->
25
+ * 0..1, clamped) and `toEpochSeconds` (ISO 8601 -> Unix seconds). Both are
26
+ * imported here rather than re-derived, so the bar this module draws and the bar
27
+ * the client's usage panel draws cannot disagree about what `utilization: 3`
28
+ * means. What is NOT shared is validation: `account-usage.ts` reads defensively
29
+ * and never rejects a report, because a missing field there costs one window,
30
+ * while here an unreadable report has to become a hard "no" so way out 2 can
31
+ * fire. Different questions, so different code — but the same answers to the
32
+ * two questions both of them ask.
33
+ *
34
+ * Kept self-contained — no import from `acp-agent.ts`, mirroring
35
+ * `thinking-option.ts` and `context-compaction.ts`. The reason is merge cost:
36
+ * `acp-agent.ts` is a ~9,700-line file that changes upstream almost daily, so
37
+ * every coupling to it is paid again at each sync. `acp-agent.ts` wires this in
38
+ * at the `/usage` turn: it decides which turn owns a structured render, and
39
+ * publishes the result at most once across the message shapes the SDK can
40
+ * deliver one local command through.
41
+ */
42
+ import { z } from "zod";
43
+ import { normalizeUtilization, toEpochSeconds } from "./account-usage.js";
44
+ /**
45
+ * How long the structured report may take before the original output wins.
46
+ *
47
+ * Not a guess at network latency: the request may never be serviced at all (see
48
+ * the header), so this is the ceiling on how long a user waits for a decoration
49
+ * on an answer that is already sitting in the buffer.
50
+ */
51
+ export const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
52
+ const countSchema = z.number().finite().nonnegative();
53
+ const percentSchema = countSchema.max(100);
54
+ const usageWindowSchema = z
55
+ .object({ utilization: percentSchema.nullable(), resets_at: z.string().nullable() })
56
+ .nullable()
57
+ .optional();
58
+ const contributionSchema = z.object({ name: z.string(), pct: percentSchema });
59
+ const behaviorPeriodSchema = z.object({
60
+ request_count: countSchema,
61
+ session_count: countSchema,
62
+ mcp_servers: z.array(contributionSchema),
63
+ });
64
+ const modelUsageSchema = z.object({
65
+ inputTokens: countSchema,
66
+ outputTokens: countSchema,
67
+ cacheReadInputTokens: countSchema,
68
+ cacheCreationInputTokens: countSchema,
69
+ });
70
+ const usageResponseSchema = z.object({
71
+ session: z.object({
72
+ total_cost_usd: countSchema,
73
+ total_api_duration_ms: countSchema,
74
+ total_duration_ms: countSchema,
75
+ model_usage: z.record(z.string(), modelUsageSchema),
76
+ }),
77
+ subscription_type: z.string().nullable(),
78
+ rate_limits_available: z.boolean(),
79
+ rate_limits: z
80
+ .object({
81
+ five_hour: usageWindowSchema,
82
+ seven_day: usageWindowSchema,
83
+ seven_day_oauth_apps: usageWindowSchema,
84
+ seven_day_opus: usageWindowSchema,
85
+ seven_day_sonnet: usageWindowSchema,
86
+ model_scoped: z
87
+ .array(z.object({
88
+ display_name: z.string(),
89
+ utilization: percentSchema.nullable(),
90
+ resets_at: z.string().nullable(),
91
+ }))
92
+ .optional(),
93
+ extra_usage: z
94
+ .object({
95
+ is_enabled: z.boolean(),
96
+ monthly_limit: countSchema.nullable(),
97
+ used_credits: countSchema.nullable(),
98
+ utilization: percentSchema.nullable(),
99
+ currency: z.string().nullable().optional(),
100
+ })
101
+ .nullable()
102
+ .optional(),
103
+ })
104
+ .nullable(),
105
+ behaviors: z.object({ day: behaviorPeriodSchema, week: behaviorPeriodSchema }).nullable(),
106
+ });
107
+ /**
108
+ * Validate the experimental SDK response at the runtime boundary — way out 2.
109
+ * Null means "this is not a report this renderer can read", which the caller
110
+ * turns into Claude Code's original text.
111
+ */
112
+ export function parseUsageResponse(value) {
113
+ const parsed = usageResponseSchema.safeParse(value);
114
+ // Validate only the fields the renderer reads, but preserve the COMPLETE
115
+ // response rather than the schema's stripped output. The API's own doc
116
+ // comment says the shape may change; a value added to a subtree nobody reads
117
+ // must not cost the already-validated part that renders fine.
118
+ return parsed.success ? value : null;
119
+ }
120
+ /**
121
+ * Whether a prompt is exactly the local `/usage` command.
122
+ *
123
+ * Exact, not a prefix: `/usage now` is an argument form this renderer has no
124
+ * mapping for, and prose that merely mentions the word is a model turn.
125
+ */
126
+ export function isUsageCommandText(text) {
127
+ return text.trim() === "/usage";
128
+ }
129
+ /** Cells in a progress bar. Fixed width, so bars line up under one another. */
130
+ const USAGE_BAR_CELLS = 20;
131
+ /**
132
+ * A bar for a utilization already normalised to 0..1 by
133
+ * {@link normalizeUtilization} — which is what keeps this bar and the client's
134
+ * own rate-limit bar on the same scale.
135
+ *
136
+ * A non-zero fraction always fills at least one cell: rounding 0.6% to an empty
137
+ * bar would render "some usage" and "none" identically.
138
+ */
139
+ function usageBar(fraction) {
140
+ const filled = fraction === 0 ? 0 : Math.max(1, Math.round(fraction * USAGE_BAR_CELLS));
141
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(USAGE_BAR_CELLS - filled)}`;
142
+ }
143
+ /** Server-supplied text is DATA, not Markdown: neutralise it before inlining. */
144
+ function escapeMarkdown(value) {
145
+ return value.replace(/([\\`*_[\]<>|])/g, "\\$1").replace(/[\r\n]+/g, " ");
146
+ }
147
+ /** Milliseconds per second — {@link toEpochSeconds} answers in the latter. */
148
+ const MILLISECONDS_PER_SECOND = 1000;
149
+ const SECONDS_PER_MINUTE = 60;
150
+ function formatDuration(milliseconds) {
151
+ const seconds = Math.max(0, Math.round(milliseconds / MILLISECONDS_PER_SECOND));
152
+ if (seconds < SECONDS_PER_MINUTE) {
153
+ return `${seconds}s`;
154
+ }
155
+ const minutes = Math.floor(seconds / SECONDS_PER_MINUTE);
156
+ const remainder = seconds % SECONDS_PER_MINUTE;
157
+ return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
158
+ }
159
+ const COUNT_FORMAT = new Intl.NumberFormat("en", {
160
+ notation: "compact",
161
+ maximumFractionDigits: 1,
162
+ });
163
+ const RESET_FORMAT = new Intl.DateTimeFormat("en", {
164
+ month: "short",
165
+ day: "numeric",
166
+ hour: "numeric",
167
+ minute: "2-digit",
168
+ timeZoneName: "short",
169
+ });
170
+ function formatCount(value) {
171
+ return COUNT_FORMAT.format(value);
172
+ }
173
+ /**
174
+ * The reset suffix for a limit row, or empty when the report carried no instant.
175
+ *
176
+ * Parsed through {@link toEpochSeconds} so this module and the quota-window
177
+ * transport agree on which instants are readable at all. An instant that reader
178
+ * rejects is still SHOWN, verbatim and escaped: the report said something, and
179
+ * printing it is more useful than silently dropping it.
180
+ */
181
+ function formatReset(value) {
182
+ if (!value) {
183
+ return "";
184
+ }
185
+ const seconds = toEpochSeconds(value);
186
+ if (seconds === undefined) {
187
+ return ` \u00b7 Resets ${escapeMarkdown(value)}`;
188
+ }
189
+ return ` \u00b7 Resets ${RESET_FORMAT.format(new Date(seconds * MILLISECONDS_PER_SECOND))}`;
190
+ }
191
+ /**
192
+ * One limit row — heading, then its bar — or nothing when the report supplied no
193
+ * usable utilization for that window.
194
+ *
195
+ * The percentage is printed as the report stated it while the BAR is drawn from
196
+ * the normalised fraction. That split is deliberate: the number is the report's
197
+ * own claim, the bar is the shared 0..1 scale, and `normalizeUtilization` is the
198
+ * single place that decides whether a value is usable at all (null, absent and
199
+ * non-finite all collapse to "no row").
200
+ */
201
+ function appendLimit(lines, label, window) {
202
+ const fraction = normalizeUtilization(window?.utilization);
203
+ if (!window || fraction === undefined) {
204
+ return;
205
+ }
206
+ lines.push(`**${escapeMarkdown(label)}** \u2014 **${window.utilization}%**${formatReset(window.resets_at)}`, "", `\`${usageBar(fraction)}\``, "");
207
+ }
208
+ /** How many MCP servers one contribution table lists. */
209
+ const TOP_CONTRIBUTIONS = 3;
210
+ function appendContributions(lines, label, period) {
211
+ lines.push("", `**${label}** \u00b7 ${period.request_count} requests \u00b7 ${period.session_count} sessions`);
212
+ if (period.mcp_servers.length === 0) {
213
+ return;
214
+ }
215
+ lines.push("", "| MCP server | Usage |", "|:--|--:|");
216
+ // Copied before sorting: the report is the caller's object, not ours.
217
+ const ranked = [...period.mcp_servers].sort((a, b) => b.pct - a.pct);
218
+ for (const server of ranked.slice(0, TOP_CONTRIBUTIONS)) {
219
+ const bar = usageBar(normalizeUtilization(server.pct) ?? 0);
220
+ lines.push(`| ${escapeMarkdown(server.name)} | \`${bar}\` ${server.pct}% |`);
221
+ }
222
+ }
223
+ /**
224
+ * The session's own spend: one row of wall-clock facts, then the token
225
+ * breakdown summed across every model the session used.
226
+ *
227
+ * Summed rather than listed per model: the question `/usage` answers is what
228
+ * this session cost, and a per-model split of four counters is a table nobody
229
+ * reads to answer it.
230
+ */
231
+ function appendSessionTotals(lines, session) {
232
+ const totals = Object.values(session.model_usage).reduce((sum, model) => ({
233
+ input: sum.input + model.inputTokens,
234
+ output: sum.output + model.outputTokens,
235
+ cacheRead: sum.cacheRead + model.cacheReadInputTokens,
236
+ cacheWrite: sum.cacheWrite + model.cacheCreationInputTokens,
237
+ }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
238
+ lines.push("", "---", "", "### This session", "", "| Cost | API time | Active |", "|:--|:--|:--|", `| $${session.total_cost_usd.toFixed(2)} | ${formatDuration(session.total_api_duration_ms)} | ${formatDuration(session.total_duration_ms)} |`, "", "| Breakdown | Tokens |", "|:--|--:|", `| Input | ${formatCount(totals.input)} |`, `| Output | ${formatCount(totals.output)} |`, `| Cache read | ${formatCount(totals.cacheRead)} |`, `| Cache write | ${formatCount(totals.cacheWrite)} |`);
239
+ }
240
+ /**
241
+ * The plan's rate-limit windows as labelled bars, or nothing at all.
242
+ *
243
+ * `rate_limits_available` GOVERNS, not the presence of data: an API-key,
244
+ * Bedrock or Vertex session can carry a populated `rate_limits` while declaring
245
+ * plan limits inapplicable, and a section of bars would then claim the account
246
+ * is unused rather than unmeasured. Same rule `account-usage.ts` applies.
247
+ */
248
+ function appendLimits(lines, usage) {
249
+ if (!usage.rate_limits_available || !usage.rate_limits) {
250
+ return;
251
+ }
252
+ const limitLines = [];
253
+ appendLimit(limitLines, "5-hour limit", usage.rate_limits.five_hour);
254
+ appendLimit(limitLines, "Weekly \u00b7 all models", usage.rate_limits.seven_day);
255
+ // The server's own per-model buckets when it emits them; the two fixed fields
256
+ // are the older shape and would double-report the same windows.
257
+ const modelWindows = usage.rate_limits.model_scoped ?? [];
258
+ for (const model of modelWindows) {
259
+ appendLimit(limitLines, `Weekly \u00b7 ${model.display_name}`, model);
260
+ }
261
+ if (modelWindows.length === 0) {
262
+ appendLimit(limitLines, "Weekly \u00b7 Opus", usage.rate_limits.seven_day_opus);
263
+ appendLimit(limitLines, "Weekly \u00b7 Sonnet", usage.rate_limits.seven_day_sonnet);
264
+ }
265
+ // A heading with no rows under it claims the limits are unmeasured rather
266
+ // than unavailable, so the section only exists once a row does.
267
+ if (limitLines.length === 0) {
268
+ return;
269
+ }
270
+ if (limitLines.at(-1) === "") {
271
+ limitLines.pop();
272
+ }
273
+ lines.push("", "### Limits", "", ...limitLines);
274
+ }
275
+ /**
276
+ * What is consuming the plan's limits, as the CLI's own dialog reports it.
277
+ *
278
+ * Null for a session with no claude.ai subscription, or when the local
279
+ * transcript scan behind it failed — in both cases the section is simply
280
+ * absent rather than shown empty.
281
+ */
282
+ function appendBehaviors(lines, usage) {
283
+ if (!usage.behaviors) {
284
+ return;
285
+ }
286
+ lines.push("", "---", "", "### What\u2019s using your limits?", "", "> Approximate, overlapping measures \u00b7 this machine only \u00b7 excludes claude.ai");
287
+ appendContributions(lines, "Last 24h", usage.behaviors.day);
288
+ appendContributions(lines, "Last 7d", usage.behaviors.week);
289
+ }
290
+ /** Render the SDK's structured `/usage` response as Markdown (R2.3). */
291
+ export function formatUsageResponse(usage) {
292
+ const lines = ["## Usage"];
293
+ if (usage.subscription_type) {
294
+ lines.push("", `> Claude ${escapeMarkdown(usage.subscription_type)} subscription usage`);
295
+ }
296
+ appendLimits(lines, usage);
297
+ appendSessionTotals(lines, usage.session);
298
+ appendBehaviors(lines, usage);
299
+ return lines.join("\n");
300
+ }
301
+ /**
302
+ * What the race yields when no report arrived. A symbol rather than `null`, so
303
+ * "the wait ended" can never be confused with a value the SDK sent.
304
+ */
305
+ const NO_REPORT = Symbol("structured-usage-unavailable");
306
+ /**
307
+ * The structured `/usage` render for one turn, or null when any of the three
308
+ * ways out fired (R2.4). Never throws and never resolves late: the caller is
309
+ * holding a completed local command's output while it awaits this.
310
+ *
311
+ * Raced against BOTH a timeout and the turn's abort signal. The timeout covers
312
+ * a request that is never serviced (see the header); the signal covers a turn
313
+ * that ended — cancelled, or settled — while the request was still in flight,
314
+ * so a dead turn's overlay is abandoned rather than merely ignored.
315
+ */
316
+ export async function structuredUsageMarkdown(query, signal, logger) {
317
+ if (signal.aborted) {
318
+ return null;
319
+ }
320
+ let timer;
321
+ let onAbort;
322
+ try {
323
+ const response = await Promise.race([
324
+ // Written out rather than hidden behind a helper, for the same reason
325
+ // `publishAccountUsage` writes it out: this awful spelling is the only
326
+ // name the real `Query` answers to, and `UsageReportQuery` above pins the
327
+ // same literal.
328
+ query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(),
329
+ new Promise((resolve) => {
330
+ timer = setTimeout(() => resolve(NO_REPORT), STRUCTURED_USAGE_TIMEOUT_MS);
331
+ // The overlay must never be the reason the process stays alive.
332
+ timer.unref?.();
333
+ }),
334
+ new Promise((resolve) => {
335
+ onAbort = () => resolve(NO_REPORT);
336
+ // `addEventListener` never fires on an already-aborted signal, so an
337
+ // abort raised between the guard above and this line would otherwise
338
+ // leave this leg permanently pending.
339
+ if (signal.aborted) {
340
+ onAbort();
341
+ }
342
+ else {
343
+ signal.addEventListener("abort", onAbort, { once: true });
344
+ }
345
+ }),
346
+ ]);
347
+ if (response === NO_REPORT) {
348
+ // An aborted turn is not a fault: nobody is waiting for this any more.
349
+ if (!signal.aborted) {
350
+ logger.error("Structured /usage timed out; preserving Claude Code output");
351
+ }
352
+ return null;
353
+ }
354
+ const usage = parseUsageResponse(response);
355
+ if (usage === null) {
356
+ logger.error("Structured /usage returned an incompatible response; preserving Claude Code output");
357
+ return null;
358
+ }
359
+ return formatUsageResponse(usage);
360
+ }
361
+ catch (error) {
362
+ logger.error(`Structured /usage failed; preserving Claude Code output: ${error}`);
363
+ return null;
364
+ }
365
+ finally {
366
+ if (timer !== undefined) {
367
+ clearTimeout(timer);
368
+ }
369
+ if (onAbort !== undefined) {
370
+ signal.removeEventListener("abort", onAbort);
371
+ }
372
+ }
373
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.11.1",
6
+ "version": "0.13.0",
7
7
  "description": "ACP adapter for the Claude Agent SDK with VS Code-parity UX for Zed and other ACP clients — checkbox multi-select questions, refusal consent dialog, dynamic agent name. Fork of claude-agent-acp.",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",
@@ -68,19 +68,19 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@agentclientprotocol/sdk": "1.4.0",
71
- "@anthropic-ai/claude-agent-sdk": "0.3.250",
72
- "zod": "^3.25.0 || ^4.0.0"
71
+ "@anthropic-ai/claude-agent-sdk": "0.3.261",
72
+ "zod": "^4.0.0"
73
73
  },
74
74
  "devDependencies": {
75
- "@anthropic-ai/sdk": "0.122.0",
75
+ "@anthropic-ai/sdk": "0.124.0",
76
76
  "@eslint/js": "10.0.1",
77
77
  "@tsconfig/node24": "24.0.5",
78
- "@types/node": "26.4.0",
79
- "@typescript-eslint/eslint-plugin": "8.68.0",
80
- "@typescript-eslint/parser": "8.68.0",
81
- "eslint": "10.9.1",
78
+ "@types/node": "26.4.1",
79
+ "@typescript-eslint/eslint-plugin": "8.69.0",
80
+ "@typescript-eslint/parser": "8.69.0",
81
+ "eslint": "10.10.0",
82
82
  "eslint-config-prettier": "10.1.8",
83
- "globals": "17.11.0",
83
+ "globals": "17.12.0",
84
84
  "prettier": "3.9.6",
85
85
  "ts-node": "10.9.2",
86
86
  "typescript": "6.0.3",