claude-code-rust 0.14.2 → 0.14.4
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/README.md +2 -3
- package/agent-sdk/dist/bridge/account_metadata.js +6 -2
- package/agent-sdk/dist/bridge/agents.js +9 -3
- package/agent-sdk/dist/bridge/available_commands.js +13 -3
- package/agent-sdk/dist/bridge/command_lifecycle.js +79 -4
- package/agent-sdk/dist/bridge/command_scheduler.js +7 -2
- package/agent-sdk/dist/bridge/command_session_control.js +5 -1
- package/agent-sdk/dist/bridge/command_session_data.js +225 -10
- package/agent-sdk/dist/bridge/commands.js +48 -11
- package/agent-sdk/dist/bridge/error_classification.js +5 -1
- package/agent-sdk/dist/bridge/events.js +45 -7
- package/agent-sdk/dist/bridge/history.js +33 -10
- package/agent-sdk/dist/bridge/logger.js +19 -3
- package/agent-sdk/dist/bridge/mcp.js +7 -2
- package/agent-sdk/dist/bridge/mcp_auth_adapter.js +4 -2
- package/agent-sdk/dist/bridge/mcp_metadata.js +113 -39
- package/agent-sdk/dist/bridge/mcp_monitor.js +6 -1
- package/agent-sdk/dist/bridge/message_handlers.js +336 -74
- package/agent-sdk/dist/bridge/model_metadata.js +19 -6
- package/agent-sdk/dist/bridge/permissions.js +32 -8
- package/agent-sdk/dist/bridge/session_lifecycle.js +224 -45
- package/agent-sdk/dist/bridge/state_parsing.js +23 -9
- package/agent-sdk/dist/bridge/tasks.js +62 -22
- package/agent-sdk/dist/bridge/tool_calls.js +89 -31
- package/agent-sdk/dist/bridge/tooling.js +430 -82
- package/agent-sdk/dist/bridge/user_interaction.js +45 -13
- package/agent-sdk/dist/bridge.js +200 -104
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A native Rust terminal interface for Claude Code. Drop-in replacement for Anthropic's stock Node.js/React Ink TUI, built for performance and a better user experience.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/claude-code-rust)
|
|
5
|
+
[](https://github.com/srothgan/claude-code-rust/releases/latest)
|
|
7
6
|
[](https://github.com/srothgan/claude-code-rust/actions/workflows/pr.yml)
|
|
8
7
|
[](https://srothgan.github.io/claude-code-rust/)
|
|
9
|
-
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
10
9
|
|
|
11
10
|
<p align="center">
|
|
12
11
|
<img src="assets/banner.png" alt="Claude Code Rust running a Read tool call with syntax-highlighted output" width="900">
|
|
@@ -9,7 +9,9 @@ const KNOWN_API_PROVIDERS = new Set([
|
|
|
9
9
|
"gateway",
|
|
10
10
|
]);
|
|
11
11
|
function trimmedString(value) {
|
|
12
|
-
return typeof value === "string" && value.trim().length > 0
|
|
12
|
+
return typeof value === "string" && value.trim().length > 0
|
|
13
|
+
? value.trim()
|
|
14
|
+
: undefined;
|
|
13
15
|
}
|
|
14
16
|
export function isKnownApiProvider(provider) {
|
|
15
17
|
return provider !== undefined && KNOWN_API_PROVIDERS.has(provider);
|
|
@@ -20,7 +22,9 @@ export function apiProviderIsExternal(provider) {
|
|
|
20
22
|
export function mapSdkAccountInfo(account) {
|
|
21
23
|
const apiProvider = trimmedString(account.apiProvider);
|
|
22
24
|
return {
|
|
23
|
-
...(trimmedString(account.email)
|
|
25
|
+
...(trimmedString(account.email)
|
|
26
|
+
? { email: trimmedString(account.email) }
|
|
27
|
+
: {}),
|
|
24
28
|
...(trimmedString(account.organization)
|
|
25
29
|
? { organization: trimmedString(account.organization) }
|
|
26
30
|
: {}),
|
|
@@ -23,13 +23,16 @@ export function mapAvailableAgents(value) {
|
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
25
|
const description = typeof record.description === "string" ? record.description : "";
|
|
26
|
-
const model = typeof record.model === "string" && record.model.trim().length > 0
|
|
26
|
+
const model = typeof record.model === "string" && record.model.trim().length > 0
|
|
27
|
+
? record.model
|
|
28
|
+
: undefined;
|
|
27
29
|
const existing = byName.get(name);
|
|
28
30
|
if (!existing) {
|
|
29
31
|
byName.set(name, { name, description, model });
|
|
30
32
|
continue;
|
|
31
33
|
}
|
|
32
|
-
if (existing.description.trim().length === 0 &&
|
|
34
|
+
if (existing.description.trim().length === 0 &&
|
|
35
|
+
description.trim().length > 0) {
|
|
33
36
|
existing.description = description;
|
|
34
37
|
}
|
|
35
38
|
if (!existing.model && model) {
|
|
@@ -58,7 +61,10 @@ export function emitAvailableAgentsIfChanged(session, agents) {
|
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
60
63
|
session.lastAvailableAgentsSignature = signature;
|
|
61
|
-
emitSessionUpdate(session.sessionId, {
|
|
64
|
+
emitSessionUpdate(session.sessionId, {
|
|
65
|
+
type: "available_agents_update",
|
|
66
|
+
agents,
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
export function refreshAvailableAgents(session) {
|
|
64
70
|
if (typeof session.query.supportedAgents !== "function") {
|
|
@@ -4,7 +4,11 @@ function isDynamicSource(source) {
|
|
|
4
4
|
return source === "commands_changed" || source === "reload_plugins";
|
|
5
5
|
}
|
|
6
6
|
function commandSignature(commands) {
|
|
7
|
-
return JSON.stringify(commands.map((command) => [
|
|
7
|
+
return JSON.stringify(commands.map((command) => [
|
|
8
|
+
command.name,
|
|
9
|
+
command.description,
|
|
10
|
+
command.input_hint ?? "",
|
|
11
|
+
]));
|
|
8
12
|
}
|
|
9
13
|
function logAvailableCommandsDecision(session, source, commands, outcome, reason, generation) {
|
|
10
14
|
bridgeLogger.info({
|
|
@@ -35,7 +39,10 @@ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
|
|
|
35
39
|
}
|
|
36
40
|
if (!current) {
|
|
37
41
|
if (commands.length === 0) {
|
|
38
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
accept: false,
|
|
44
|
+
reason: "empty bootstrap snapshot ignored before first command list",
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
return { accept: true, reason: "initial command snapshot" };
|
|
41
48
|
}
|
|
@@ -54,7 +61,10 @@ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
|
|
|
54
61
|
if (commands.length === 0) {
|
|
55
62
|
return { accept: false, reason: "empty bootstrap snapshot ignored" };
|
|
56
63
|
}
|
|
57
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
accept: true,
|
|
66
|
+
reason: "bootstrap refresh before dynamic command source",
|
|
67
|
+
};
|
|
58
68
|
}
|
|
59
69
|
export function updateAvailableCommands(session, source, commands) {
|
|
60
70
|
const decision = shouldAcceptAvailableCommandsSnapshot(session, source, commands);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getSessionMessages, listSessions, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import { currentSessionListOptions, emitSessionsList, failConnection, setSessionListingDir, slashError, writeEvent, } from "./events.js";
|
|
2
|
+
import { currentSessionListOptions, emitSessionResumeFailed, emitSessionsList, failConnection, setSessionListingDir, slashError, writeEvent, } from "./events.js";
|
|
3
3
|
import { mapSessionMessagesToUpdates } from "./history.js";
|
|
4
4
|
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
5
|
-
import { closeAllSessions, createSession, sessions, } from "./session_lifecycle.js";
|
|
6
|
-
export async function handleLifecycleCommand(command, requestId, sdkVersionError) {
|
|
5
|
+
import { awaitSessionInitialization, closeAllSessions, closeSessionWithLogging, commitDeferredSession, createSession, detachSessionForClose, sessions, } from "./session_lifecycle.js";
|
|
6
|
+
export async function handleLifecycleCommand(command, requestId, sdkVersionError, deps) {
|
|
7
7
|
switch (command.command) {
|
|
8
8
|
case "initialize":
|
|
9
9
|
await initialize(command, requestId, sdkVersionError);
|
|
@@ -14,6 +14,9 @@ export async function handleLifecycleCommand(command, requestId, sdkVersionError
|
|
|
14
14
|
case "resume_session":
|
|
15
15
|
await resume(command, requestId);
|
|
16
16
|
return;
|
|
17
|
+
case "resume_session_at":
|
|
18
|
+
await resumeAt(command, requestId, deps.buildRewindConversationPlan);
|
|
19
|
+
return;
|
|
17
20
|
case "new_session":
|
|
18
21
|
await replace(command, requestId);
|
|
19
22
|
return;
|
|
@@ -21,6 +24,76 @@ export async function handleLifecycleCommand(command, requestId, sdkVersionError
|
|
|
21
24
|
await shutdown(requestId);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
27
|
+
async function resumeAt(command, requestId, buildPlan) {
|
|
28
|
+
if (!requestId) {
|
|
29
|
+
slashError(command.session_id, "resume before a selected message requires an operation id");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const targetUserMessageId = command.target_user_message_id.trim();
|
|
33
|
+
if (!targetUserMessageId) {
|
|
34
|
+
emitSessionResumeFailed(command.session_id, requestId, "resume target cannot be empty");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let candidate;
|
|
38
|
+
try {
|
|
39
|
+
const sdkSessions = await listSessions(currentSessionListOptions());
|
|
40
|
+
const matched = sdkSessions.find((entry) => entry.sessionId === command.session_id);
|
|
41
|
+
if (!matched) {
|
|
42
|
+
emitSessionResumeFailed(command.session_id, requestId, `unknown session: ${command.session_id}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cwd = matched.cwd ?? process.cwd();
|
|
46
|
+
setSessionListingDir(cwd);
|
|
47
|
+
const historyMessages = await getSessionMessages(command.session_id, {
|
|
48
|
+
dir: cwd,
|
|
49
|
+
includeSystemMessages: true,
|
|
50
|
+
});
|
|
51
|
+
const plan = buildPlan(historyMessages, targetUserMessageId);
|
|
52
|
+
if (!plan) {
|
|
53
|
+
throw new Error(`stale or inconsistent resume target: ${targetUserMessageId}`);
|
|
54
|
+
}
|
|
55
|
+
const staleSessions = Array.from(sessions.values());
|
|
56
|
+
const connectEvent = staleSessions.length > 0 ? "session_replaced" : "connected";
|
|
57
|
+
candidate = plan.resumeSessionAtUuid
|
|
58
|
+
? await createSession({
|
|
59
|
+
cwd,
|
|
60
|
+
resume: command.session_id,
|
|
61
|
+
resumeSessionAt: plan.resumeSessionAtUuid,
|
|
62
|
+
resumeDropsTurn: plan.resumeDropsTurnId,
|
|
63
|
+
forkSession: true,
|
|
64
|
+
launchSettings: command.launch_settings,
|
|
65
|
+
connectEvent,
|
|
66
|
+
requestId,
|
|
67
|
+
deferConnect: true,
|
|
68
|
+
resumeUpdates: plan.resumeUpdates,
|
|
69
|
+
sessionsToCloseAfterConnect: staleSessions,
|
|
70
|
+
})
|
|
71
|
+
: await createSession({
|
|
72
|
+
cwd,
|
|
73
|
+
launchSettings: command.launch_settings,
|
|
74
|
+
connectEvent,
|
|
75
|
+
requestId,
|
|
76
|
+
deferConnect: true,
|
|
77
|
+
sessionsToCloseAfterConnect: staleSessions,
|
|
78
|
+
});
|
|
79
|
+
await awaitSessionInitialization(candidate);
|
|
80
|
+
commitDeferredSession(candidate);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
if (candidate) {
|
|
85
|
+
detachSessionForClose(candidate);
|
|
86
|
+
await closeSessionWithLogging(candidate, {
|
|
87
|
+
reason: "resume_at_candidate_rejected",
|
|
88
|
+
requestId,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const userMessage = message.startsWith("Resume rejected by --resume-drops-turn:")
|
|
92
|
+
? "The session changed while you were selecting a message, so Claude Code refused to discard the newer turn. Reopen Resume and try again."
|
|
93
|
+
: `Failed to fork before selected message: ${message}`;
|
|
94
|
+
emitSessionResumeFailed(command.session_id, requestId, userMessage);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
24
97
|
async function initialize(command, requestId, sdkVersionError) {
|
|
25
98
|
if (sdkVersionError) {
|
|
26
99
|
bridgeLogger.error({
|
|
@@ -129,7 +202,9 @@ async function resume(command, requestId) {
|
|
|
129
202
|
...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
|
|
130
203
|
connectEvent: hadActiveSession ? "session_replaced" : "connected",
|
|
131
204
|
requestId,
|
|
132
|
-
...(hadActiveSession
|
|
205
|
+
...(hadActiveSession
|
|
206
|
+
? { sessionsToCloseAfterConnect: staleSessions }
|
|
207
|
+
: {}),
|
|
133
208
|
});
|
|
134
209
|
}
|
|
135
210
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ const LIFECYCLE_COMMANDS = new Set([
|
|
|
2
2
|
"initialize",
|
|
3
3
|
"create_session",
|
|
4
4
|
"resume_session",
|
|
5
|
+
"resume_session_at",
|
|
5
6
|
"new_session",
|
|
6
7
|
"rewind",
|
|
7
8
|
"shutdown",
|
|
@@ -89,7 +90,9 @@ export class BridgeCommandScheduler {
|
|
|
89
90
|
if (this.lifecycleTail) {
|
|
90
91
|
dependencies.add(this.lifecycleTail);
|
|
91
92
|
}
|
|
92
|
-
const operation = dependencies.size === 0
|
|
93
|
+
const operation = dependencies.size === 0
|
|
94
|
+
? this.start(task)
|
|
95
|
+
: Promise.all(dependencies).then(task);
|
|
93
96
|
const tail = settle(operation);
|
|
94
97
|
this.lifecycleTail = tail;
|
|
95
98
|
void tail.then(() => {
|
|
@@ -108,7 +111,9 @@ export class BridgeCommandScheduler {
|
|
|
108
111
|
if (previousSessionTask) {
|
|
109
112
|
dependencies.push(previousSessionTask);
|
|
110
113
|
}
|
|
111
|
-
const operation = dependencies.length === 0
|
|
114
|
+
const operation = dependencies.length === 0
|
|
115
|
+
? this.start(task)
|
|
116
|
+
: Promise.all(dependencies).then(task);
|
|
112
117
|
const tail = settle(operation);
|
|
113
118
|
this.sessionTails.set(sessionId, tail);
|
|
114
119
|
void tail.then(() => {
|
|
@@ -10,7 +10,11 @@ export async function handleSessionControlCommand(command, requestId, deps) {
|
|
|
10
10
|
handlePrompt(command, requestId, deps);
|
|
11
11
|
return;
|
|
12
12
|
case "cancel_turn":
|
|
13
|
-
await dispatchCancelTurnCommand(command, {
|
|
13
|
+
await dispatchCancelTurnCommand(command, {
|
|
14
|
+
requestId,
|
|
15
|
+
sessionById,
|
|
16
|
+
slashError,
|
|
17
|
+
});
|
|
14
18
|
return;
|
|
15
19
|
case "set_model":
|
|
16
20
|
await setModel(command, requestId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { getSessionMessages, renameSession, } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import { getSessionMessages, listSessions, renameSession, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { asRecordOrNull } from "./shared.js";
|
|
2
3
|
import { mapSdkAccountInfo } from "./account_metadata.js";
|
|
3
|
-
import { emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
|
|
4
|
+
import { currentSessionListOptions, emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
|
|
4
5
|
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
5
6
|
import { refreshCurrentModel, sessionById, } from "./session_lifecycle.js";
|
|
6
7
|
export async function handleSessionDataCommand(command, requestId, deps) {
|
|
@@ -17,6 +18,9 @@ export async function handleSessionDataCommand(command, requestId, deps) {
|
|
|
17
18
|
case "get_context_usage":
|
|
18
19
|
await getContextUsage(command, requestId);
|
|
19
20
|
return;
|
|
21
|
+
case "get_usage":
|
|
22
|
+
await getUsage(command, requestId);
|
|
23
|
+
return;
|
|
20
24
|
case "get_rewind_targets":
|
|
21
25
|
await getRewindTargets(command, requestId, deps);
|
|
22
26
|
return;
|
|
@@ -124,14 +128,18 @@ async function getContextUsage(command, requestId) {
|
|
|
124
128
|
normalized_percentage: normalizedPercentage,
|
|
125
129
|
total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
|
|
126
130
|
max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
|
|
127
|
-
raw_max_tokens: typeof usage.rawMaxTokens === "number"
|
|
131
|
+
raw_max_tokens: typeof usage.rawMaxTokens === "number"
|
|
132
|
+
? usage.rawMaxTokens
|
|
133
|
+
: undefined,
|
|
128
134
|
model: typeof usage.model === "string" ? usage.model : undefined,
|
|
129
135
|
},
|
|
130
136
|
});
|
|
131
137
|
writeEvent({
|
|
132
138
|
event: "context_usage",
|
|
133
139
|
session_id: session.sessionId,
|
|
134
|
-
...(normalizedPercentage !== undefined
|
|
140
|
+
...(normalizedPercentage !== undefined
|
|
141
|
+
? { percentage: normalizedPercentage }
|
|
142
|
+
: {}),
|
|
135
143
|
}, requestId);
|
|
136
144
|
}
|
|
137
145
|
catch (error) {
|
|
@@ -151,14 +159,216 @@ async function getContextUsage(command, requestId) {
|
|
|
151
159
|
}, requestId);
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
|
-
|
|
162
|
+
function finiteNumber(record, key) {
|
|
163
|
+
const value = record?.[key];
|
|
164
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
165
|
+
? value
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
function structuredUsageWindow(value) {
|
|
169
|
+
const record = asRecordOrNull(value);
|
|
170
|
+
const utilization = finiteNumber(record, "utilization");
|
|
171
|
+
if (utilization === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const resetsAt = typeof record?.resets_at === "string" ? record.resets_at.trim() : "";
|
|
175
|
+
return {
|
|
176
|
+
utilization: Math.max(0, Math.min(100, utilization)),
|
|
177
|
+
...(resetsAt ? { resets_at: resetsAt } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function structuredModelUsageWindows(value) {
|
|
181
|
+
if (!Array.isArray(value)) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return value.flatMap((entry) => {
|
|
185
|
+
const record = asRecordOrNull(entry);
|
|
186
|
+
const displayName = typeof record?.display_name === "string"
|
|
187
|
+
? record.display_name.trim()
|
|
188
|
+
: "";
|
|
189
|
+
const window = structuredUsageWindow(record);
|
|
190
|
+
return displayName && window
|
|
191
|
+
? [{ display_name: displayName, ...window }]
|
|
192
|
+
: [];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function structuredExtraUsage(value) {
|
|
196
|
+
const record = asRecordOrNull(value);
|
|
197
|
+
if (!record || record.is_enabled === false) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const monthlyLimit = finiteNumber(record, "monthly_limit");
|
|
201
|
+
const usedCredits = finiteNumber(record, "used_credits");
|
|
202
|
+
const utilization = finiteNumber(record, "utilization");
|
|
203
|
+
const normalized = {
|
|
204
|
+
...(monthlyLimit !== undefined ? { monthly_limit: monthlyLimit } : {}),
|
|
205
|
+
...(usedCredits !== undefined ? { used_credits: usedCredits } : {}),
|
|
206
|
+
...(utilization !== undefined
|
|
207
|
+
? { utilization: Math.max(0, Math.min(100, utilization)) }
|
|
208
|
+
: {}),
|
|
209
|
+
...(typeof record.currency === "string" && record.currency.trim()
|
|
210
|
+
? { currency: record.currency.trim() }
|
|
211
|
+
: {}),
|
|
212
|
+
};
|
|
213
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
214
|
+
}
|
|
215
|
+
function structuredSessionUsage(value) {
|
|
216
|
+
const record = asRecordOrNull(value);
|
|
217
|
+
if (!record) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const modelUsage = asRecordOrNull(record.model_usage);
|
|
221
|
+
const normalized = {
|
|
222
|
+
...(finiteNumber(record, "total_cost_usd") !== undefined
|
|
223
|
+
? { total_cost_usd: finiteNumber(record, "total_cost_usd") }
|
|
224
|
+
: {}),
|
|
225
|
+
...(finiteNumber(record, "total_api_duration_ms") !== undefined
|
|
226
|
+
? { total_api_duration_ms: finiteNumber(record, "total_api_duration_ms") }
|
|
227
|
+
: {}),
|
|
228
|
+
...(finiteNumber(record, "total_duration_ms") !== undefined
|
|
229
|
+
? { total_duration_ms: finiteNumber(record, "total_duration_ms") }
|
|
230
|
+
: {}),
|
|
231
|
+
...(finiteNumber(record, "total_lines_added") !== undefined
|
|
232
|
+
? { total_lines_added: finiteNumber(record, "total_lines_added") }
|
|
233
|
+
: {}),
|
|
234
|
+
...(finiteNumber(record, "total_lines_removed") !== undefined
|
|
235
|
+
? { total_lines_removed: finiteNumber(record, "total_lines_removed") }
|
|
236
|
+
: {}),
|
|
237
|
+
...(modelUsage ? { model_count: Object.keys(modelUsage).length } : {}),
|
|
238
|
+
};
|
|
239
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
240
|
+
}
|
|
241
|
+
function structuredActivityWindow(value) {
|
|
242
|
+
const record = asRecordOrNull(value);
|
|
243
|
+
const requestCount = finiteNumber(record, "request_count");
|
|
244
|
+
const sessionCount = finiteNumber(record, "session_count");
|
|
245
|
+
if (requestCount === undefined ||
|
|
246
|
+
sessionCount === undefined ||
|
|
247
|
+
requestCount < 0 ||
|
|
248
|
+
sessionCount < 0) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
request_count: Math.trunc(requestCount),
|
|
253
|
+
session_count: Math.trunc(sessionCount),
|
|
254
|
+
behaviors: structuredBehaviorAttributions(record?.behaviors),
|
|
255
|
+
agents: structuredNamedAttributions(record?.agents),
|
|
256
|
+
skills: structuredNamedAttributions(record?.skills),
|
|
257
|
+
plugins: structuredNamedAttributions(record?.plugins),
|
|
258
|
+
mcp_servers: structuredNamedAttributions(record?.mcp_servers),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function structuredBehaviorAttributions(value) {
|
|
262
|
+
if (!Array.isArray(value)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
return value.flatMap((entry) => {
|
|
266
|
+
const record = asRecordOrNull(entry);
|
|
267
|
+
const key = typeof record?.key === "string" ? record.key.trim() : "";
|
|
268
|
+
const pct = finiteNumber(record, "pct");
|
|
269
|
+
const count = finiteNumber(record, "count");
|
|
270
|
+
if (!key || pct === undefined || count === undefined || count < 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
return [
|
|
274
|
+
{ key, pct: Math.max(0, Math.min(100, pct)), count: Math.trunc(count) },
|
|
275
|
+
];
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function structuredNamedAttributions(value) {
|
|
279
|
+
if (!Array.isArray(value)) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
return value.flatMap((entry) => {
|
|
283
|
+
const record = asRecordOrNull(entry);
|
|
284
|
+
const name = typeof record?.name === "string" ? record.name.trim() : "";
|
|
285
|
+
const pct = finiteNumber(record, "pct");
|
|
286
|
+
return name && pct !== undefined
|
|
287
|
+
? [{ name, pct: Math.max(0, Math.min(100, pct)) }]
|
|
288
|
+
: [];
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export function normalizeStructuredUsage(value) {
|
|
292
|
+
const root = asRecordOrNull(value);
|
|
293
|
+
const limits = asRecordOrNull(root?.rate_limits);
|
|
294
|
+
const behaviors = asRecordOrNull(root?.behaviors);
|
|
295
|
+
const fiveHour = structuredUsageWindow(limits?.five_hour);
|
|
296
|
+
const sevenDay = structuredUsageWindow(limits?.seven_day);
|
|
297
|
+
const sevenDayOauthApps = structuredUsageWindow(limits?.seven_day_oauth_apps);
|
|
298
|
+
const sevenDayOpus = structuredUsageWindow(limits?.seven_day_opus);
|
|
299
|
+
const sevenDaySonnet = structuredUsageWindow(limits?.seven_day_sonnet);
|
|
300
|
+
const modelScoped = structuredModelUsageWindows(limits?.model_scoped);
|
|
301
|
+
const extraUsage = structuredExtraUsage(limits?.extra_usage);
|
|
302
|
+
const session = structuredSessionUsage(root?.session);
|
|
303
|
+
const activityDay = structuredActivityWindow(behaviors?.day);
|
|
304
|
+
const activityWeek = structuredActivityWindow(behaviors?.week);
|
|
305
|
+
return {
|
|
306
|
+
...(typeof root?.subscription_type === "string" &&
|
|
307
|
+
root.subscription_type.trim()
|
|
308
|
+
? { subscription_type: root.subscription_type.trim() }
|
|
309
|
+
: {}),
|
|
310
|
+
...(typeof root?.rate_limits_available === "boolean"
|
|
311
|
+
? { rate_limits_available: root.rate_limits_available }
|
|
312
|
+
: {}),
|
|
313
|
+
...(fiveHour ? { five_hour: fiveHour } : {}),
|
|
314
|
+
...(sevenDay ? { seven_day: sevenDay } : {}),
|
|
315
|
+
...(sevenDayOauthApps ? { seven_day_oauth_apps: sevenDayOauthApps } : {}),
|
|
316
|
+
...(sevenDayOpus ? { seven_day_opus: sevenDayOpus } : {}),
|
|
317
|
+
...(sevenDaySonnet ? { seven_day_sonnet: sevenDaySonnet } : {}),
|
|
318
|
+
...(modelScoped.length > 0 ? { model_scoped: modelScoped } : {}),
|
|
319
|
+
...(extraUsage ? { extra_usage: extraUsage } : {}),
|
|
320
|
+
...(session ? { session } : {}),
|
|
321
|
+
...(activityDay ? { activity_day: activityDay } : {}),
|
|
322
|
+
...(activityWeek ? { activity_week: activityWeek } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function getUsage(command, requestId) {
|
|
155
326
|
const session = requireSession(command.session_id, requestId);
|
|
156
327
|
if (!session) {
|
|
157
328
|
return;
|
|
158
329
|
}
|
|
159
330
|
try {
|
|
331
|
+
const usageMethod = session.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
|
|
332
|
+
if (typeof usageMethod !== "function") {
|
|
333
|
+
throw new Error("structured SDK usage is unavailable in this runtime");
|
|
334
|
+
}
|
|
335
|
+
const snapshot = normalizeStructuredUsage(await usageMethod.call(session.query));
|
|
336
|
+
if (Object.keys(snapshot).length === 0) {
|
|
337
|
+
throw new Error("structured SDK usage returned an incompatible empty payload");
|
|
338
|
+
}
|
|
339
|
+
writeEvent({ event: "usage_snapshot", session_id: session.sessionId, snapshot }, requestId);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
bridgeLogger.warn({
|
|
344
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
345
|
+
eventName: "structured_usage_failed",
|
|
346
|
+
message: "experimental structured SDK usage failed",
|
|
347
|
+
outcome: "fallback_required",
|
|
348
|
+
...(requestId ? { requestId } : {}),
|
|
349
|
+
sessionId: session.sessionId,
|
|
350
|
+
fields: { error_message: message },
|
|
351
|
+
});
|
|
352
|
+
writeEvent({
|
|
353
|
+
event: "usage_snapshot",
|
|
354
|
+
session_id: session.sessionId,
|
|
355
|
+
error: message,
|
|
356
|
+
}, requestId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function getRewindTargets(command, requestId, deps) {
|
|
360
|
+
const activeSession = sessionById(command.session_id);
|
|
361
|
+
try {
|
|
362
|
+
let cwd = activeSession?.cwd;
|
|
363
|
+
if (!cwd) {
|
|
364
|
+
const listedSession = (await listSessions(currentSessionListOptions())).find((entry) => entry.sessionId === command.session_id);
|
|
365
|
+
if (!listedSession) {
|
|
366
|
+
throw new Error(`unknown session: ${command.session_id}`);
|
|
367
|
+
}
|
|
368
|
+
cwd = listedSession.cwd?.trim() || currentSessionListOptions().dir;
|
|
369
|
+
}
|
|
160
370
|
const historyMessages = await getSessionMessages(command.session_id, {
|
|
161
|
-
dir:
|
|
371
|
+
...(cwd ? { dir: cwd } : {}),
|
|
162
372
|
includeSystemMessages: true,
|
|
163
373
|
});
|
|
164
374
|
const targets = deps.rewindTargetsFromSessionMessages(historyMessages);
|
|
@@ -168,7 +378,7 @@ async function getRewindTargets(command, requestId, deps) {
|
|
|
168
378
|
message: "rewind targets loaded from session history",
|
|
169
379
|
outcome: "success",
|
|
170
380
|
...(requestId ? { requestId } : {}),
|
|
171
|
-
sessionId:
|
|
381
|
+
sessionId: command.session_id,
|
|
172
382
|
fields: {
|
|
173
383
|
history_message_count: historyMessages.length,
|
|
174
384
|
target_count: targets.length,
|
|
@@ -176,7 +386,7 @@ async function getRewindTargets(command, requestId, deps) {
|
|
|
176
386
|
});
|
|
177
387
|
writeEvent({
|
|
178
388
|
event: "rewind_targets",
|
|
179
|
-
session_id:
|
|
389
|
+
session_id: command.session_id,
|
|
180
390
|
targets,
|
|
181
391
|
}, requestId);
|
|
182
392
|
}
|
|
@@ -188,10 +398,15 @@ async function getRewindTargets(command, requestId, deps) {
|
|
|
188
398
|
message: "failed to load rewind targets",
|
|
189
399
|
outcome: "failure",
|
|
190
400
|
...(requestId ? { requestId } : {}),
|
|
191
|
-
sessionId:
|
|
401
|
+
sessionId: command.session_id,
|
|
192
402
|
fields: { error_message: message },
|
|
193
403
|
});
|
|
194
|
-
|
|
404
|
+
writeEvent({
|
|
405
|
+
event: "rewind_targets",
|
|
406
|
+
session_id: command.session_id,
|
|
407
|
+
targets: [],
|
|
408
|
+
error: `failed to load rewind targets: ${message}`,
|
|
409
|
+
}, requestId);
|
|
195
410
|
}
|
|
196
411
|
}
|
|
197
412
|
function requireSession(sessionId, requestId) {
|
|
@@ -10,11 +10,27 @@ const MODE_NAMES = {
|
|
|
10
10
|
};
|
|
11
11
|
const MODE_OPTIONS = [
|
|
12
12
|
{ id: "default", name: "Default", description: "Standard permission flow" },
|
|
13
|
-
{
|
|
14
|
-
|
|
13
|
+
{
|
|
14
|
+
id: "auto",
|
|
15
|
+
name: "Auto",
|
|
16
|
+
description: "Model-classified permission approvals",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "acceptEdits",
|
|
20
|
+
name: "Accept Edits",
|
|
21
|
+
description: "Auto-approve edit operations",
|
|
22
|
+
},
|
|
15
23
|
{ id: "plan", name: "Plan", description: "No tool execution" },
|
|
16
|
-
{
|
|
17
|
-
|
|
24
|
+
{
|
|
25
|
+
id: "dontAsk",
|
|
26
|
+
name: "Don't Ask",
|
|
27
|
+
description: "Reject non-approved tools",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "bypassPermissions",
|
|
31
|
+
name: "Bypass Permissions",
|
|
32
|
+
description: "Auto-approve all tools",
|
|
33
|
+
},
|
|
18
34
|
];
|
|
19
35
|
const BASE_SUPPORTED_MODE_IDS = [
|
|
20
36
|
"default",
|
|
@@ -27,10 +43,10 @@ function currentModelSupportsAutoMode(session) {
|
|
|
27
43
|
return currentModel.supports_auto_mode === true;
|
|
28
44
|
}
|
|
29
45
|
function modeInfoForId(mode) {
|
|
30
|
-
return MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
|
|
46
|
+
return (MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
|
|
31
47
|
id: mode,
|
|
32
48
|
name: MODE_NAMES[mode],
|
|
33
|
-
};
|
|
49
|
+
});
|
|
34
50
|
}
|
|
35
51
|
function uniqueModeIds(modeIds) {
|
|
36
52
|
const unique = new Set(modeIds);
|
|
@@ -53,13 +69,17 @@ function computedSupportedModeIds(session) {
|
|
|
53
69
|
}
|
|
54
70
|
export function refreshSupportedModesForSession(session) {
|
|
55
71
|
const computed = computedSupportedModeIds(session);
|
|
56
|
-
session.supportedModeIds = computed.filter((mode) => mode === session.mode ||
|
|
72
|
+
session.supportedModeIds = computed.filter((mode) => mode === session.mode ||
|
|
73
|
+
!session.runtimeUnavailableModeIds.includes(mode));
|
|
57
74
|
}
|
|
58
75
|
export function markModeUnavailableForSession(session, mode) {
|
|
59
76
|
if (session.runtimeUnavailableModeIds.includes(mode)) {
|
|
60
77
|
return false;
|
|
61
78
|
}
|
|
62
|
-
session.runtimeUnavailableModeIds = [
|
|
79
|
+
session.runtimeUnavailableModeIds = [
|
|
80
|
+
...session.runtimeUnavailableModeIds,
|
|
81
|
+
mode,
|
|
82
|
+
];
|
|
63
83
|
refreshSupportedModesForSession(session);
|
|
64
84
|
return true;
|
|
65
85
|
}
|
|
@@ -225,6 +245,13 @@ export function parseCommandEnvelope(line) {
|
|
|
225
245
|
launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session"),
|
|
226
246
|
metadata: optionalMetadata(raw, "metadata"),
|
|
227
247
|
};
|
|
248
|
+
case "resume_session_at":
|
|
249
|
+
return {
|
|
250
|
+
command: "resume_session_at",
|
|
251
|
+
session_id: expectString(raw, "session_id", "resume_session_at"),
|
|
252
|
+
target_user_message_id: expectString(raw, "target_user_message_id", "resume_session_at"),
|
|
253
|
+
launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session_at"),
|
|
254
|
+
};
|
|
228
255
|
case "new_session":
|
|
229
256
|
return {
|
|
230
257
|
command: "new_session",
|
|
@@ -294,6 +321,11 @@ export function parseCommandEnvelope(line) {
|
|
|
294
321
|
command: "get_context_usage",
|
|
295
322
|
session_id: expectString(raw, "session_id", "get_context_usage"),
|
|
296
323
|
};
|
|
324
|
+
case "get_usage":
|
|
325
|
+
return {
|
|
326
|
+
command: "get_usage",
|
|
327
|
+
session_id: expectString(raw, "session_id", "get_usage"),
|
|
328
|
+
};
|
|
297
329
|
case "get_rewind_targets":
|
|
298
330
|
return {
|
|
299
331
|
command: "get_rewind_targets",
|
|
@@ -367,9 +399,12 @@ export function parseCommandEnvelope(line) {
|
|
|
367
399
|
? {
|
|
368
400
|
outcome: "answered",
|
|
369
401
|
selected_option_ids: expectStringArray(outcome, "selected_option_ids", "question_response.outcome"),
|
|
370
|
-
...(outcome.annotation === undefined ||
|
|
402
|
+
...(outcome.annotation === undefined ||
|
|
403
|
+
outcome.annotation === null
|
|
371
404
|
? {}
|
|
372
|
-
: {
|
|
405
|
+
: {
|
|
406
|
+
annotation: parseQuestionAnnotation(outcome.annotation),
|
|
407
|
+
}),
|
|
373
408
|
}
|
|
374
409
|
: { outcome: "cancelled" };
|
|
375
410
|
return {
|
|
@@ -405,7 +440,9 @@ export function parseCommandEnvelope(line) {
|
|
|
405
440
|
elicitation_request_id: expectString(raw, "elicitation_request_id", "elicitation_response"),
|
|
406
441
|
action: expectElicitationAction(raw, "action", "elicitation_response"),
|
|
407
442
|
...(optionalJsonObject(raw, "content", "elicitation_response")
|
|
408
|
-
? {
|
|
443
|
+
? {
|
|
444
|
+
content: optionalJsonObject(raw, "content", "elicitation_response"),
|
|
445
|
+
}
|
|
409
446
|
: {}),
|
|
410
447
|
};
|
|
411
448
|
case "mcp_authenticate":
|