claude-code-rust 0.3.0 → 0.4.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.
@@ -0,0 +1,99 @@
1
+ const SESSION_PERMISSION_DESTINATIONS = new Set(["session", "cliArg"]);
2
+ const PERSISTENT_PERMISSION_DESTINATIONS = new Set(["userSettings", "projectSettings", "localSettings"]);
3
+ function formatPermissionRule(rule) {
4
+ return rule.ruleContent === undefined ? rule.toolName : `${rule.toolName}(${rule.ruleContent})`;
5
+ }
6
+ export function formatPermissionUpdates(updates) {
7
+ if (!updates || updates.length === 0) {
8
+ return "<none>";
9
+ }
10
+ return updates
11
+ .map((update) => {
12
+ if (update.type === "addRules" || update.type === "replaceRules" || update.type === "removeRules") {
13
+ const rules = update.rules.map((rule) => formatPermissionRule(rule)).join(", ");
14
+ return `${update.type}:${update.behavior}:${update.destination}=[${rules}]`;
15
+ }
16
+ if (update.type === "setMode") {
17
+ return `${update.type}:${update.mode}:${update.destination}`;
18
+ }
19
+ return `${update.type}:${update.destination}=[${update.directories.join(", ")}]`;
20
+ })
21
+ .join(" | ");
22
+ }
23
+ function splitPermissionSuggestionsByScope(suggestions) {
24
+ if (!suggestions || suggestions.length === 0) {
25
+ return { session: [], persistent: [] };
26
+ }
27
+ const session = [];
28
+ const persistent = [];
29
+ for (const suggestion of suggestions) {
30
+ if (SESSION_PERMISSION_DESTINATIONS.has(suggestion.destination)) {
31
+ session.push(suggestion);
32
+ continue;
33
+ }
34
+ if (PERSISTENT_PERMISSION_DESTINATIONS.has(suggestion.destination)) {
35
+ persistent.push(suggestion);
36
+ continue;
37
+ }
38
+ session.push(suggestion);
39
+ }
40
+ return { session, persistent };
41
+ }
42
+ export function permissionOptionsFromSuggestions(suggestions) {
43
+ const scoped = splitPermissionSuggestionsByScope(suggestions);
44
+ const hasSessionScoped = scoped.session.length > 0;
45
+ const hasPersistentScoped = scoped.persistent.length > 0;
46
+ const sessionOnly = hasSessionScoped && !hasPersistentScoped;
47
+ const options = [{ option_id: "allow_once", name: "Allow once", kind: "allow_once" }];
48
+ options.push({
49
+ option_id: sessionOnly ? "allow_session" : "allow_always",
50
+ name: sessionOnly ? "Allow for session" : "Always allow",
51
+ kind: sessionOnly ? "allow_session" : "allow_always",
52
+ });
53
+ options.push({ option_id: "reject_once", name: "Deny", kind: "reject_once" });
54
+ return options;
55
+ }
56
+ export function permissionResultFromOutcome(outcome, toolCallId, inputData, suggestions, toolName) {
57
+ const scopedSuggestions = splitPermissionSuggestionsByScope(suggestions);
58
+ if (outcome.outcome === "selected") {
59
+ if (outcome.option_id === "allow_once") {
60
+ return { behavior: "allow", updatedInput: inputData, toolUseID: toolCallId };
61
+ }
62
+ if (outcome.option_id === "allow_session") {
63
+ const sessionSuggestions = scopedSuggestions.session;
64
+ const fallbackSuggestions = sessionSuggestions.length > 0
65
+ ? sessionSuggestions
66
+ : toolName
67
+ ? [
68
+ {
69
+ type: "addRules",
70
+ rules: [{ toolName }],
71
+ behavior: "allow",
72
+ destination: "session",
73
+ },
74
+ ]
75
+ : undefined;
76
+ return {
77
+ behavior: "allow",
78
+ updatedInput: inputData,
79
+ ...(fallbackSuggestions && fallbackSuggestions.length > 0
80
+ ? { updatedPermissions: fallbackSuggestions }
81
+ : {}),
82
+ toolUseID: toolCallId,
83
+ };
84
+ }
85
+ if (outcome.option_id === "allow_always") {
86
+ const suggestionsForAlways = scopedSuggestions.persistent;
87
+ return {
88
+ behavior: "allow",
89
+ updatedInput: inputData,
90
+ ...(suggestionsForAlways && suggestionsForAlways.length > 0
91
+ ? { updatedPermissions: suggestionsForAlways }
92
+ : {}),
93
+ toolUseID: toolCallId,
94
+ };
95
+ }
96
+ return { behavior: "deny", message: "Permission denied", toolUseID: toolCallId };
97
+ }
98
+ return { behavior: "deny", message: "Permission cancelled", toolUseID: toolCallId };
99
+ }
@@ -0,0 +1,6 @@
1
+ export function asRecordOrNull(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ return null;
4
+ }
5
+ return value;
6
+ }
@@ -0,0 +1,290 @@
1
+ import { asRecordOrNull } from "./shared.js";
2
+ export const TOOL_RESULT_TYPES = new Set([
3
+ "tool_result",
4
+ "tool_search_tool_result",
5
+ "web_fetch_tool_result",
6
+ "web_search_tool_result",
7
+ "code_execution_tool_result",
8
+ "bash_code_execution_tool_result",
9
+ "text_editor_code_execution_tool_result",
10
+ "mcp_tool_result",
11
+ ]);
12
+ export function isToolUseBlockType(blockType) {
13
+ return blockType === "tool_use" || blockType === "server_tool_use" || blockType === "mcp_tool_use";
14
+ }
15
+ export function normalizeToolKind(name) {
16
+ switch (name) {
17
+ case "Bash":
18
+ return "execute";
19
+ case "Read":
20
+ return "read";
21
+ case "Write":
22
+ case "Edit":
23
+ return "edit";
24
+ case "Delete":
25
+ return "delete";
26
+ case "Move":
27
+ return "move";
28
+ case "Glob":
29
+ case "Grep":
30
+ return "search";
31
+ case "WebFetch":
32
+ return "fetch";
33
+ case "TodoWrite":
34
+ return "other";
35
+ case "Task":
36
+ return "think";
37
+ case "ExitPlanMode":
38
+ return "switch_mode";
39
+ default:
40
+ return "think";
41
+ }
42
+ }
43
+ export function toolTitle(name, input) {
44
+ if (name === "Bash") {
45
+ const command = typeof input.command === "string" ? input.command : "";
46
+ return command || "Terminal";
47
+ }
48
+ if (name === "Glob") {
49
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
50
+ const path = typeof input.path === "string" ? input.path : "";
51
+ if (pattern && path) {
52
+ return `Glob ${pattern} in ${path}`;
53
+ }
54
+ if (pattern) {
55
+ return `Glob ${pattern}`;
56
+ }
57
+ if (path) {
58
+ return `Glob ${path}`;
59
+ }
60
+ }
61
+ if (name === "WebFetch") {
62
+ const url = typeof input.url === "string" ? input.url : "";
63
+ if (url) {
64
+ return `WebFetch ${url}`;
65
+ }
66
+ }
67
+ if (name === "WebSearch") {
68
+ const query = typeof input.query === "string" ? input.query : "";
69
+ if (query) {
70
+ return `WebSearch ${query}`;
71
+ }
72
+ }
73
+ if ((name === "Read" || name === "Write" || name === "Edit") && typeof input.file_path === "string") {
74
+ return `${name} ${input.file_path}`;
75
+ }
76
+ return name;
77
+ }
78
+ function editDiffContent(name, input) {
79
+ const filePath = typeof input.file_path === "string" ? input.file_path : "";
80
+ if (!filePath) {
81
+ return [];
82
+ }
83
+ if (name === "Edit") {
84
+ const oldText = typeof input.old_string === "string" ? input.old_string : "";
85
+ const newText = typeof input.new_string === "string" ? input.new_string : "";
86
+ if (!oldText && !newText) {
87
+ return [];
88
+ }
89
+ return [{ type: "diff", old_path: filePath, new_path: filePath, old: oldText, new: newText }];
90
+ }
91
+ if (name === "Write") {
92
+ const newText = typeof input.content === "string" ? input.content : "";
93
+ if (!newText) {
94
+ return [];
95
+ }
96
+ return [{ type: "diff", old_path: filePath, new_path: filePath, old: "", new: newText }];
97
+ }
98
+ return [];
99
+ }
100
+ export function createToolCall(toolUseId, name, input) {
101
+ return {
102
+ tool_call_id: toolUseId,
103
+ title: toolTitle(name, input),
104
+ kind: normalizeToolKind(name),
105
+ status: "pending",
106
+ content: editDiffContent(name, input),
107
+ raw_input: input,
108
+ locations: typeof input.file_path === "string" ? [{ path: input.file_path }] : [],
109
+ meta: {
110
+ claudeCode: {
111
+ toolName: name,
112
+ },
113
+ },
114
+ };
115
+ }
116
+ export function extractText(value) {
117
+ if (typeof value === "string") {
118
+ return value;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return value
122
+ .map((entry) => {
123
+ if (typeof entry === "string") {
124
+ return entry;
125
+ }
126
+ if (entry && typeof entry === "object" && "text" in entry && typeof entry.text === "string") {
127
+ return entry.text;
128
+ }
129
+ return "";
130
+ })
131
+ .filter((part) => part.length > 0)
132
+ .join("\n");
133
+ }
134
+ if (value && typeof value === "object" && "text" in value && typeof value.text === "string") {
135
+ return value.text;
136
+ }
137
+ return "";
138
+ }
139
+ const PERSISTED_OUTPUT_OPEN_TAG = "<persisted-output>";
140
+ const PERSISTED_OUTPUT_CLOSE_TAG = "</persisted-output>";
141
+ function extractPersistedOutputInnerText(text) {
142
+ const lower = text.toLowerCase();
143
+ const openIdx = lower.indexOf(PERSISTED_OUTPUT_OPEN_TAG);
144
+ if (openIdx < 0) {
145
+ return null;
146
+ }
147
+ const bodyStart = openIdx + PERSISTED_OUTPUT_OPEN_TAG.length;
148
+ const closeIdx = lower.indexOf(PERSISTED_OUTPUT_CLOSE_TAG, bodyStart);
149
+ if (closeIdx < 0) {
150
+ return null;
151
+ }
152
+ return text.slice(bodyStart, closeIdx);
153
+ }
154
+ function persistedOutputFirstLine(text) {
155
+ const inner = extractPersistedOutputInnerText(text);
156
+ if (inner === null) {
157
+ return null;
158
+ }
159
+ for (const line of inner.split(/\r?\n/)) {
160
+ const cleaned = line.replace(/^[\s|│┃║]+/u, "").trim();
161
+ if (cleaned.length > 0) {
162
+ return cleaned;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ export function normalizeToolResultText(value) {
168
+ const text = extractText(value);
169
+ if (!text) {
170
+ return "";
171
+ }
172
+ const persistedLine = persistedOutputFirstLine(text);
173
+ if (persistedLine) {
174
+ return persistedLine;
175
+ }
176
+ return text;
177
+ }
178
+ function resolveToolName(toolCall) {
179
+ const meta = asRecordOrNull(toolCall?.meta);
180
+ const claudeCode = asRecordOrNull(meta?.claudeCode);
181
+ const toolName = claudeCode?.toolName;
182
+ return typeof toolName === "string" ? toolName : "";
183
+ }
184
+ function writeDiffFromInput(rawInput) {
185
+ const input = asRecordOrNull(rawInput);
186
+ if (!input) {
187
+ return [];
188
+ }
189
+ const filePath = typeof input.file_path === "string" ? input.file_path : "";
190
+ const content = typeof input.content === "string" ? input.content : "";
191
+ if (!filePath || !content) {
192
+ return [];
193
+ }
194
+ return [{ type: "diff", old_path: filePath, new_path: filePath, old: "", new: content }];
195
+ }
196
+ function editDiffFromInput(rawInput) {
197
+ const input = asRecordOrNull(rawInput);
198
+ if (!input) {
199
+ return [];
200
+ }
201
+ const filePath = typeof input.file_path === "string" ? input.file_path : "";
202
+ const oldText = typeof input.old_string === "string"
203
+ ? input.old_string
204
+ : typeof input.oldString === "string"
205
+ ? input.oldString
206
+ : "";
207
+ const newText = typeof input.new_string === "string"
208
+ ? input.new_string
209
+ : typeof input.newString === "string"
210
+ ? input.newString
211
+ : "";
212
+ if (!filePath || (!oldText && !newText)) {
213
+ return [];
214
+ }
215
+ return [{ type: "diff", old_path: filePath, new_path: filePath, old: oldText, new: newText }];
216
+ }
217
+ function writeDiffFromResult(rawContent) {
218
+ const candidates = Array.isArray(rawContent) ? rawContent : [rawContent];
219
+ for (const candidate of candidates) {
220
+ const record = asRecordOrNull(candidate);
221
+ if (!record) {
222
+ continue;
223
+ }
224
+ const filePath = typeof record.filePath === "string"
225
+ ? record.filePath
226
+ : typeof record.file_path === "string"
227
+ ? record.file_path
228
+ : "";
229
+ const content = typeof record.content === "string" ? record.content : "";
230
+ const originalRaw = "originalFile" in record ? record.originalFile : "original_file" in record ? record.original_file : undefined;
231
+ if (!filePath || !content || originalRaw === undefined) {
232
+ continue;
233
+ }
234
+ const original = typeof originalRaw === "string" ? originalRaw : originalRaw === null ? "" : "";
235
+ return [{ type: "diff", old_path: filePath, new_path: filePath, old: original, new: content }];
236
+ }
237
+ return [];
238
+ }
239
+ export function buildToolResultFields(isError, rawContent, base) {
240
+ const rawOutput = normalizeToolResultText(rawContent);
241
+ const toolName = resolveToolName(base);
242
+ const fields = {
243
+ status: isError ? "failed" : "completed",
244
+ raw_output: rawOutput || JSON.stringify(rawContent),
245
+ };
246
+ if (!isError && toolName === "Write") {
247
+ const structuredDiff = writeDiffFromResult(rawContent);
248
+ if (structuredDiff.length > 0) {
249
+ fields.content = structuredDiff;
250
+ return fields;
251
+ }
252
+ const inputDiff = writeDiffFromInput(base?.raw_input);
253
+ if (inputDiff.length > 0) {
254
+ fields.content = inputDiff;
255
+ return fields;
256
+ }
257
+ }
258
+ if (!isError && toolName === "Edit") {
259
+ const inputDiff = editDiffFromInput(base?.raw_input);
260
+ if (inputDiff.length > 0) {
261
+ fields.content = inputDiff;
262
+ return fields;
263
+ }
264
+ if (base?.content.some((entry) => entry.type === "diff")) {
265
+ return fields;
266
+ }
267
+ }
268
+ if (rawOutput) {
269
+ fields.content = [{ type: "content", content: { type: "text", text: rawOutput } }];
270
+ }
271
+ return fields;
272
+ }
273
+ export function unwrapToolUseResult(rawResult) {
274
+ if (!rawResult || typeof rawResult !== "object") {
275
+ return { isError: false, content: rawResult };
276
+ }
277
+ const record = rawResult;
278
+ const isError = (typeof record.is_error === "boolean" && record.is_error) ||
279
+ (typeof record.error === "boolean" && record.error);
280
+ if ("content" in record) {
281
+ return { isError: Boolean(isError), content: record.content };
282
+ }
283
+ if ("result" in record) {
284
+ return { isError: Boolean(isError), content: record.result };
285
+ }
286
+ if ("text" in record) {
287
+ return { isError: Boolean(isError), content: record.text };
288
+ }
289
+ return { isError: Boolean(isError), content: rawResult };
290
+ }
@@ -0,0 +1,95 @@
1
+ import { asRecordOrNull } from "./shared.js";
2
+ function numberField(record, ...keys) {
3
+ for (const key of keys) {
4
+ const value = record[key];
5
+ if (typeof value === "number" && Number.isFinite(value)) {
6
+ return value;
7
+ }
8
+ }
9
+ return undefined;
10
+ }
11
+ function selectModelUsageRecord(session, message) {
12
+ const modelUsageRaw = asRecordOrNull(message.modelUsage);
13
+ if (!modelUsageRaw) {
14
+ return null;
15
+ }
16
+ const sortedKeys = Object.keys(modelUsageRaw).sort();
17
+ if (sortedKeys.length === 0) {
18
+ return null;
19
+ }
20
+ const preferredKeys = new Set();
21
+ if (session?.model) {
22
+ preferredKeys.add(session.model);
23
+ }
24
+ if (typeof message.model === "string") {
25
+ preferredKeys.add(message.model);
26
+ }
27
+ for (const key of preferredKeys) {
28
+ const value = asRecordOrNull(modelUsageRaw[key]);
29
+ if (value) {
30
+ return value;
31
+ }
32
+ }
33
+ for (const key of sortedKeys) {
34
+ const value = asRecordOrNull(modelUsageRaw[key]);
35
+ if (value) {
36
+ return value;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ export function buildUsageUpdateFromResultForSession(session, message) {
42
+ const usage = asRecordOrNull(message.usage);
43
+ const inputTokens = usage ? numberField(usage, "inputTokens", "input_tokens") : undefined;
44
+ const outputTokens = usage ? numberField(usage, "outputTokens", "output_tokens") : undefined;
45
+ const cacheReadTokens = usage
46
+ ? numberField(usage, "cacheReadInputTokens", "cache_read_input_tokens", "cache_read_tokens")
47
+ : undefined;
48
+ const cacheWriteTokens = usage
49
+ ? numberField(usage, "cacheCreationInputTokens", "cache_creation_input_tokens", "cache_write_tokens")
50
+ : undefined;
51
+ const totalCostUsd = numberField(message, "total_cost_usd", "totalCostUsd");
52
+ let turnCostUsd;
53
+ if (totalCostUsd !== undefined && session) {
54
+ if (session.lastTotalCostUsd === undefined) {
55
+ turnCostUsd = totalCostUsd;
56
+ }
57
+ else {
58
+ turnCostUsd = Math.max(0, totalCostUsd - session.lastTotalCostUsd);
59
+ }
60
+ session.lastTotalCostUsd = totalCostUsd;
61
+ }
62
+ const modelUsage = selectModelUsageRecord(session, message);
63
+ const contextWindow = modelUsage
64
+ ? numberField(modelUsage, "contextWindow", "context_window")
65
+ : undefined;
66
+ const maxOutputTokens = modelUsage
67
+ ? numberField(modelUsage, "maxOutputTokens", "max_output_tokens")
68
+ : undefined;
69
+ if (inputTokens === undefined &&
70
+ outputTokens === undefined &&
71
+ cacheReadTokens === undefined &&
72
+ cacheWriteTokens === undefined &&
73
+ totalCostUsd === undefined &&
74
+ turnCostUsd === undefined &&
75
+ contextWindow === undefined &&
76
+ maxOutputTokens === undefined) {
77
+ return null;
78
+ }
79
+ return {
80
+ type: "usage_update",
81
+ usage: {
82
+ ...(inputTokens !== undefined ? { input_tokens: inputTokens } : {}),
83
+ ...(outputTokens !== undefined ? { output_tokens: outputTokens } : {}),
84
+ ...(cacheReadTokens !== undefined ? { cache_read_tokens: cacheReadTokens } : {}),
85
+ ...(cacheWriteTokens !== undefined ? { cache_write_tokens: cacheWriteTokens } : {}),
86
+ ...(totalCostUsd !== undefined ? { total_cost_usd: totalCostUsd } : {}),
87
+ ...(turnCostUsd !== undefined ? { turn_cost_usd: turnCostUsd } : {}),
88
+ ...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
89
+ ...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
90
+ },
91
+ };
92
+ }
93
+ export function buildUsageUpdateFromResult(message) {
94
+ return buildUsageUpdateFromResultForSession(undefined, message);
95
+ }