@agentclientprotocol/claude-agent-acp 0.74.0 → 0.75.1

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.
@@ -1,41 +1,143 @@
1
1
  import { RequestError } from "@agentclientprotocol/sdk";
2
- import { forkSession as forkClaudeSession, getSessionMessages, } from "@anthropic-ai/claude-agent-sdk";
3
- function forkPointMessageId(meta) {
2
+ import { forkSession as forkClaudeSession, getSessionMessages, importSessionToStore, } from "@anthropic-ai/claude-agent-sdk";
3
+ import { createHash } from "node:crypto";
4
+ import { assistantMessageText } from "./session-failure-extension.js";
5
+ import { SessionTiming } from "./session-timing.js";
6
+ function forkPoint(meta) {
4
7
  const fork = meta?.jetbrains?.air?.fork;
5
8
  if (fork?.version !== 1)
6
9
  return undefined;
7
10
  const messageId = fork.messageId?.trim();
8
- return messageId || undefined;
11
+ if (!messageId)
12
+ return undefined;
13
+ const messageFingerprint = fork.messageFingerprint?.trim();
14
+ const messageOccurrence = fork.messageOccurrence;
15
+ return {
16
+ messageId,
17
+ ...(messageFingerprint ? { messageFingerprint } : {}),
18
+ // AIR computes this with count(...) over the visible prefix, so it is 1-based.
19
+ ...(typeof messageOccurrence === "number" &&
20
+ Number.isSafeInteger(messageOccurrence) &&
21
+ messageOccurrence > 0
22
+ ? { messageOccurrence }
23
+ : {}),
24
+ };
9
25
  }
10
26
  function forkPointMessageIdCandidates(messageId) {
11
27
  // Older AIR builds sent their visible segment id. Prefer the exact id before its ACP source id.
12
28
  const protocolMessageId = messageId.replace(/:segment:\d+$/, "");
13
29
  return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId];
14
30
  }
31
+ async function loadFullSessionHistory(sessionId, cwd) {
32
+ // getSessionMessages returns only the active parentUuid chain. The import keeps inactive branches that AIR can still reference.
33
+ const entries = [];
34
+ const store = {
35
+ append: async (_key, batch) => {
36
+ entries.push(...batch);
37
+ },
38
+ load: async () => null,
39
+ };
40
+ await importSessionToStore(sessionId, store, { dir: cwd, includeSubagents: false });
41
+ return entries;
42
+ }
43
+ function assistantGroups(entries, messageIdForGrouping) {
44
+ const groups = new Map();
45
+ for (const entry of entries) {
46
+ if (entry.type !== "assistant" ||
47
+ entry.isSidechain === true ||
48
+ entry.parent_tool_use_id != null ||
49
+ entry.parent_agent_id != null) {
50
+ continue;
51
+ }
52
+ const messageId = messageIdForGrouping(entry);
53
+ if (!messageId)
54
+ continue;
55
+ const group = groups.get(messageId) ?? { text: "" };
56
+ if (typeof entry.uuid === "string" && entry.uuid.length > 0)
57
+ group.uuid = entry.uuid;
58
+ group.text += assistantMessageText(entry.message) ?? "";
59
+ groups.set(messageId, group);
60
+ }
61
+ return groups;
62
+ }
63
+ function fingerprint(text) {
64
+ return `sha256:${createHash("sha256").update(text).digest("hex")}`;
65
+ }
66
+ function fingerprintOccurrenceOnBranch(targetUuid, expectedFingerprint, entries, groups, messageIdForGrouping) {
67
+ const entriesByUuid = new Map(entries.flatMap((entry) => (typeof entry.uuid === "string" ? [[entry.uuid, entry]] : [])));
68
+ const seenGroups = new Set();
69
+ let occurrence = 0;
70
+ let cursor = targetUuid;
71
+ while (cursor) {
72
+ const entry = entriesByUuid.get(cursor);
73
+ if (!entry)
74
+ break;
75
+ if (entry.type === "assistant" &&
76
+ entry.isSidechain !== true &&
77
+ entry.parent_tool_use_id == null &&
78
+ entry.parent_agent_id == null) {
79
+ const messageId = messageIdForGrouping(entry);
80
+ if (messageId && !seenGroups.has(messageId)) {
81
+ seenGroups.add(messageId);
82
+ const group = groups.get(messageId);
83
+ if (group && fingerprint(group.text) === expectedFingerprint)
84
+ occurrence++;
85
+ }
86
+ }
87
+ cursor = typeof entry.parentUuid === "string" ? entry.parentUuid : undefined;
88
+ }
89
+ return occurrence;
90
+ }
91
+ function resolveFromFullHistory(entries, candidates, point, messageIdForGrouping) {
92
+ const groups = assistantGroups(entries, messageIdForGrouping);
93
+ const exact = candidates.map((candidate) => groups.get(candidate)?.uuid).find(Boolean);
94
+ if (exact)
95
+ return exact;
96
+ if (!point.messageFingerprint || !point.messageOccurrence)
97
+ return undefined;
98
+ const fingerprintMatches = [...groups.values()].filter((group) => group.uuid && fingerprint(group.text) === point.messageFingerprint);
99
+ // If only one persisted message has this content, the fingerprint already
100
+ // identifies it even when an older client supplied an incompatible index.
101
+ if (fingerprintMatches.length === 1)
102
+ return fingerprintMatches[0]?.uuid;
103
+ const occurrenceMatches = fingerprintMatches.filter((group) => group.uuid &&
104
+ fingerprintOccurrenceOnBranch(group.uuid, point.messageFingerprint, entries, groups, messageIdForGrouping) === point.messageOccurrence);
105
+ return occurrenceMatches.length === 1 ? occurrenceMatches[0]?.uuid : undefined;
106
+ }
15
107
  export async function forkSession(params, dependencies) {
16
- const messageId = forkPointMessageId(params._meta);
17
- if (!messageId) {
108
+ const timing = new SessionTiming(dependencies.logger, "fork", params.sessionId);
109
+ const point = forkPoint(params._meta);
110
+ if (!point) {
18
111
  const forked = await forkClaudeSession(params.sessionId, { dir: params.cwd });
112
+ timing.phase("sdk-fork", " resolution=latest");
19
113
  return { sessionId: forked.sessionId };
20
114
  }
21
- const candidateIds = forkPointMessageIdCandidates(messageId);
115
+ const candidateIds = forkPointMessageIdCandidates(point.messageId);
22
116
  const liveUuid = candidateIds
23
117
  .map((candidateId) => dependencies.liveMessageIdToUuid?.get(candidateId))
24
118
  .find(Boolean);
25
119
  const history = liveUuid
26
120
  ? undefined
27
121
  : await getSessionMessages(params.sessionId, { dir: params.cwd });
122
+ timing.phase("active-history", ` resolution=${liveUuid ? "live" : "pending"}`);
28
123
  const messageUuid = liveUuid ??
29
124
  candidateIds
30
- .map((candidateId) => history?.find((message) => dependencies.messageIdForGrouping(message) === candidateId)
31
- ?.uuid)
125
+ .map((candidateId) => history
126
+ ?.slice()
127
+ .reverse()
128
+ .find((message) => dependencies.messageIdForGrouping(message) === candidateId)?.uuid)
32
129
  .find(Boolean);
33
- if (!messageUuid) {
34
- throw RequestError.invalidParams({ messageId }, `Fork point message ${messageId} was not found in session ${params.sessionId}`);
130
+ const fullHistoryUuid = messageUuid
131
+ ? undefined
132
+ : resolveFromFullHistory(await loadFullSessionHistory(params.sessionId, params.cwd), candidateIds, point, dependencies.messageIdForGrouping);
133
+ timing.phase("full-history", ` resolution=${liveUuid ? "live" : messageUuid ? "active" : fullHistoryUuid ? "full" : "missing"}`);
134
+ if (!messageUuid && !fullHistoryUuid) {
135
+ throw RequestError.invalidParams({ messageId: point.messageId }, `Fork point message ${point.messageId} was not found in session ${params.sessionId}`);
35
136
  }
36
137
  const forked = await forkClaudeSession(params.sessionId, {
37
138
  dir: params.cwd,
38
- upToMessageId: messageUuid,
139
+ upToMessageId: messageUuid ?? fullHistoryUuid,
39
140
  });
141
+ timing.phase("sdk-fork");
40
142
  return { sessionId: forked.sessionId };
41
143
  }
@@ -21,13 +21,14 @@
21
21
  * one, because the first turn was the one that signed out, the recreation
22
22
  * starts a fresh query under the same session id instead.
23
23
  *
24
- * One case is left open. The CLI re-reads its credential store on its own. A
25
- * file-based credential can therefore be removed and replaced by a claude.ai
26
- * subscription between two turns. This covers a managed Console key and a key
27
- * from a settings helper. No turn fails in between, so nothing ends the query,
28
- * and the cached account still names the key that is gone. Closing this case
29
- * would cost one `initialize` control request per turn. That buys too little
30
- * for the price.
24
+ * A file-based credential replaced by a subscription between two turns is seen
25
+ * by the CLI probe the agent fires at the START of every user prompt. That read
26
+ * is never awaited, so it usually lands mid-turn; all it does there is mark the
27
+ * session. The guard consumes the mark at the turn boundary — the next prompt
28
+ * recreates the query and layer 1 judges the account the new `initialize`
29
+ * reports — so a probe never interrupts a running turn. A swap that happens
30
+ * after the last probe stays unseen until the next prompt reads the store
31
+ * again. Closing that window would cost a blocking read per turn.
31
32
  */
32
33
  import { RequestError } from "@agentclientprotocol/sdk";
33
34
  import type { AccountInfo, Query } from "@anthropic-ai/claude-agent-sdk";
@@ -114,6 +115,12 @@ type GuardOptions = {
114
115
  /** Built lazily: most turns pass and never need a controller. */
115
116
  sessionFailures: () => SessionFailureController;
116
117
  logger: GuardLogger;
118
+ /** Display hook: called with the account the guard just read, before the
119
+ * guard decides on it. It exists so a refused turn still reports which
120
+ * account it was refused for. Purely observational — this module knows
121
+ * nothing about what the caller does with it, and a throw from here never
122
+ * changes the verdict. */
123
+ onAccount?: (account: AccountInfo) => void;
117
124
  };
118
125
  /**
119
126
  * The per-turn guard for a live session.
@@ -1 +1 @@
1
- {"version":3,"file":"hide-claude-auth.d.ts","sourceRoot":"","sources":["../src/hide-claude-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE/E,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wCAAwC,sCAAsC,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,OAAO,wCAAwC,CAAC;AAEjF,eAAO,MAAM,yCAAyC,qEACc,CAAC;AAErE;;qEAEqE;AACrE,eAAO,MAAM,6BAA6B,0EAC+B,CAAC;AAmB1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWxF;AAED,wBAAgB,mCAAmC,IAAI,YAAY,CAKlE;AAED;oEACoE;AACpE,wBAAgB,wBAAwB,IAAI,YAAY,CAEvD;AAED;;;;GAIG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACpC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,4BAA4B,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,IAAI,CAWP;AAED,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IAClC,UAAU,EAAE,4BAA4B,CAAC;IACzC,iEAAiE;IACjE,eAAe,EAAE,MAAM,wBAAwB,CAAC;IAChD,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAavF"}
1
+ {"version":3,"file":"hide-claude-auth.d.ts","sourceRoot":"","sources":["../src/hide-claude-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE/E,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wCAAwC,sCAAsC,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,OAAO,wCAAwC,CAAC;AAEjF,eAAO,MAAM,yCAAyC,qEACc,CAAC;AAErE;;qEAEqE;AACrE,eAAO,MAAM,6BAA6B,0EAC+B,CAAC;AAmB1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWxF;AAED,wBAAgB,mCAAmC,IAAI,YAAY,CAKlE;AAED;oEACoE;AACpE,wBAAgB,wBAAwB,IAAI,YAAY,CAEvD;AAED;;;;GAIG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACpC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,4BAA4B,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,IAAI,CAWP;AAED,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IAClC,UAAU,EAAE,4BAA4B,CAAC;IACzC,iEAAiE;IACjE,eAAe,EAAE,MAAM,wBAAwB,CAAC;IAChD,MAAM,EAAE,WAAW,CAAC;IACpB;;;;+BAI2B;IAC3B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CAC5C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAavF"}
@@ -21,13 +21,14 @@
21
21
  * one, because the first turn was the one that signed out, the recreation
22
22
  * starts a fresh query under the same session id instead.
23
23
  *
24
- * One case is left open. The CLI re-reads its credential store on its own. A
25
- * file-based credential can therefore be removed and replaced by a claude.ai
26
- * subscription between two turns. This covers a managed Console key and a key
27
- * from a settings helper. No turn fails in between, so nothing ends the query,
28
- * and the cached account still names the key that is gone. Closing this case
29
- * would cost one `initialize` control request per turn. That buys too little
30
- * for the price.
24
+ * A file-based credential replaced by a subscription between two turns is seen
25
+ * by the CLI probe the agent fires at the START of every user prompt. That read
26
+ * is never awaited, so it usually lands mid-turn; all it does there is mark the
27
+ * session. The guard consumes the mark at the turn boundary — the next prompt
28
+ * recreates the query and layer 1 judges the account the new `initialize`
29
+ * reports — so a probe never interrupts a running turn. A swap that happens
30
+ * after the last probe stays unseen until the next prompt reads the store
31
+ * again. Closing that window would cost a blocking read per turn.
31
32
  */
32
33
  import { RequestError } from "@agentclientprotocol/sdk";
33
34
  export function shouldHideClaudeAuth() {
@@ -185,6 +186,14 @@ async function evaluateGuard(options) {
185
186
  if (!account) {
186
187
  return undefined;
187
188
  }
189
+ // Before the decision, so the client learns the identity even when the next
190
+ // lines refuse the turn. A display hook must never fail a guard.
191
+ try {
192
+ options.onAccount?.(account);
193
+ }
194
+ catch (error) {
195
+ options.logger.error(`Session ${options.sessionId}: the guard's onAccount hook threw:`, error instanceof Error ? error.message : String(error));
196
+ }
188
197
  if (billsClaudeSubscription(account)) {
189
198
  await publishRefusal(options.sessionFailures(), {
190
199
  details: CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE,
package/dist/lib.d.ts CHANGED
@@ -2,5 +2,6 @@ export { ClaudeAcpAgent, isLocalCommandMetadata, stripLocalCommandMetadata, runA
2
2
  export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
3
3
  export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
4
4
  export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
5
+ export { AUTH_STATUS_UPDATE_METHOD, type AuthStatus, type AuthStatusAccount, type AuthStatusKind, type AuthStatusUpdateNotification, } from "./auth-status.js";
5
6
  export type { ClaudePlanEntry } from "./tools.js";
6
7
  //# sourceMappingURL=lib.d.ts.map
package/dist/lib.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,MAAM,EACN,kBAAkB,EAClB,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG7E,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,MAAM,EACN,kBAAkB,EAClB,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG7E,OAAO,EACL,yBAAyB,EACzB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,4BAA4B,GAClC,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
package/dist/lib.js CHANGED
@@ -3,3 +3,6 @@ export { ClaudeAcpAgent, isLocalCommandMetadata, stripLocalCommandMetadata, runA
3
3
  export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
4
4
  export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
5
5
  export { SettingsManager } from "./settings.js";
6
+ // The `authStatus` extension's wire surface: the notification's method name and
7
+ // the payload types needed to read the traffic. The mappers stay internal.
8
+ export { AUTH_STATUS_UPDATE_METHOD, } from "./auth-status.js";
@@ -0,0 +1,19 @@
1
+ import { type SessionMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ type ResumeLogger = {
3
+ log: (...args: unknown[]) => void;
4
+ error: (...args: unknown[]) => void;
5
+ };
6
+ export type ResumedSessionSnapshot = {
7
+ messages?: SessionMessage[];
8
+ model?: string;
9
+ };
10
+ /** Return the concrete model recorded by the last real assistant response.
11
+ * Claude Code restores a resumed query from this same transcript field.
12
+ * Synthetic assistant records use angle-bracket placeholders and do not
13
+ * describe a model the resumed query can run. */
14
+ export declare function resumedModelFromTranscript(messages: SessionMessage[]): string | undefined;
15
+ /** Read the resume model from the local transcript without starting a Claude
16
+ * control request. This is intentionally on the load critical path. */
17
+ export declare function readResumedSession(sessionId: string, logger?: ResumeLogger): Promise<ResumedSessionSnapshot>;
18
+ export {};
19
+ //# sourceMappingURL=resumed-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumed-session.d.ts","sourceRoot":"","sources":["../src/resumed-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGzF,KAAK,YAAY,GAAG;IAClB,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;iDAGiD;AACjD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,SAAS,CAkBzF;AAED;uEACuE;AACvE,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,sBAAsB,CAAC,CAejC"}
@@ -0,0 +1,42 @@
1
+ import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
2
+ import { SessionTiming } from "./session-timing.js";
3
+ /** Return the concrete model recorded by the last real assistant response.
4
+ * Claude Code restores a resumed query from this same transcript field.
5
+ * Synthetic assistant records use angle-bracket placeholders and do not
6
+ * describe a model the resumed query can run. */
7
+ export function resumedModelFromTranscript(messages) {
8
+ for (let index = messages.length - 1; index >= 0; index--) {
9
+ const entry = messages[index];
10
+ if (entry?.type !== "assistant" ||
11
+ entry.parent_tool_use_id != null ||
12
+ entry.parent_agent_id != null ||
13
+ !entry.message ||
14
+ typeof entry.message !== "object") {
15
+ continue;
16
+ }
17
+ const model = entry.message.model;
18
+ if (typeof model === "string" && model.trim().length > 0 && !/^<[^>]+>$/.test(model.trim())) {
19
+ return model.trim();
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ /** Read the resume model from the local transcript without starting a Claude
25
+ * control request. This is intentionally on the load critical path. */
26
+ export async function readResumedSession(sessionId, logger) {
27
+ const timing = new SessionTiming(logger, "models", sessionId);
28
+ try {
29
+ // Deliberately search all project directories, matching replaySessionHistory.
30
+ // A client may reopen a session from a worktree or normalized path that is
31
+ // different from the directory under which Claude persisted the transcript.
32
+ const messages = await getSessionMessages(sessionId);
33
+ const model = resumedModelFromTranscript(messages);
34
+ timing.phase("read-transcript", ` messages=${messages.length} model=${model ?? "unknown"}`);
35
+ return { messages, model };
36
+ }
37
+ catch (error) {
38
+ timing.phase("read-transcript", " outcome=error");
39
+ logger?.error(`Failed to read transcript for resumed session ${sessionId}:`, error);
40
+ return {};
41
+ }
42
+ }
@@ -0,0 +1,15 @@
1
+ type TimingLogger = {
2
+ log: (...args: unknown[]) => void;
3
+ };
4
+ /** Small phase timer for session lifecycle diagnostics. */
5
+ export declare class SessionTiming {
6
+ private readonly logger;
7
+ private readonly operation;
8
+ private readonly sessionId;
9
+ private readonly startedAt;
10
+ private phaseStartedAt;
11
+ constructor(logger: TimingLogger | undefined, operation: string, sessionId: string, startedAt?: number);
12
+ phase(name: string, detail?: string): void;
13
+ }
14
+ export {};
15
+ //# sourceMappingURL=session-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-timing.d.ts","sourceRoot":"","sources":["../src/session-timing.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,2DAA2D;AAC3D,qBAAa,aAAa;IAKtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAN5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,cAAc,CAAS;gBAGZ,MAAM,EAAE,YAAY,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EAClC,SAAS,SAAoB;IAM/B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,IAAI;CAOvC"}
@@ -0,0 +1,20 @@
1
+ /** Small phase timer for session lifecycle diagnostics. */
2
+ export class SessionTiming {
3
+ logger;
4
+ operation;
5
+ sessionId;
6
+ startedAt;
7
+ phaseStartedAt;
8
+ constructor(logger, operation, sessionId, startedAt = performance.now()) {
9
+ this.logger = logger;
10
+ this.operation = operation;
11
+ this.sessionId = sessionId;
12
+ this.startedAt = startedAt;
13
+ this.phaseStartedAt = startedAt;
14
+ }
15
+ phase(name, detail = "") {
16
+ const finishedAt = performance.now();
17
+ this.logger?.log(`[session/${this.operation}] sessionId=${this.sessionId} phase=${name} durationMs=${Math.round(finishedAt - this.phaseStartedAt)} totalMs=${Math.round(finishedAt - this.startedAt)}${detail}`);
18
+ this.phaseStartedAt = finishedAt;
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { SDKControlGetUsageResponse } from "@anthropic-ai/claude-agent-sdk";
2
+ /** Validate the experimental SDK response at the runtime boundary. */
3
+ export declare function parseUsageResponse(value: unknown): SDKControlGetUsageResponse | null;
4
+ export declare function isUsageCommandText(text: string): boolean;
5
+ /** Render the SDK's structured `/usage` response as Markdown. */
6
+ export declare function formatUsageResponse(usage: SDKControlGetUsageResponse): string;
7
+ //# 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,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AA8DjF,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,GAAG,IAAI,CAMpF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AA0ED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,0BAA0B,GAAG,MAAM,CAiE7E"}
@@ -0,0 +1,154 @@
1
+ import { z } from "zod";
2
+ const countSchema = z.number().finite().nonnegative();
3
+ const percentSchema = countSchema.max(100);
4
+ const usageWindowSchema = z
5
+ .object({ utilization: percentSchema.nullable(), resets_at: z.string().nullable() })
6
+ .nullable()
7
+ .optional();
8
+ const contributionSchema = z.object({ name: z.string(), pct: percentSchema });
9
+ const behaviorPeriodSchema = z.object({
10
+ request_count: countSchema,
11
+ session_count: countSchema,
12
+ mcp_servers: z.array(contributionSchema),
13
+ });
14
+ const modelUsageSchema = z.object({
15
+ inputTokens: countSchema,
16
+ outputTokens: countSchema,
17
+ cacheReadInputTokens: countSchema,
18
+ cacheCreationInputTokens: countSchema,
19
+ });
20
+ const usageResponseSchema = z.object({
21
+ session: z.object({
22
+ total_cost_usd: countSchema,
23
+ total_api_duration_ms: countSchema,
24
+ total_duration_ms: countSchema,
25
+ model_usage: z.record(z.string(), modelUsageSchema),
26
+ }),
27
+ subscription_type: z.string().nullable(),
28
+ rate_limits_available: z.boolean(),
29
+ rate_limits: z
30
+ .object({
31
+ five_hour: usageWindowSchema,
32
+ seven_day: usageWindowSchema,
33
+ seven_day_oauth_apps: usageWindowSchema,
34
+ seven_day_opus: usageWindowSchema,
35
+ seven_day_sonnet: usageWindowSchema,
36
+ model_scoped: z
37
+ .array(z.object({
38
+ display_name: z.string(),
39
+ utilization: percentSchema.nullable(),
40
+ resets_at: z.string().nullable(),
41
+ }))
42
+ .optional(),
43
+ extra_usage: z
44
+ .object({
45
+ is_enabled: z.boolean(),
46
+ monthly_limit: countSchema.nullable(),
47
+ used_credits: countSchema.nullable(),
48
+ utilization: percentSchema.nullable(),
49
+ currency: z.string().nullable().optional(),
50
+ })
51
+ .nullable()
52
+ .optional(),
53
+ })
54
+ .nullable(),
55
+ behaviors: z.object({ day: behaviorPeriodSchema, week: behaviorPeriodSchema }).nullable(),
56
+ });
57
+ /** Validate the experimental SDK response at the runtime boundary. */
58
+ export function parseUsageResponse(value) {
59
+ const parsed = usageResponseSchema.safeParse(value);
60
+ // Validate only the fields the renderer reads, but preserve the complete
61
+ // response. The experimental API may add values to unused subtrees without
62
+ // making the useful, already-validated portion unsafe to render.
63
+ return parsed.success ? value : null;
64
+ }
65
+ export function isUsageCommandText(text) {
66
+ return text.trim() === "/usage";
67
+ }
68
+ function usageBar(percent) {
69
+ const cells = 20;
70
+ const clamped = Math.max(0, Math.min(100, percent));
71
+ const filled = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * cells));
72
+ return `${"█".repeat(filled)}${"░".repeat(cells - filled)}`;
73
+ }
74
+ function escapeMarkdown(value) {
75
+ return value.replace(/([\\`*_[\]<>|])/g, "\\$1").replace(/[\r\n]+/g, " ");
76
+ }
77
+ function formatDuration(milliseconds) {
78
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
79
+ if (seconds < 60)
80
+ return `${seconds}s`;
81
+ const minutes = Math.floor(seconds / 60);
82
+ const remainder = seconds % 60;
83
+ return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
84
+ }
85
+ function formatCount(value) {
86
+ return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
87
+ }
88
+ function formatReset(value) {
89
+ if (!value)
90
+ return "";
91
+ const reset = new Date(value);
92
+ if (Number.isNaN(reset.getTime()))
93
+ return ` · Resets ${escapeMarkdown(value)}`;
94
+ return ` · Resets ${new Intl.DateTimeFormat("en", {
95
+ month: "short",
96
+ day: "numeric",
97
+ hour: "numeric",
98
+ minute: "2-digit",
99
+ timeZoneName: "short",
100
+ }).format(reset)}`;
101
+ }
102
+ function appendLimit(lines, label, window) {
103
+ if (!window || window.utilization === null)
104
+ return;
105
+ lines.push(`**${escapeMarkdown(label)}** — **${window.utilization}%**${formatReset(window.resets_at)}`, "", `\`${usageBar(window.utilization)}\``, "");
106
+ }
107
+ function appendContributions(lines, label, period) {
108
+ lines.push("", `**${label}** · ${period.request_count} requests · ${period.session_count} sessions`);
109
+ if (period.mcp_servers.length === 0)
110
+ return;
111
+ lines.push("", "| MCP server | Usage |", "|:--|--:|");
112
+ for (const server of [...period.mcp_servers].sort((a, b) => b.pct - a.pct).slice(0, 3)) {
113
+ lines.push(`| ${escapeMarkdown(server.name)} | \`${usageBar(server.pct)}\` ${server.pct}% |`);
114
+ }
115
+ }
116
+ /** Render the SDK's structured `/usage` response as Markdown. */
117
+ export function formatUsageResponse(usage) {
118
+ const lines = ["## Usage"];
119
+ if (usage.subscription_type) {
120
+ lines.push("", `> Claude ${escapeMarkdown(usage.subscription_type)} subscription usage`);
121
+ }
122
+ if (usage.rate_limits_available && usage.rate_limits) {
123
+ const limitLines = [];
124
+ appendLimit(limitLines, "5-hour limit", usage.rate_limits.five_hour);
125
+ appendLimit(limitLines, "Weekly · all models", usage.rate_limits.seven_day);
126
+ const modelWindows = usage.rate_limits.model_scoped ?? [];
127
+ for (const model of modelWindows) {
128
+ appendLimit(limitLines, `Weekly · ${model.display_name}`, model);
129
+ }
130
+ if (modelWindows.length === 0) {
131
+ appendLimit(limitLines, "Weekly · Opus", usage.rate_limits.seven_day_opus);
132
+ appendLimit(limitLines, "Weekly · Sonnet", usage.rate_limits.seven_day_sonnet);
133
+ }
134
+ if (limitLines.length > 0) {
135
+ if (limitLines.at(-1) === "")
136
+ limitLines.pop();
137
+ lines.push("", "### Limits", "", ...limitLines);
138
+ }
139
+ }
140
+ const models = Object.values(usage.session.model_usage);
141
+ const totals = models.reduce((sum, model) => ({
142
+ input: sum.input + model.inputTokens,
143
+ output: sum.output + model.outputTokens,
144
+ cacheRead: sum.cacheRead + model.cacheReadInputTokens,
145
+ cacheWrite: sum.cacheWrite + model.cacheCreationInputTokens,
146
+ }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
147
+ lines.push("", "---", "", "### This session", "", "| Cost | API time | Active |", "|:--|:--|:--|", `| $${usage.session.total_cost_usd.toFixed(2)} | ${formatDuration(usage.session.total_api_duration_ms)} | ${formatDuration(usage.session.total_duration_ms)} |`, "", "| Breakdown | Tokens |", "|:--|--:|", `| Input | ${formatCount(totals.input)} |`, `| Output | ${formatCount(totals.output)} |`, `| Cache read | ${formatCount(totals.cacheRead)} |`, `| Cache write | ${formatCount(totals.cacheWrite)} |`);
148
+ if (usage.behaviors) {
149
+ lines.push("", "---", "", "### What’s using your limits?", "", "> Approximate, overlapping measures · this machine only · excludes claude.ai");
150
+ appendContributions(lines, "Last 24h", usage.behaviors.day);
151
+ appendContributions(lines, "Last 7d", usage.behaviors.week);
152
+ }
153
+ return lines.join("\n");
154
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.74.0",
6
+ "version": "0.75.1",
7
7
  "description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",