@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.
- package/dist/accounts.d.ts +143 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +196 -0
- package/dist/acp-agent.d.ts +76 -2
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +395 -94
- package/dist/context-compaction-meta.d.ts +73 -0
- package/dist/context-compaction-meta.d.ts.map +1 -0
- package/dist/context-compaction-meta.js +53 -0
- package/dist/context-compaction.d.ts +160 -0
- package/dist/context-compaction.d.ts.map +1 -0
- package/dist/context-compaction.js +275 -0
- package/dist/ultracode.d.ts +100 -0
- package/dist/ultracode.d.ts.map +1 -0
- package/dist/ultracode.js +103 -0
- package/dist/usage-markdown.d.ts +92 -0
- package/dist/usage-markdown.d.ts.map +1 -0
- package/dist/usage-markdown.js +373 -0
- package/package.json +9 -9
|
@@ -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.
|
|
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.
|
|
72
|
-
"zod": "^
|
|
71
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.261",
|
|
72
|
+
"zod": "^4.0.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@anthropic-ai/sdk": "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.
|
|
79
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
80
|
-
"@typescript-eslint/parser": "8.
|
|
81
|
-
"eslint": "10.
|
|
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.
|
|
83
|
+
"globals": "17.12.0",
|
|
84
84
|
"prettier": "3.9.6",
|
|
85
85
|
"ts-node": "10.9.2",
|
|
86
86
|
"typescript": "6.0.3",
|