@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.
- package/dist/acp-agent.d.ts +134 -14
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +540 -174
- package/dist/auth-status.d.ts +140 -0
- package/dist/auth-status.d.ts.map +1 -0
- package/dist/auth-status.js +271 -0
- package/dist/context-compaction-meta.d.ts +18 -0
- package/dist/context-compaction-meta.d.ts.map +1 -0
- package/dist/context-compaction-meta.js +15 -0
- package/dist/context-compaction.d.ts +43 -0
- package/dist/context-compaction.d.ts.map +1 -0
- package/dist/context-compaction.js +152 -0
- package/dist/fork-session.d.ts +3 -0
- package/dist/fork-session.d.ts.map +1 -1
- package/dist/fork-session.js +113 -11
- package/dist/hide-claude-auth.d.ts +14 -7
- package/dist/hide-claude-auth.d.ts.map +1 -1
- package/dist/hide-claude-auth.js +16 -7
- package/dist/lib.d.ts +1 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +3 -0
- package/dist/resumed-session.d.ts +19 -0
- package/dist/resumed-session.d.ts.map +1 -0
- package/dist/resumed-session.js +42 -0
- package/dist/session-timing.d.ts +15 -0
- package/dist/session-timing.d.ts.map +1 -0
- package/dist/session-timing.js +20 -0
- package/dist/usage-markdown.d.ts +7 -0
- package/dist/usage-markdown.d.ts.map +1 -0
- package/dist/usage-markdown.js +154 -0
- package/package.json +1 -1
package/dist/fork-session.js
CHANGED
|
@@ -1,41 +1,143 @@
|
|
|
1
1
|
import { RequestError } from "@agentclientprotocol/sdk";
|
|
2
|
-
import { forkSession as forkClaudeSession, getSessionMessages, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
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
|
|
31
|
-
?.
|
|
125
|
+
.map((candidateId) => history
|
|
126
|
+
?.slice()
|
|
127
|
+
.reverse()
|
|
128
|
+
.find((message) => dependencies.messageIdForGrouping(message) === candidateId)?.uuid)
|
|
32
129
|
.find(Boolean);
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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"}
|
package/dist/hide-claude-auth.js
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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