claude-code-rust 0.5.1 → 0.7.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.
@@ -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
- export function normalizeToolResultText(value) {
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
- if (persistedLine) {
179
- return persistedLine;
199
+ const normalized = persistedLine || text;
200
+ if (!isError) {
201
+ return normalized;
180
202
  }
181
- return text;
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",
@@ -0,0 +1,175 @@
1
+ import { asRecordOrNull } from "./shared.js";
2
+ import { writeEvent, emitSessionUpdate } from "./events.js";
3
+ import { setToolCallStatus } from "./tool_calls.js";
4
+ export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
5
+ export const QUESTION_CHOICE_KIND = "question_choice";
6
+ export const EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode";
7
+ export const PLAN_APPROVE_KIND = "plan_approve";
8
+ export const PLAN_REJECT_KIND = "plan_reject";
9
+ export async function requestExitPlanModeApproval(session, toolUseId, inputData, baseToolCall) {
10
+ const options = [
11
+ {
12
+ option_id: "approve",
13
+ name: "Approve",
14
+ description: "Approve the plan and continue",
15
+ kind: PLAN_APPROVE_KIND,
16
+ },
17
+ {
18
+ option_id: "reject",
19
+ name: "Reject",
20
+ description: "Reject the plan",
21
+ kind: PLAN_REJECT_KIND,
22
+ },
23
+ ];
24
+ const request = {
25
+ tool_call: baseToolCall,
26
+ options,
27
+ };
28
+ const outcome = await new Promise((resolve) => {
29
+ session.pendingPermissions.set(toolUseId, {
30
+ onOutcome: resolve,
31
+ toolName: EXIT_PLAN_MODE_TOOL_NAME,
32
+ inputData,
33
+ });
34
+ writeEvent({ event: "permission_request", session_id: session.sessionId, request });
35
+ });
36
+ if (outcome.outcome !== "selected" || outcome.option_id === "reject") {
37
+ setToolCallStatus(session, toolUseId, "failed", "Plan rejected");
38
+ return { behavior: "deny", message: "Plan rejected", toolUseID: toolUseId };
39
+ }
40
+ return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
41
+ }
42
+ export function parseAskUserQuestionPrompts(inputData) {
43
+ const rawQuestions = Array.isArray(inputData.questions) ? inputData.questions : [];
44
+ const prompts = [];
45
+ for (const rawQuestion of rawQuestions) {
46
+ const questionRecord = asRecordOrNull(rawQuestion);
47
+ if (!questionRecord) {
48
+ continue;
49
+ }
50
+ const question = typeof questionRecord.question === "string" ? questionRecord.question.trim() : "";
51
+ if (!question) {
52
+ continue;
53
+ }
54
+ const headerRaw = typeof questionRecord.header === "string" ? questionRecord.header.trim() : "";
55
+ const header = headerRaw || `Q${prompts.length + 1}`;
56
+ const multiSelect = Boolean(questionRecord.multiSelect);
57
+ const rawOptions = Array.isArray(questionRecord.options) ? questionRecord.options : [];
58
+ const options = [];
59
+ for (const rawOption of rawOptions) {
60
+ const optionRecord = asRecordOrNull(rawOption);
61
+ if (!optionRecord) {
62
+ continue;
63
+ }
64
+ const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
65
+ const description = typeof optionRecord.description === "string" ? optionRecord.description.trim() : "";
66
+ if (!label) {
67
+ continue;
68
+ }
69
+ options.push({ label, description });
70
+ }
71
+ if (options.length < 2) {
72
+ continue;
73
+ }
74
+ prompts.push({ question, header, multiSelect, options });
75
+ }
76
+ return prompts;
77
+ }
78
+ function askUserQuestionOptions(prompt) {
79
+ return prompt.options.map((option, index) => ({
80
+ option_id: `question_${index}`,
81
+ name: option.label,
82
+ description: option.description,
83
+ kind: QUESTION_CHOICE_KIND,
84
+ }));
85
+ }
86
+ function askUserQuestionPromptToolCall(base, prompt, index, total) {
87
+ return {
88
+ ...base,
89
+ title: prompt.question,
90
+ raw_input: {
91
+ questions: [
92
+ {
93
+ question: prompt.question,
94
+ header: prompt.header,
95
+ multiSelect: prompt.multiSelect,
96
+ options: prompt.options,
97
+ },
98
+ ],
99
+ question_index: index,
100
+ total_questions: total,
101
+ },
102
+ };
103
+ }
104
+ function askUserQuestionTranscript(answers) {
105
+ return answers.map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`).join("\n");
106
+ }
107
+ export async function requestAskUserQuestionAnswers(session, toolUseId, toolName, inputData, baseToolCall) {
108
+ const prompts = parseAskUserQuestionPrompts(inputData);
109
+ if (prompts.length === 0) {
110
+ return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
111
+ }
112
+ const answers = {};
113
+ const transcript = [];
114
+ for (const [index, prompt] of prompts.entries()) {
115
+ const promptToolCall = askUserQuestionPromptToolCall(baseToolCall, prompt, index, prompts.length);
116
+ const fields = {
117
+ title: promptToolCall.title,
118
+ status: "in_progress",
119
+ raw_input: promptToolCall.raw_input,
120
+ };
121
+ emitSessionUpdate(session.sessionId, {
122
+ type: "tool_call_update",
123
+ tool_call_update: { tool_call_id: toolUseId, fields },
124
+ });
125
+ const tracked = session.toolCalls.get(toolUseId);
126
+ if (tracked) {
127
+ tracked.title = promptToolCall.title;
128
+ tracked.status = "in_progress";
129
+ tracked.raw_input = promptToolCall.raw_input;
130
+ }
131
+ const request = {
132
+ tool_call: promptToolCall,
133
+ options: askUserQuestionOptions(prompt),
134
+ };
135
+ const outcome = await new Promise((resolve) => {
136
+ session.pendingPermissions.set(toolUseId, {
137
+ onOutcome: resolve,
138
+ toolName,
139
+ inputData,
140
+ });
141
+ writeEvent({ event: "permission_request", session_id: session.sessionId, request });
142
+ });
143
+ if (outcome.outcome !== "selected") {
144
+ setToolCallStatus(session, toolUseId, "failed", "Question cancelled");
145
+ return { behavior: "deny", message: "Question cancelled", toolUseID: toolUseId };
146
+ }
147
+ const selected = request.options.find((option) => option.option_id === outcome.option_id);
148
+ if (!selected) {
149
+ setToolCallStatus(session, toolUseId, "failed", "Question answer was invalid");
150
+ return { behavior: "deny", message: "Question answer was invalid", toolUseID: toolUseId };
151
+ }
152
+ answers[prompt.question] = selected.name;
153
+ transcript.push({ header: prompt.header, question: prompt.question, answer: selected.name });
154
+ const summary = askUserQuestionTranscript(transcript);
155
+ const progressFields = {
156
+ status: index + 1 >= prompts.length ? "completed" : "in_progress",
157
+ raw_output: summary,
158
+ content: [{ type: "content", content: { type: "text", text: summary } }],
159
+ };
160
+ emitSessionUpdate(session.sessionId, {
161
+ type: "tool_call_update",
162
+ tool_call_update: { tool_call_id: toolUseId, fields: progressFields },
163
+ });
164
+ if (tracked) {
165
+ tracked.status = progressFields.status ?? tracked.status;
166
+ tracked.raw_output = summary;
167
+ tracked.content = progressFields.content ?? tracked.content;
168
+ }
169
+ }
170
+ return {
171
+ behavior: "allow",
172
+ updatedInput: { ...inputData, answers },
173
+ toolUseID: toolUseId,
174
+ };
175
+ }