claude-code-rust 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/agent-sdk/dist/bridge/commands.js +3 -3
- package/agent-sdk/dist/bridge/history.js +50 -242
- package/agent-sdk/dist/bridge/tooling.js +27 -5
- package/agent-sdk/dist/bridge.js +262 -29
- package/agent-sdk/dist/bridge.test.js +201 -105
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
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)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-code-rust)
|
|
5
7
|
[](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
|
|
6
8
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
10
|
|
|
9
11
|
## About
|
|
10
12
|
|
|
@@ -56,8 +58,6 @@ Three-layer design:
|
|
|
56
58
|
|
|
57
59
|
## Known Limitations
|
|
58
60
|
|
|
59
|
-
- Token usage and cost tracking can be partial when upstream runtime events do not include full usage data.
|
|
60
|
-
- Session resume via `--resume` depends on runtime support and account/session availability.
|
|
61
61
|
- `/login` and `/logout` are intentionally not offered in command discovery for this release.
|
|
62
62
|
|
|
63
63
|
## Status
|
|
@@ -81,10 +81,10 @@ export function parseCommandEnvelope(line) {
|
|
|
81
81
|
resume: optionalString(raw, "resume", "create_session"),
|
|
82
82
|
metadata: optionalMetadata(raw, "metadata"),
|
|
83
83
|
};
|
|
84
|
-
case "
|
|
84
|
+
case "resume_session":
|
|
85
85
|
return {
|
|
86
|
-
command: "
|
|
87
|
-
session_id: expectString(raw, "session_id", "
|
|
86
|
+
command: "resume_session",
|
|
87
|
+
session_id: expectString(raw, "session_id", "resume_session"),
|
|
88
88
|
metadata: optionalMetadata(raw, "metadata"),
|
|
89
89
|
};
|
|
90
90
|
case "new_session":
|
|
@@ -1,224 +1,22 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
|
|
5
1
|
import { asRecordOrNull } from "./shared.js";
|
|
2
|
+
import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
|
|
6
3
|
import { buildUsageUpdateFromResult } from "./usage.js";
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
10
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
11
|
-
return text;
|
|
12
|
-
}
|
|
13
|
-
function truncateTextByChars(text, maxChars) {
|
|
14
|
-
const chars = Array.from(text);
|
|
15
|
-
if (chars.length <= maxChars) {
|
|
16
|
-
return text;
|
|
17
|
-
}
|
|
18
|
-
return chars.slice(0, maxChars).join("");
|
|
19
|
-
}
|
|
20
|
-
function firstUserMessageTitleFromRecord(record) {
|
|
21
|
-
if (record.type !== "user") {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
const message = asRecordOrNull(record.message);
|
|
25
|
-
if (!message || message.role !== "user" || !Array.isArray(message.content)) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
const parts = [];
|
|
29
|
-
for (const item of message.content) {
|
|
30
|
-
const block = asRecordOrNull(item);
|
|
31
|
-
if (!block || block.type !== "text" || typeof block.text !== "string") {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const cleaned = normalizeUserPromptText(block.text);
|
|
35
|
-
if (!cleaned) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
parts.push(cleaned);
|
|
39
|
-
const combined = parts.join(" ");
|
|
40
|
-
if (Array.from(combined).length >= 180) {
|
|
41
|
-
return truncateTextByChars(combined, 180);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (parts.length === 0) {
|
|
4
|
+
function nonEmptyTrimmed(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
45
6
|
return undefined;
|
|
46
7
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function extractSessionPreviewFromJsonl(filePath) {
|
|
50
|
-
let text;
|
|
51
|
-
try {
|
|
52
|
-
text = fs.readFileSync(filePath, "utf8");
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
let cwd;
|
|
58
|
-
let title;
|
|
59
|
-
const lines = text.split(/\r?\n/);
|
|
60
|
-
for (const rawLine of lines) {
|
|
61
|
-
const line = rawLine.trim();
|
|
62
|
-
if (line.length === 0) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
let parsed;
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(line);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const record = asRecordOrNull(parsed);
|
|
73
|
-
if (!record) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (!cwd && typeof record.cwd === "string" && record.cwd.trim().length > 0) {
|
|
77
|
-
cwd = record.cwd;
|
|
78
|
-
}
|
|
79
|
-
if (!title) {
|
|
80
|
-
title = firstUserMessageTitleFromRecord(record);
|
|
81
|
-
}
|
|
82
|
-
if (cwd && title) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
...(cwd ? { cwd } : {}),
|
|
88
|
-
...(title ? { title } : {}),
|
|
89
|
-
};
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
90
10
|
}
|
|
91
|
-
|
|
92
|
-
const root = path.join(os.homedir(), ".claude", "projects");
|
|
93
|
-
if (!fs.existsSync(root)) {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
11
|
+
function messageCandidates(raw) {
|
|
96
12
|
const candidates = [];
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
for (const dirent of projectDirs) {
|
|
105
|
-
const projectDir = path.join(root, dirent.name);
|
|
106
|
-
let sessionFiles;
|
|
107
|
-
try {
|
|
108
|
-
sessionFiles = fs
|
|
109
|
-
.readdirSync(projectDir, { withFileTypes: true })
|
|
110
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"));
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
sessionFiles = [];
|
|
114
|
-
}
|
|
115
|
-
for (const sessionFile of sessionFiles) {
|
|
116
|
-
const sessionId = sessionFile.name.slice(0, -".jsonl".length);
|
|
117
|
-
if (!sessionId) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
let mtimeMs = 0;
|
|
121
|
-
try {
|
|
122
|
-
mtimeMs = fs.statSync(path.join(projectDir, sessionFile.name)).mtimeMs;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (!Number.isFinite(mtimeMs) || mtimeMs <= 0) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
candidates.push({
|
|
131
|
-
session_id: sessionId,
|
|
132
|
-
cwd: "",
|
|
133
|
-
file_path: path.join(projectDir, sessionFile.name),
|
|
134
|
-
updated_at: new Date(mtimeMs).toISOString(),
|
|
135
|
-
sort_ms: mtimeMs,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
candidates.sort((a, b) => b.sort_ms - a.sort_ms);
|
|
140
|
-
const deduped = [];
|
|
141
|
-
const seen = new Set();
|
|
142
|
-
for (const candidate of candidates) {
|
|
143
|
-
if (seen.has(candidate.session_id)) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
seen.add(candidate.session_id);
|
|
147
|
-
const preview = extractSessionPreviewFromJsonl(candidate.file_path);
|
|
148
|
-
const cwd = preview.cwd?.trim();
|
|
149
|
-
if (!cwd) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
deduped.push({
|
|
153
|
-
session_id: candidate.session_id,
|
|
154
|
-
cwd,
|
|
155
|
-
file_path: candidate.file_path,
|
|
156
|
-
...(preview.title ? { title: preview.title } : {}),
|
|
157
|
-
...(candidate.updated_at ? { updated_at: candidate.updated_at } : {}),
|
|
158
|
-
sort_ms: candidate.sort_ms,
|
|
159
|
-
});
|
|
160
|
-
if (deduped.length >= limit) {
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return deduped;
|
|
165
|
-
}
|
|
166
|
-
export function resolvePersistedSessionEntry(sessionId) {
|
|
167
|
-
if (sessionId.trim().length === 0 ||
|
|
168
|
-
sessionId.includes("/") ||
|
|
169
|
-
sessionId.includes("\\") ||
|
|
170
|
-
sessionId.includes("..")) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const root = path.join(os.homedir(), ".claude", "projects");
|
|
174
|
-
if (!fs.existsSync(root)) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
let projectDirs;
|
|
178
|
-
try {
|
|
179
|
-
projectDirs = fs.readdirSync(root, { withFileTypes: true }).filter((dirent) => dirent.isDirectory());
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
let best = null;
|
|
185
|
-
for (const dirent of projectDirs) {
|
|
186
|
-
const filePath = path.join(root, dirent.name, `${sessionId}.jsonl`);
|
|
187
|
-
if (!fs.existsSync(filePath)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const preview = extractSessionPreviewFromJsonl(filePath);
|
|
191
|
-
const cwd = preview.cwd?.trim();
|
|
192
|
-
if (!cwd) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
let mtimeMs = 0;
|
|
196
|
-
try {
|
|
197
|
-
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
mtimeMs = 0;
|
|
201
|
-
}
|
|
202
|
-
if (!best || mtimeMs >= best.sort_ms) {
|
|
203
|
-
best = {
|
|
204
|
-
session_id: sessionId,
|
|
205
|
-
cwd,
|
|
206
|
-
file_path: filePath,
|
|
207
|
-
sort_ms: mtimeMs,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return best;
|
|
212
|
-
}
|
|
213
|
-
function persistedMessageCandidates(record) {
|
|
214
|
-
const candidates = [];
|
|
215
|
-
const topLevel = asRecordOrNull(record.message);
|
|
13
|
+
const topLevel = asRecordOrNull(raw);
|
|
216
14
|
if (topLevel) {
|
|
217
15
|
candidates.push(topLevel);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
16
|
+
const nested = asRecordOrNull(topLevel.message);
|
|
17
|
+
if (nested) {
|
|
18
|
+
candidates.push(nested);
|
|
19
|
+
}
|
|
222
20
|
}
|
|
223
21
|
return candidates;
|
|
224
22
|
}
|
|
@@ -278,39 +76,49 @@ function pushResumeUsageUpdate(updates, message, emittedUsageMessageIds) {
|
|
|
278
76
|
emittedUsageMessageIds.add(messageId);
|
|
279
77
|
}
|
|
280
78
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
79
|
+
function summaryFromSession(info) {
|
|
80
|
+
return (nonEmptyTrimmed(info.summary) ??
|
|
81
|
+
nonEmptyTrimmed(info.customTitle) ??
|
|
82
|
+
nonEmptyTrimmed(info.firstPrompt) ??
|
|
83
|
+
info.sessionId);
|
|
84
|
+
}
|
|
85
|
+
export function mapSdkSessionInfo(info) {
|
|
86
|
+
return {
|
|
87
|
+
session_id: info.sessionId,
|
|
88
|
+
summary: summaryFromSession(info),
|
|
89
|
+
last_modified_ms: info.lastModified,
|
|
90
|
+
file_size_bytes: info.fileSize,
|
|
91
|
+
...(nonEmptyTrimmed(info.cwd) ? { cwd: info.cwd?.trim() } : {}),
|
|
92
|
+
...(nonEmptyTrimmed(info.gitBranch) ? { git_branch: info.gitBranch?.trim() } : {}),
|
|
93
|
+
...(nonEmptyTrimmed(info.customTitle) ? { custom_title: info.customTitle?.trim() } : {}),
|
|
94
|
+
...(nonEmptyTrimmed(info.firstPrompt) ? { first_prompt: info.firstPrompt?.trim() } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function mapSdkSessions(infos, limit = 50) {
|
|
98
|
+
const sorted = [...infos].sort((a, b) => b.lastModified - a.lastModified);
|
|
99
|
+
const entries = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const info of sorted) {
|
|
102
|
+
if (!info.sessionId || seen.has(info.sessionId)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
seen.add(info.sessionId);
|
|
106
|
+
entries.push(mapSdkSessionInfo(info));
|
|
107
|
+
if (entries.length >= limit) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
288
110
|
}
|
|
111
|
+
return entries;
|
|
112
|
+
}
|
|
113
|
+
export function mapSessionMessagesToUpdates(messages) {
|
|
289
114
|
const updates = [];
|
|
290
115
|
const toolCalls = new Map();
|
|
291
116
|
const emittedUsageMessageIds = new Set();
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
let parsed;
|
|
299
|
-
try {
|
|
300
|
-
parsed = JSON.parse(line);
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
const record = asRecordOrNull(parsed);
|
|
306
|
-
if (!record) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
for (const message of persistedMessageCandidates(record)) {
|
|
310
|
-
const role = message.role;
|
|
311
|
-
if (role !== "user" && role !== "assistant") {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
117
|
+
for (const entry of messages) {
|
|
118
|
+
const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
|
|
119
|
+
for (const message of messageCandidates(entry.message)) {
|
|
120
|
+
const roleCandidate = message.role;
|
|
121
|
+
const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
|
|
314
122
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
315
123
|
for (const item of content) {
|
|
316
124
|
const block = asRecordOrNull(item);
|
|
@@ -34,6 +34,7 @@ export function normalizeToolKind(name) {
|
|
|
34
34
|
case "TodoWrite":
|
|
35
35
|
return "other";
|
|
36
36
|
case "Task":
|
|
37
|
+
case "Agent":
|
|
37
38
|
return "think";
|
|
38
39
|
case "ExitPlanMode":
|
|
39
40
|
return "switch_mode";
|
|
@@ -169,16 +170,37 @@ function persistedOutputFirstLine(text) {
|
|
|
169
170
|
}
|
|
170
171
|
return null;
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Replace verbose SDK-internal tool rejection messages with short user-facing text.
|
|
175
|
+
* The SDK sends these as tool result content meant for Claude, not for the user.
|
|
176
|
+
*/
|
|
177
|
+
const USER_REJECTED_TOOL_USE_EXACT = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
178
|
+
const USER_REJECTED_TOOL_USE_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:";
|
|
179
|
+
const PERMISSION_DENIED_TOOL_USE_EXACT = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.";
|
|
180
|
+
const PERMISSION_DENIED_TOOL_USE_PREFIX = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). The user said:";
|
|
181
|
+
function sanitizeSdkRejectionText(text) {
|
|
182
|
+
const normalized = text.trim();
|
|
183
|
+
if (normalized === USER_REJECTED_TOOL_USE_EXACT ||
|
|
184
|
+
normalized.startsWith(USER_REJECTED_TOOL_USE_PREFIX)) {
|
|
185
|
+
return "Cancelled by user.";
|
|
186
|
+
}
|
|
187
|
+
if (normalized === PERMISSION_DENIED_TOOL_USE_EXACT ||
|
|
188
|
+
normalized.startsWith(PERMISSION_DENIED_TOOL_USE_PREFIX)) {
|
|
189
|
+
return "Permission denied.";
|
|
190
|
+
}
|
|
191
|
+
return text;
|
|
192
|
+
}
|
|
193
|
+
export function normalizeToolResultText(value, isError = false) {
|
|
173
194
|
const text = extractText(value);
|
|
174
195
|
if (!text) {
|
|
175
196
|
return "";
|
|
176
197
|
}
|
|
177
198
|
const persistedLine = persistedOutputFirstLine(text);
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
const normalized = persistedLine || text;
|
|
200
|
+
if (!isError) {
|
|
201
|
+
return normalized;
|
|
180
202
|
}
|
|
181
|
-
return
|
|
203
|
+
return sanitizeSdkRejectionText(normalized);
|
|
182
204
|
}
|
|
183
205
|
function resolveToolName(toolCall) {
|
|
184
206
|
const meta = asRecordOrNull(toolCall?.meta);
|
|
@@ -242,7 +264,7 @@ function writeDiffFromResult(rawContent) {
|
|
|
242
264
|
return [];
|
|
243
265
|
}
|
|
244
266
|
export function buildToolResultFields(isError, rawContent, base) {
|
|
245
|
-
const rawOutput = normalizeToolResultText(rawContent);
|
|
267
|
+
const rawOutput = normalizeToolResultText(rawContent, isError);
|
|
246
268
|
const toolName = resolveToolName(base);
|
|
247
269
|
const fields = {
|
|
248
270
|
status: isError ? "failed" : "completed",
|
package/agent-sdk/dist/bridge.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
import { getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import { parseCommandEnvelope, toPermissionMode, buildModeState } from "./bridge/commands.js";
|
|
9
9
|
import { asRecordOrNull } from "./bridge/shared.js";
|
|
10
10
|
import { looksLikeAuthRequired } from "./bridge/auth.js";
|
|
@@ -12,10 +12,11 @@ import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, normalizeTool
|
|
|
12
12
|
import { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./bridge/cache_policy.js";
|
|
13
13
|
import { buildUsageUpdateFromResult, buildUsageUpdateFromResultForSession } from "./bridge/usage.js";
|
|
14
14
|
import { formatPermissionUpdates, permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./bridge/permissions.js";
|
|
15
|
-
import {
|
|
16
|
-
export { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall,
|
|
15
|
+
import { mapSdkSessions, mapSessionMessagesToUpdates, } from "./bridge/history.js";
|
|
16
|
+
export { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, mapSessionMessagesToUpdates, mapSdkSessions, looksLikeAuthRequired, normalizeToolKind, normalizeToolResultText, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, unwrapToolUseResult, };
|
|
17
17
|
const sessions = new Map();
|
|
18
|
-
const EXPECTED_AGENT_SDK_VERSION = "0.2.
|
|
18
|
+
const EXPECTED_AGENT_SDK_VERSION = "0.2.63";
|
|
19
|
+
const SESSION_LIST_LIMIT = 50;
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const permissionDebugEnabled = process.env.CLAUDE_RS_SDK_PERMISSION_DEBUG === "1" || process.env.CLAUDE_RS_SDK_DEBUG === "1";
|
|
21
22
|
export function resolveInstalledAgentSdkVersion() {
|
|
@@ -130,6 +131,7 @@ function emitConnectEvent(session) {
|
|
|
130
131
|
const staleSessions = session.sessionsToCloseAfterConnect;
|
|
131
132
|
session.sessionsToCloseAfterConnect = undefined;
|
|
132
133
|
if (!staleSessions || staleSessions.length === 0) {
|
|
134
|
+
refreshSessionsList();
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
137
|
void (async () => {
|
|
@@ -142,8 +144,25 @@ function emitConnectEvent(session) {
|
|
|
142
144
|
}
|
|
143
145
|
await closeSession(stale);
|
|
144
146
|
}
|
|
147
|
+
refreshSessionsList();
|
|
145
148
|
})();
|
|
146
149
|
}
|
|
150
|
+
async function emitSessionsList(requestId) {
|
|
151
|
+
try {
|
|
152
|
+
const sdkSessions = await listSessions({ limit: SESSION_LIST_LIMIT });
|
|
153
|
+
writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
console.error(`[sdk warn] listSessions failed: ${message}`);
|
|
158
|
+
writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function refreshSessionsList() {
|
|
162
|
+
void emitSessionsList().catch(() => {
|
|
163
|
+
// Defensive no-op.
|
|
164
|
+
});
|
|
165
|
+
}
|
|
147
166
|
function textFromPrompt(command) {
|
|
148
167
|
const chunks = command.chunks ?? [];
|
|
149
168
|
return chunks
|
|
@@ -346,7 +365,7 @@ function handleTaskSystemMessage(session, subtype, msg) {
|
|
|
346
365
|
if (!toolUseId) {
|
|
347
366
|
return;
|
|
348
367
|
}
|
|
349
|
-
const toolCall = ensureToolCallVisible(session, toolUseId, "
|
|
368
|
+
const toolCall = ensureToolCallVisible(session, toolUseId, "Agent", {});
|
|
350
369
|
if (toolCall.status === "pending") {
|
|
351
370
|
toolCall.status = "in_progress";
|
|
352
371
|
emitSessionUpdate(session.sessionId, {
|
|
@@ -568,11 +587,150 @@ function numberField(record, ...keys) {
|
|
|
568
587
|
}
|
|
569
588
|
return undefined;
|
|
570
589
|
}
|
|
590
|
+
export function parseFastModeState(value) {
|
|
591
|
+
if (value === "off" || value === "cooldown" || value === "on") {
|
|
592
|
+
return value;
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
export function parseRateLimitStatus(value) {
|
|
597
|
+
if (value === "allowed" || value === "allowed_warning" || value === "rejected") {
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
export function buildRateLimitUpdate(rateLimitInfo) {
|
|
603
|
+
const info = asRecordOrNull(rateLimitInfo);
|
|
604
|
+
if (!info) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const status = parseRateLimitStatus(info.status);
|
|
608
|
+
if (!status) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const update = {
|
|
612
|
+
type: "rate_limit_update",
|
|
613
|
+
status,
|
|
614
|
+
};
|
|
615
|
+
const resetsAt = numberField(info, "resetsAt");
|
|
616
|
+
if (resetsAt !== undefined) {
|
|
617
|
+
update.resets_at = resetsAt;
|
|
618
|
+
}
|
|
619
|
+
const utilization = numberField(info, "utilization");
|
|
620
|
+
if (utilization !== undefined) {
|
|
621
|
+
update.utilization = utilization;
|
|
622
|
+
}
|
|
623
|
+
if (typeof info.rateLimitType === "string" && info.rateLimitType.length > 0) {
|
|
624
|
+
update.rate_limit_type = info.rateLimitType;
|
|
625
|
+
}
|
|
626
|
+
const overageStatus = parseRateLimitStatus(info.overageStatus);
|
|
627
|
+
if (overageStatus) {
|
|
628
|
+
update.overage_status = overageStatus;
|
|
629
|
+
}
|
|
630
|
+
const overageResetsAt = numberField(info, "overageResetsAt");
|
|
631
|
+
if (overageResetsAt !== undefined) {
|
|
632
|
+
update.overage_resets_at = overageResetsAt;
|
|
633
|
+
}
|
|
634
|
+
if (typeof info.overageDisabledReason === "string" && info.overageDisabledReason.length > 0) {
|
|
635
|
+
update.overage_disabled_reason = info.overageDisabledReason;
|
|
636
|
+
}
|
|
637
|
+
if (typeof info.isUsingOverage === "boolean") {
|
|
638
|
+
update.is_using_overage = info.isUsingOverage;
|
|
639
|
+
}
|
|
640
|
+
const surpassedThreshold = numberField(info, "surpassedThreshold");
|
|
641
|
+
if (surpassedThreshold !== undefined) {
|
|
642
|
+
update.surpassed_threshold = surpassedThreshold;
|
|
643
|
+
}
|
|
644
|
+
return update;
|
|
645
|
+
}
|
|
646
|
+
function availableAgentsSignature(agents) {
|
|
647
|
+
return JSON.stringify(agents);
|
|
648
|
+
}
|
|
649
|
+
function normalizeAvailableAgentName(value) {
|
|
650
|
+
if (typeof value !== "string") {
|
|
651
|
+
return "";
|
|
652
|
+
}
|
|
653
|
+
return value.trim();
|
|
654
|
+
}
|
|
655
|
+
export function mapAvailableAgents(value) {
|
|
656
|
+
if (!Array.isArray(value)) {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
const byName = new Map();
|
|
660
|
+
for (const entry of value) {
|
|
661
|
+
if (!entry || typeof entry !== "object") {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const record = entry;
|
|
665
|
+
const name = normalizeAvailableAgentName(record.name);
|
|
666
|
+
if (!name) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
670
|
+
const model = typeof record.model === "string" && record.model.trim().length > 0 ? record.model : undefined;
|
|
671
|
+
const existing = byName.get(name);
|
|
672
|
+
if (!existing) {
|
|
673
|
+
byName.set(name, { name, description, model });
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (existing.description.trim().length === 0 && description.trim().length > 0) {
|
|
677
|
+
existing.description = description;
|
|
678
|
+
}
|
|
679
|
+
if (!existing.model && model) {
|
|
680
|
+
existing.model = model;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
684
|
+
}
|
|
685
|
+
function mapAvailableAgentsFromNames(value) {
|
|
686
|
+
if (!Array.isArray(value)) {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
const byName = new Map();
|
|
690
|
+
for (const entry of value) {
|
|
691
|
+
const name = normalizeAvailableAgentName(entry);
|
|
692
|
+
if (!name || byName.has(name)) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
byName.set(name, { name, description: "" });
|
|
696
|
+
}
|
|
697
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
698
|
+
}
|
|
699
|
+
function emitAvailableAgentsIfChanged(session, agents) {
|
|
700
|
+
const signature = availableAgentsSignature(agents);
|
|
701
|
+
if (session.lastAvailableAgentsSignature === signature) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
session.lastAvailableAgentsSignature = signature;
|
|
705
|
+
emitSessionUpdate(session.sessionId, { type: "available_agents_update", agents });
|
|
706
|
+
}
|
|
707
|
+
function refreshAvailableAgents(session) {
|
|
708
|
+
if (typeof session.query.supportedAgents !== "function") {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
void session.query
|
|
712
|
+
.supportedAgents()
|
|
713
|
+
.then((agents) => {
|
|
714
|
+
emitAvailableAgentsIfChanged(session, mapAvailableAgents(agents));
|
|
715
|
+
})
|
|
716
|
+
.catch(() => {
|
|
717
|
+
// Best-effort only.
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function emitFastModeUpdateIfChanged(session, value) {
|
|
721
|
+
const next = parseFastModeState(value);
|
|
722
|
+
if (!next || next === session.fastModeState) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
session.fastModeState = next;
|
|
726
|
+
emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
|
|
727
|
+
}
|
|
571
728
|
function handleResultMessage(session, message) {
|
|
572
729
|
const usageUpdate = buildUsageUpdateFromResultForSession(session, message);
|
|
573
730
|
if (usageUpdate) {
|
|
574
731
|
emitSessionUpdate(session.sessionId, usageUpdate);
|
|
575
732
|
}
|
|
733
|
+
emitFastModeUpdateIfChanged(session, message.fast_mode_state);
|
|
576
734
|
const subtype = typeof message.subtype === "string" ? message.subtype : "";
|
|
577
735
|
if (subtype === "success") {
|
|
578
736
|
session.lastAssistantError = undefined;
|
|
@@ -619,6 +777,7 @@ function handleSdkMessage(session, message) {
|
|
|
619
777
|
if (incomingMode) {
|
|
620
778
|
session.mode = incomingMode;
|
|
621
779
|
}
|
|
780
|
+
emitFastModeUpdateIfChanged(session, msg.fast_mode_state);
|
|
622
781
|
if (!session.connected) {
|
|
623
782
|
emitConnectEvent(session);
|
|
624
783
|
}
|
|
@@ -635,6 +794,7 @@ function handleSdkMessage(session, message) {
|
|
|
635
794
|
: {}),
|
|
636
795
|
});
|
|
637
796
|
session.resumeUpdates = undefined;
|
|
797
|
+
refreshSessionsList();
|
|
638
798
|
}
|
|
639
799
|
if (Array.isArray(msg.slash_commands)) {
|
|
640
800
|
const commands = msg.slash_commands
|
|
@@ -644,6 +804,9 @@ function handleSdkMessage(session, message) {
|
|
|
644
804
|
emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
|
|
645
805
|
}
|
|
646
806
|
}
|
|
807
|
+
if (session.lastAvailableAgentsSignature === undefined && Array.isArray(msg.agents)) {
|
|
808
|
+
emitAvailableAgentsIfChanged(session, mapAvailableAgentsFromNames(msg.agents));
|
|
809
|
+
}
|
|
647
810
|
void session.query
|
|
648
811
|
.supportedCommands()
|
|
649
812
|
.then((commands) => {
|
|
@@ -657,6 +820,7 @@ function handleSdkMessage(session, message) {
|
|
|
657
820
|
.catch(() => {
|
|
658
821
|
// Best-effort only; slash commands from init were already emitted.
|
|
659
822
|
});
|
|
823
|
+
refreshAvailableAgents(session);
|
|
660
824
|
return;
|
|
661
825
|
}
|
|
662
826
|
if (subtype === "status") {
|
|
@@ -671,6 +835,7 @@ function handleSdkMessage(session, message) {
|
|
|
671
835
|
else if (msg.status === null) {
|
|
672
836
|
emitSessionUpdate(session.sessionId, { type: "session_status_update", status: "idle" });
|
|
673
837
|
}
|
|
838
|
+
emitFastModeUpdateIfChanged(session, msg.fast_mode_state);
|
|
674
839
|
return;
|
|
675
840
|
}
|
|
676
841
|
if (subtype === "compact_boundary") {
|
|
@@ -689,6 +854,20 @@ function handleSdkMessage(session, message) {
|
|
|
689
854
|
}
|
|
690
855
|
return;
|
|
691
856
|
}
|
|
857
|
+
if (subtype === "local_command_output") {
|
|
858
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
859
|
+
if (content.trim().length > 0) {
|
|
860
|
+
emitSessionUpdate(session.sessionId, {
|
|
861
|
+
type: "agent_message_chunk",
|
|
862
|
+
content: { type: "text", text: content },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (subtype === "elicitation_complete") {
|
|
868
|
+
// No-op: elicitation flow is auto-canceled in the onElicitation callback.
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
692
871
|
handleTaskSystemMessage(session, subtype, msg);
|
|
693
872
|
return;
|
|
694
873
|
}
|
|
@@ -729,6 +908,13 @@ function handleSdkMessage(session, message) {
|
|
|
729
908
|
}
|
|
730
909
|
return;
|
|
731
910
|
}
|
|
911
|
+
if (type === "rate_limit_event") {
|
|
912
|
+
const update = buildRateLimitUpdate(msg.rate_limit_info);
|
|
913
|
+
if (update) {
|
|
914
|
+
emitSessionUpdate(session.sessionId, update);
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
732
918
|
if (type === "user") {
|
|
733
919
|
handleUserToolResultBlocks(session, msg);
|
|
734
920
|
const toolUseId = typeof msg.parent_tool_use_id === "string" ? msg.parent_tool_use_id : "";
|
|
@@ -751,6 +937,42 @@ function handleSdkMessage(session, message) {
|
|
|
751
937
|
}
|
|
752
938
|
const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
|
|
753
939
|
const QUESTION_CHOICE_KIND = "question_choice";
|
|
940
|
+
const EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode";
|
|
941
|
+
const PLAN_APPROVE_KIND = "plan_approve";
|
|
942
|
+
const PLAN_REJECT_KIND = "plan_reject";
|
|
943
|
+
async function requestExitPlanModeApproval(session, toolUseId, inputData, baseToolCall) {
|
|
944
|
+
const options = [
|
|
945
|
+
{
|
|
946
|
+
option_id: "approve",
|
|
947
|
+
name: "Approve",
|
|
948
|
+
description: "Approve the plan and continue",
|
|
949
|
+
kind: PLAN_APPROVE_KIND,
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
option_id: "reject",
|
|
953
|
+
name: "Reject",
|
|
954
|
+
description: "Reject the plan",
|
|
955
|
+
kind: PLAN_REJECT_KIND,
|
|
956
|
+
},
|
|
957
|
+
];
|
|
958
|
+
const request = {
|
|
959
|
+
tool_call: baseToolCall,
|
|
960
|
+
options,
|
|
961
|
+
};
|
|
962
|
+
const outcome = await new Promise((resolve) => {
|
|
963
|
+
session.pendingPermissions.set(toolUseId, {
|
|
964
|
+
onOutcome: resolve,
|
|
965
|
+
toolName: EXIT_PLAN_MODE_TOOL_NAME,
|
|
966
|
+
inputData,
|
|
967
|
+
});
|
|
968
|
+
writeEvent({ event: "permission_request", session_id: session.sessionId, request });
|
|
969
|
+
});
|
|
970
|
+
if (outcome.outcome !== "selected" || outcome.option_id === "reject") {
|
|
971
|
+
setToolCallStatus(session, toolUseId, "failed", "Plan rejected");
|
|
972
|
+
return { behavior: "deny", message: "Plan rejected", toolUseID: toolUseId };
|
|
973
|
+
}
|
|
974
|
+
return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
|
|
975
|
+
}
|
|
754
976
|
function parseAskUserQuestionPrompts(inputData) {
|
|
755
977
|
const rawQuestions = Array.isArray(inputData.questions) ? inputData.questions : [];
|
|
756
978
|
const prompts = [];
|
|
@@ -906,8 +1128,9 @@ async function createSession(params) {
|
|
|
906
1128
|
let session;
|
|
907
1129
|
const canUseTool = async (toolName, inputData, options) => {
|
|
908
1130
|
const toolUseId = options.toolUseID;
|
|
909
|
-
if (toolName ===
|
|
910
|
-
|
|
1131
|
+
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
1132
|
+
const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
|
|
1133
|
+
return await requestExitPlanModeApproval(session, toolUseId, inputData, existing);
|
|
911
1134
|
}
|
|
912
1135
|
logPermissionDebug(`request tool_use_id=${toolUseId} tool=${toolName} blocked_path=${options.blockedPath ?? "<none>"} ` +
|
|
913
1136
|
`decision_reason=${options.decisionReason ?? "<none>"} suggestions=${formatPermissionUpdates(options.suggestions)}`);
|
|
@@ -981,6 +1204,19 @@ async function createSession(params) {
|
|
|
981
1204
|
resume: params.resume,
|
|
982
1205
|
model: params.model,
|
|
983
1206
|
canUseTool,
|
|
1207
|
+
onElicitation: async (request) => {
|
|
1208
|
+
const requestMode = typeof request.mode === "string" ? request.mode : "unknown";
|
|
1209
|
+
const requestServer = typeof request.serverName === "string" && request.serverName.trim().length > 0
|
|
1210
|
+
? request.serverName
|
|
1211
|
+
: "unknown";
|
|
1212
|
+
const requestMessage = typeof request.message === "string" && request.message.trim().length > 0
|
|
1213
|
+
? request.message
|
|
1214
|
+
: "<no message>";
|
|
1215
|
+
console.error(`[sdk warn] elicitation unsupported without MCP settings UI; ` +
|
|
1216
|
+
`auto-canceling session_id=${session.sessionId} server=${requestServer} ` +
|
|
1217
|
+
`mode=${requestMode} message=${JSON.stringify(requestMessage)}`);
|
|
1218
|
+
return { action: "cancel" };
|
|
1219
|
+
},
|
|
984
1220
|
},
|
|
985
1221
|
});
|
|
986
1222
|
}
|
|
@@ -995,6 +1231,7 @@ async function createSession(params) {
|
|
|
995
1231
|
cwd: params.cwd,
|
|
996
1232
|
model: params.model ?? "default",
|
|
997
1233
|
mode: startMode,
|
|
1234
|
+
fastModeState: "off",
|
|
998
1235
|
yolo: params.yolo,
|
|
999
1236
|
query: queryHandle,
|
|
1000
1237
|
input,
|
|
@@ -1022,6 +1259,7 @@ async function createSession(params) {
|
|
|
1022
1259
|
if (!session.connected) {
|
|
1023
1260
|
emitConnectEvent(session);
|
|
1024
1261
|
}
|
|
1262
|
+
emitFastModeUpdateIfChanged(session, result.fast_mode_state);
|
|
1025
1263
|
const commands = Array.isArray(result.commands)
|
|
1026
1264
|
? result.commands.map((command) => ({
|
|
1027
1265
|
name: command.name,
|
|
@@ -1032,6 +1270,8 @@ async function createSession(params) {
|
|
|
1032
1270
|
if (commands.length > 0) {
|
|
1033
1271
|
emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
|
|
1034
1272
|
}
|
|
1273
|
+
emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
|
|
1274
|
+
refreshAvailableAgents(session);
|
|
1035
1275
|
})
|
|
1036
1276
|
.catch((error) => {
|
|
1037
1277
|
if (session.connected) {
|
|
@@ -1129,21 +1369,12 @@ async function handleCommand(command, requestId) {
|
|
|
1129
1369
|
capabilities: {
|
|
1130
1370
|
prompt_image: false,
|
|
1131
1371
|
prompt_embedded_context: true,
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
supports_resume: true,
|
|
1372
|
+
supports_session_listing: true,
|
|
1373
|
+
supports_resume_session: true,
|
|
1135
1374
|
},
|
|
1136
1375
|
},
|
|
1137
1376
|
}, requestId);
|
|
1138
|
-
|
|
1139
|
-
event: "sessions_listed",
|
|
1140
|
-
sessions: listRecentPersistedSessions().map((entry) => ({
|
|
1141
|
-
session_id: entry.session_id,
|
|
1142
|
-
cwd: entry.cwd,
|
|
1143
|
-
...(entry.title ? { title: entry.title } : {}),
|
|
1144
|
-
...(entry.updated_at ? { updated_at: entry.updated_at } : {}),
|
|
1145
|
-
})),
|
|
1146
|
-
});
|
|
1377
|
+
await emitSessionsList(requestId);
|
|
1147
1378
|
return;
|
|
1148
1379
|
case "create_session":
|
|
1149
1380
|
await createSession({
|
|
@@ -1155,18 +1386,20 @@ async function handleCommand(command, requestId) {
|
|
|
1155
1386
|
requestId,
|
|
1156
1387
|
});
|
|
1157
1388
|
return;
|
|
1158
|
-
case "
|
|
1159
|
-
const persisted = resolvePersistedSessionEntry(command.session_id);
|
|
1160
|
-
if (!persisted) {
|
|
1161
|
-
slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
const resumeUpdates = extractSessionHistoryUpdatesFromJsonl(persisted.file_path);
|
|
1165
|
-
const staleSessions = Array.from(sessions.values());
|
|
1166
|
-
const hadActiveSession = staleSessions.length > 0;
|
|
1389
|
+
case "resume_session": {
|
|
1167
1390
|
try {
|
|
1391
|
+
const sdkSessions = await listSessions();
|
|
1392
|
+
const matched = sdkSessions.find((entry) => entry.sessionId === command.session_id);
|
|
1393
|
+
if (!matched) {
|
|
1394
|
+
slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const historyMessages = await getSessionMessages(command.session_id, matched.cwd ? { dir: matched.cwd } : undefined);
|
|
1398
|
+
const resumeUpdates = mapSessionMessagesToUpdates(historyMessages);
|
|
1399
|
+
const staleSessions = Array.from(sessions.values());
|
|
1400
|
+
const hadActiveSession = staleSessions.length > 0;
|
|
1168
1401
|
await createSession({
|
|
1169
|
-
cwd:
|
|
1402
|
+
cwd: matched.cwd ?? process.cwd(),
|
|
1170
1403
|
yolo: false,
|
|
1171
1404
|
resume: command.session_id,
|
|
1172
1405
|
...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
3
|
+
import { CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, mapAvailableAgents, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
7
4
|
test("parseCommandEnvelope validates initialize command", () => {
|
|
8
5
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
9
6
|
request_id: "req-1",
|
|
@@ -17,15 +14,15 @@ test("parseCommandEnvelope validates initialize command", () => {
|
|
|
17
14
|
}
|
|
18
15
|
assert.equal(parsed.command.cwd, "C:/work");
|
|
19
16
|
});
|
|
20
|
-
test("parseCommandEnvelope validates
|
|
17
|
+
test("parseCommandEnvelope validates resume_session command without cwd", () => {
|
|
21
18
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
22
19
|
request_id: "req-2",
|
|
23
|
-
command: "
|
|
20
|
+
command: "resume_session",
|
|
24
21
|
session_id: "session-123",
|
|
25
22
|
}));
|
|
26
23
|
assert.equal(parsed.requestId, "req-2");
|
|
27
|
-
assert.equal(parsed.command.command, "
|
|
28
|
-
if (parsed.command.command !== "
|
|
24
|
+
assert.equal(parsed.command.command, "resume_session");
|
|
25
|
+
if (parsed.command.command !== "resume_session") {
|
|
29
26
|
throw new Error("unexpected command variant");
|
|
30
27
|
}
|
|
31
28
|
assert.equal(parsed.command.session_id, "session-123");
|
|
@@ -37,9 +34,76 @@ test("normalizeToolKind maps known tool names", () => {
|
|
|
37
34
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
38
35
|
assert.equal(normalizeToolKind("Delete"), "delete");
|
|
39
36
|
assert.equal(normalizeToolKind("Move"), "move");
|
|
37
|
+
assert.equal(normalizeToolKind("Task"), "think");
|
|
38
|
+
assert.equal(normalizeToolKind("Agent"), "think");
|
|
40
39
|
assert.equal(normalizeToolKind("ExitPlanMode"), "switch_mode");
|
|
41
40
|
assert.equal(normalizeToolKind("TodoWrite"), "other");
|
|
42
41
|
});
|
|
42
|
+
test("parseFastModeState accepts known values and rejects unknown values", () => {
|
|
43
|
+
assert.equal(parseFastModeState("off"), "off");
|
|
44
|
+
assert.equal(parseFastModeState("cooldown"), "cooldown");
|
|
45
|
+
assert.equal(parseFastModeState("on"), "on");
|
|
46
|
+
assert.equal(parseFastModeState("CD"), null);
|
|
47
|
+
assert.equal(parseFastModeState(undefined), null);
|
|
48
|
+
});
|
|
49
|
+
test("parseRateLimitStatus accepts known values and rejects unknown values", () => {
|
|
50
|
+
assert.equal(parseRateLimitStatus("allowed"), "allowed");
|
|
51
|
+
assert.equal(parseRateLimitStatus("allowed_warning"), "allowed_warning");
|
|
52
|
+
assert.equal(parseRateLimitStatus("rejected"), "rejected");
|
|
53
|
+
assert.equal(parseRateLimitStatus("warn"), null);
|
|
54
|
+
assert.equal(parseRateLimitStatus(undefined), null);
|
|
55
|
+
});
|
|
56
|
+
test("buildRateLimitUpdate maps SDK fields to wire shape", () => {
|
|
57
|
+
const update = buildRateLimitUpdate({
|
|
58
|
+
status: "allowed_warning",
|
|
59
|
+
resetsAt: 1_741_280_000,
|
|
60
|
+
utilization: 0.92,
|
|
61
|
+
rateLimitType: "five_hour",
|
|
62
|
+
overageStatus: "rejected",
|
|
63
|
+
overageResetsAt: 1_741_280_600,
|
|
64
|
+
overageDisabledReason: "out_of_credits",
|
|
65
|
+
isUsingOverage: false,
|
|
66
|
+
surpassedThreshold: 0.9,
|
|
67
|
+
});
|
|
68
|
+
assert.deepEqual(update, {
|
|
69
|
+
type: "rate_limit_update",
|
|
70
|
+
status: "allowed_warning",
|
|
71
|
+
resets_at: 1_741_280_000,
|
|
72
|
+
utilization: 0.92,
|
|
73
|
+
rate_limit_type: "five_hour",
|
|
74
|
+
overage_status: "rejected",
|
|
75
|
+
overage_resets_at: 1_741_280_600,
|
|
76
|
+
overage_disabled_reason: "out_of_credits",
|
|
77
|
+
is_using_overage: false,
|
|
78
|
+
surpassed_threshold: 0.9,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test("buildRateLimitUpdate rejects invalid payloads", () => {
|
|
82
|
+
assert.equal(buildRateLimitUpdate(null), null);
|
|
83
|
+
assert.equal(buildRateLimitUpdate({}), null);
|
|
84
|
+
assert.equal(buildRateLimitUpdate({ status: "warning" }), null);
|
|
85
|
+
assert.deepEqual(buildRateLimitUpdate({
|
|
86
|
+
status: "rejected",
|
|
87
|
+
overageStatus: "bad_status",
|
|
88
|
+
}), { type: "rate_limit_update", status: "rejected" });
|
|
89
|
+
});
|
|
90
|
+
test("mapAvailableAgents normalizes and deduplicates agents", () => {
|
|
91
|
+
const agents = mapAvailableAgents([
|
|
92
|
+
{ name: "reviewer", description: "", model: "" },
|
|
93
|
+
{ name: "reviewer", description: "Reviews code", model: "haiku" },
|
|
94
|
+
{ name: "explore", description: "Explore codebase", model: "sonnet" },
|
|
95
|
+
{ name: " ", description: "ignored" },
|
|
96
|
+
{},
|
|
97
|
+
]);
|
|
98
|
+
assert.deepEqual(agents, [
|
|
99
|
+
{ name: "explore", description: "Explore codebase", model: "sonnet" },
|
|
100
|
+
{ name: "reviewer", description: "Reviews code", model: "haiku" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
test("mapAvailableAgents rejects non-array payload", () => {
|
|
104
|
+
assert.deepEqual(mapAvailableAgents(null), []);
|
|
105
|
+
assert.deepEqual(mapAvailableAgents({}), []);
|
|
106
|
+
});
|
|
43
107
|
test("createToolCall builds edit diff content", () => {
|
|
44
108
|
const toolCall = createToolCall("tc-1", "Edit", {
|
|
45
109
|
file_path: "src/main.rs",
|
|
@@ -100,6 +164,26 @@ test("normalizeToolResultText collapses persisted-output payload to first meanin
|
|
|
100
164
|
`);
|
|
101
165
|
assert.equal(normalized, "Output too large (132.5KB). Full output saved to: C:\\tmp\\tool-results\\bbf63b9.txt");
|
|
102
166
|
});
|
|
167
|
+
test("normalizeToolResultText does not sanitize non-error output", () => {
|
|
168
|
+
const text = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
169
|
+
assert.equal(normalizeToolResultText(text), text);
|
|
170
|
+
});
|
|
171
|
+
test("normalizeToolResultText sanitizes exact SDK rejection payloads for errors", () => {
|
|
172
|
+
const cancelledText = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
173
|
+
assert.equal(normalizeToolResultText(cancelledText, true), "Cancelled by user.");
|
|
174
|
+
const deniedText = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.";
|
|
175
|
+
assert.equal(normalizeToolResultText(deniedText, true), "Permission denied.");
|
|
176
|
+
});
|
|
177
|
+
test("normalizeToolResultText sanitizes SDK rejection prefixes with user follow-up", () => {
|
|
178
|
+
const cancelledWithUserMessage = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\nPlease skip this";
|
|
179
|
+
assert.equal(normalizeToolResultText(cancelledWithUserMessage, true), "Cancelled by user.");
|
|
180
|
+
const deniedWithUserMessage = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). The user said:\nNot now";
|
|
181
|
+
assert.equal(normalizeToolResultText(deniedWithUserMessage, true), "Permission denied.");
|
|
182
|
+
});
|
|
183
|
+
test("normalizeToolResultText does not sanitize substring matches in error output", () => {
|
|
184
|
+
const bashOutput = "grep output: doesn't want to proceed with this tool use";
|
|
185
|
+
assert.equal(normalizeToolResultText(bashOutput, true), bashOutput);
|
|
186
|
+
});
|
|
103
187
|
test("cache split policy defaults stay aligned with UI thresholds", () => {
|
|
104
188
|
assert.equal(CACHE_SPLIT_POLICY.softLimitBytes, 1536);
|
|
105
189
|
assert.equal(CACHE_SPLIT_POLICY.hardLimitBytes, 4096);
|
|
@@ -124,6 +208,13 @@ test("buildToolResultFields uses normalized persisted-output text", () => {
|
|
|
124
208
|
},
|
|
125
209
|
]);
|
|
126
210
|
});
|
|
211
|
+
test("buildToolResultFields sanitizes SDK rejection text only for failed results", () => {
|
|
212
|
+
const sdkRejectionText = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
213
|
+
const successFields = buildToolResultFields(false, sdkRejectionText);
|
|
214
|
+
assert.equal(successFields.raw_output, sdkRejectionText);
|
|
215
|
+
const errorFields = buildToolResultFields(true, sdkRejectionText);
|
|
216
|
+
assert.equal(errorFields.raw_output, "Cancelled by user.");
|
|
217
|
+
});
|
|
127
218
|
test("buildToolResultFields maps structured Write output to diff content", () => {
|
|
128
219
|
const base = createToolCall("tc-w", "Write", {
|
|
129
220
|
file_path: "src/main.ts",
|
|
@@ -334,122 +425,127 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
334
425
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
335
426
|
});
|
|
336
427
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
337
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.
|
|
428
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.63");
|
|
338
429
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
339
430
|
});
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const filePath = path.join(dir, "session.jsonl");
|
|
343
|
-
fs.writeFileSync(filePath, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`, "utf8");
|
|
344
|
-
try {
|
|
345
|
-
run(filePath);
|
|
346
|
-
}
|
|
347
|
-
finally {
|
|
348
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
test("extractSessionHistoryUpdatesFromJsonl parses nested progress message records", () => {
|
|
352
|
-
const lines = [
|
|
431
|
+
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
432
|
+
const updates = mapSessionMessagesToUpdates([
|
|
353
433
|
{
|
|
354
434
|
type: "user",
|
|
435
|
+
uuid: "u1",
|
|
436
|
+
session_id: "s1",
|
|
437
|
+
parent_tool_use_id: null,
|
|
355
438
|
message: {
|
|
356
439
|
role: "user",
|
|
357
440
|
content: [{ type: "text", text: "Top-level user prompt" }],
|
|
358
441
|
},
|
|
359
442
|
},
|
|
360
443
|
{
|
|
361
|
-
type: "
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
input_tokens: 11,
|
|
378
|
-
output_tokens: 7,
|
|
379
|
-
cache_read_input_tokens: 5,
|
|
380
|
-
cache_creation_input_tokens: 3,
|
|
381
|
-
},
|
|
382
|
-
},
|
|
444
|
+
type: "assistant",
|
|
445
|
+
uuid: "a1",
|
|
446
|
+
session_id: "s1",
|
|
447
|
+
parent_tool_use_id: null,
|
|
448
|
+
message: {
|
|
449
|
+
id: "msg-1",
|
|
450
|
+
role: "assistant",
|
|
451
|
+
content: [
|
|
452
|
+
{ type: "tool_use", id: "tool-1", name: "Bash", input: { command: "echo hello" } },
|
|
453
|
+
{ type: "text", text: "Nested assistant final" },
|
|
454
|
+
],
|
|
455
|
+
usage: {
|
|
456
|
+
input_tokens: 11,
|
|
457
|
+
output_tokens: 7,
|
|
458
|
+
cache_read_input_tokens: 5,
|
|
459
|
+
cache_creation_input_tokens: 3,
|
|
383
460
|
},
|
|
384
461
|
},
|
|
385
462
|
},
|
|
386
463
|
{
|
|
387
|
-
type: "
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
400
|
-
],
|
|
464
|
+
type: "user",
|
|
465
|
+
uuid: "u2",
|
|
466
|
+
session_id: "s1",
|
|
467
|
+
parent_tool_use_id: null,
|
|
468
|
+
message: {
|
|
469
|
+
role: "user",
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: "tool_result",
|
|
473
|
+
tool_use_id: "tool-1",
|
|
474
|
+
content: "ok",
|
|
475
|
+
is_error: false,
|
|
401
476
|
},
|
|
402
|
-
|
|
477
|
+
],
|
|
403
478
|
},
|
|
404
479
|
},
|
|
480
|
+
]);
|
|
481
|
+
const variantCounts = new Map();
|
|
482
|
+
for (const update of updates) {
|
|
483
|
+
variantCounts.set(update.type, (variantCounts.get(update.type) ?? 0) + 1);
|
|
484
|
+
}
|
|
485
|
+
assert.equal(variantCounts.get("user_message_chunk"), 1);
|
|
486
|
+
assert.equal(variantCounts.get("agent_message_chunk"), 1);
|
|
487
|
+
assert.equal(variantCounts.get("tool_call"), 1);
|
|
488
|
+
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
489
|
+
assert.equal(variantCounts.get("usage_update"), 1);
|
|
490
|
+
const usage = updates.find((update) => update.type === "usage_update");
|
|
491
|
+
assert.ok(usage && usage.type === "usage_update");
|
|
492
|
+
assert.deepEqual(usage.usage, {
|
|
493
|
+
input_tokens: 11,
|
|
494
|
+
output_tokens: 7,
|
|
495
|
+
cache_read_tokens: 5,
|
|
496
|
+
cache_write_tokens: 3,
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
test("mapSessionMessagesToUpdates ignores unsupported records", () => {
|
|
500
|
+
const updates = mapSessionMessagesToUpdates([
|
|
405
501
|
{
|
|
406
|
-
type: "
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
content: [{ type: "text", text: "Nested assistant final" }],
|
|
414
|
-
usage: {
|
|
415
|
-
input_tokens: 11,
|
|
416
|
-
output_tokens: 7,
|
|
417
|
-
cache_read_input_tokens: 5,
|
|
418
|
-
cache_creation_input_tokens: 3,
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
},
|
|
502
|
+
type: "user",
|
|
503
|
+
uuid: "u1",
|
|
504
|
+
session_id: "s1",
|
|
505
|
+
parent_tool_use_id: null,
|
|
506
|
+
message: {
|
|
507
|
+
role: "assistant",
|
|
508
|
+
content: [{ type: "thinking", thinking: "h" }],
|
|
422
509
|
},
|
|
423
510
|
},
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
const updates = extractSessionHistoryUpdatesFromJsonl(filePath);
|
|
427
|
-
const variantCounts = new Map();
|
|
428
|
-
for (const update of updates) {
|
|
429
|
-
variantCounts.set(update.type, (variantCounts.get(update.type) ?? 0) + 1);
|
|
430
|
-
}
|
|
431
|
-
assert.equal(variantCounts.get("user_message_chunk"), 1);
|
|
432
|
-
assert.equal(variantCounts.get("agent_message_chunk"), 1);
|
|
433
|
-
assert.equal(variantCounts.get("tool_call"), 1);
|
|
434
|
-
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
435
|
-
assert.equal(variantCounts.get("usage_update"), 1);
|
|
436
|
-
const usage = updates.find((update) => update.type === "usage_update");
|
|
437
|
-
assert.ok(usage && usage.type === "usage_update");
|
|
438
|
-
assert.deepEqual(usage.usage, {
|
|
439
|
-
input_tokens: 11,
|
|
440
|
-
output_tokens: 7,
|
|
441
|
-
cache_read_tokens: 5,
|
|
442
|
-
cache_write_tokens: 3,
|
|
443
|
-
});
|
|
444
|
-
});
|
|
511
|
+
]);
|
|
512
|
+
assert.equal(updates.length, 0);
|
|
445
513
|
});
|
|
446
|
-
test("
|
|
447
|
-
|
|
448
|
-
{
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
514
|
+
test("mapSdkSessions normalizes and sorts sessions", () => {
|
|
515
|
+
const mapped = mapSdkSessions([
|
|
516
|
+
{
|
|
517
|
+
sessionId: "older",
|
|
518
|
+
summary: " Older summary ",
|
|
519
|
+
lastModified: 100,
|
|
520
|
+
fileSize: 10,
|
|
521
|
+
cwd: "C:/work",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
sessionId: "latest",
|
|
525
|
+
summary: "",
|
|
526
|
+
lastModified: 200,
|
|
527
|
+
fileSize: 20,
|
|
528
|
+
customTitle: "Custom title",
|
|
529
|
+
gitBranch: "main",
|
|
530
|
+
firstPrompt: "hello",
|
|
531
|
+
},
|
|
532
|
+
]);
|
|
533
|
+
assert.deepEqual(mapped, [
|
|
534
|
+
{
|
|
535
|
+
session_id: "latest",
|
|
536
|
+
summary: "Custom title",
|
|
537
|
+
last_modified_ms: 200,
|
|
538
|
+
file_size_bytes: 20,
|
|
539
|
+
git_branch: "main",
|
|
540
|
+
custom_title: "Custom title",
|
|
541
|
+
first_prompt: "hello",
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
session_id: "older",
|
|
545
|
+
summary: "Older summary",
|
|
546
|
+
last_modified_ms: 100,
|
|
547
|
+
file_size_bytes: 10,
|
|
548
|
+
cwd: "C:/work",
|
|
549
|
+
},
|
|
550
|
+
]);
|
|
455
551
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-rust",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Claude Code Rust - native Rust terminal interface for Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.63"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"postinstall": "node ./scripts/postinstall.js",
|
|
36
|
-
"prepack": "
|
|
36
|
+
"prepack": "pnpm -C agent-sdk run build"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|