claude-code-rust 0.9.0 → 0.10.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.
@@ -1,13 +1,177 @@
1
1
  import { emitSessionUpdate } from "./events.js";
2
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
2
3
  import { buildToolResultFields, createToolCall } from "./tooling.js";
4
+ function jsonSize(value) {
5
+ if (value === undefined) {
6
+ return undefined;
7
+ }
8
+ try {
9
+ return Buffer.byteLength(JSON.stringify(value));
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function toolNameFromMeta(meta) {
16
+ if (!meta || typeof meta !== "object") {
17
+ return undefined;
18
+ }
19
+ const claudeCode = "claudeCode" in meta && meta.claudeCode && typeof meta.claudeCode === "object" ? meta.claudeCode : undefined;
20
+ const toolName = claudeCode && "toolName" in claudeCode && typeof claudeCode.toolName === "string" ? claudeCode.toolName : "";
21
+ return toolName || undefined;
22
+ }
23
+ function toolName(base, fields) {
24
+ const fieldMeta = fields?.meta;
25
+ if (fieldMeta && typeof fieldMeta === "object") {
26
+ const fromFields = toolNameFromMeta(fieldMeta);
27
+ if (fromFields) {
28
+ return fromFields;
29
+ }
30
+ }
31
+ return toolNameFromMeta(base?.meta);
32
+ }
33
+ function classifyFailureKind(rawOutput) {
34
+ if (!rawOutput) {
35
+ return "failed";
36
+ }
37
+ const normalized = rawOutput.trim().toLowerCase();
38
+ if (normalized.includes("permission denied") ||
39
+ normalized.includes("cancelled by user") ||
40
+ normalized.includes("plan rejected") ||
41
+ normalized.includes("question cancelled")) {
42
+ return "refused";
43
+ }
44
+ if (normalized.includes("timed out") || normalized.includes("timeout")) {
45
+ return "timeout";
46
+ }
47
+ return "failed";
48
+ }
49
+ function updateOutcome(status) {
50
+ switch (status) {
51
+ case "completed":
52
+ return "success";
53
+ case "failed":
54
+ return "failure";
55
+ case "in_progress":
56
+ return "partial";
57
+ case "pending":
58
+ return "start";
59
+ default:
60
+ return "partial";
61
+ }
62
+ }
63
+ function applyFieldsToBase(base, fields) {
64
+ if (fields.title !== undefined) {
65
+ base.title = fields.title;
66
+ }
67
+ if (fields.kind !== undefined) {
68
+ base.kind = fields.kind;
69
+ }
70
+ if (fields.status !== undefined) {
71
+ base.status = fields.status;
72
+ }
73
+ if (fields.raw_input !== undefined) {
74
+ base.raw_input = fields.raw_input;
75
+ }
76
+ if (fields.raw_output !== undefined) {
77
+ base.raw_output = fields.raw_output;
78
+ }
79
+ if (fields.locations !== undefined) {
80
+ base.locations = fields.locations;
81
+ }
82
+ if (fields.output_metadata !== undefined) {
83
+ base.output_metadata = fields.output_metadata;
84
+ }
85
+ if (fields.meta !== undefined) {
86
+ base.meta = fields.meta;
87
+ }
88
+ if (fields.content !== undefined) {
89
+ base.content = fields.content;
90
+ }
91
+ }
92
+ function logToolCallSubmitted(sessionId, toolCall, updateKind) {
93
+ bridgeLogger.info({
94
+ target: LOG_TARGETS.APP_TOOL,
95
+ eventName: "tool_call_submitted",
96
+ message: "tool call submitted",
97
+ outcome: "success",
98
+ sessionId,
99
+ toolCallId: toolCall.tool_call_id,
100
+ sizeBytes: jsonSize(toolCall.raw_input),
101
+ fields: {
102
+ submission_kind: updateKind,
103
+ tool_name: toolNameFromMeta(toolCall.meta),
104
+ tool_title: toolCall.title,
105
+ tool_kind: toolCall.kind,
106
+ status: toolCall.status,
107
+ content_block_count: toolCall.content.length,
108
+ location_count: toolCall.locations.length,
109
+ has_output_metadata: toolCall.output_metadata !== undefined,
110
+ },
111
+ });
112
+ }
113
+ function logToolCallUpdateEmitted(sessionId, toolUseId, fields, base, updateKind) {
114
+ const nextStatus = fields.status ?? base?.status;
115
+ const rawOutput = typeof fields.raw_output === "string" ? fields.raw_output : base?.raw_output;
116
+ const failureKind = nextStatus === "failed" ? classifyFailureKind(rawOutput) : undefined;
117
+ const commonEvent = {
118
+ target: LOG_TARGETS.APP_TOOL,
119
+ eventName: "tool_call_update_emitted",
120
+ message: "tool call update emitted",
121
+ outcome: updateOutcome(nextStatus),
122
+ sessionId,
123
+ toolCallId: toolUseId,
124
+ sizeBytes: jsonSize(fields.raw_input),
125
+ fields: {
126
+ update_kind: updateKind,
127
+ tool_name: toolName(base, fields),
128
+ previous_status: base?.status,
129
+ next_status: nextStatus,
130
+ title_changed: fields.title !== undefined && fields.title !== base?.title,
131
+ content_block_count: fields.content?.length,
132
+ location_count: fields.locations?.length,
133
+ raw_output_chars: rawOutput?.length,
134
+ has_output_metadata: fields.output_metadata !== undefined || base?.output_metadata !== undefined,
135
+ failure_kind: failureKind,
136
+ },
137
+ };
138
+ if (nextStatus === "failed") {
139
+ bridgeLogger.warn(commonEvent);
140
+ return;
141
+ }
142
+ if (nextStatus === "completed") {
143
+ bridgeLogger.info(commonEvent);
144
+ return;
145
+ }
146
+ if (nextStatus === "in_progress" || nextStatus === "pending") {
147
+ bridgeLogger.debug(commonEvent);
148
+ return;
149
+ }
150
+ bridgeLogger.debug(commonEvent);
151
+ }
152
+ function emitInitialToolCall(session, toolCall, updateKind = "initial") {
153
+ session.toolCalls.set(toolCall.tool_call_id, toolCall);
154
+ logToolCallSubmitted(session.sessionId, toolCall, updateKind);
155
+ emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
156
+ }
157
+ export function emitToolCallUpdate(session, toolUseId, fields, updateKind) {
158
+ const base = session.toolCalls.get(toolUseId);
159
+ logToolCallUpdateEmitted(session.sessionId, toolUseId, fields, base, updateKind);
160
+ emitSessionUpdate(session.sessionId, {
161
+ type: "tool_call_update",
162
+ tool_call_update: { tool_call_id: toolUseId, fields },
163
+ });
164
+ if (base) {
165
+ applyFieldsToBase(base, fields);
166
+ }
167
+ }
3
168
  export function emitToolCall(session, toolUseId, name, input) {
4
169
  const toolCall = createToolCall(toolUseId, name, input);
5
170
  const status = "in_progress";
6
171
  toolCall.status = status;
7
172
  const existing = session.toolCalls.get(toolUseId);
8
173
  if (!existing) {
9
- session.toolCalls.set(toolUseId, toolCall);
10
- emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
174
+ emitInitialToolCall(session, toolCall);
11
175
  return;
12
176
  }
13
177
  const fields = {
@@ -21,19 +185,7 @@ export function emitToolCall(session, toolUseId, name, input) {
21
185
  if (toolCall.content.length > 0) {
22
186
  fields.content = toolCall.content;
23
187
  }
24
- emitSessionUpdate(session.sessionId, {
25
- type: "tool_call_update",
26
- tool_call_update: { tool_call_id: toolUseId, fields },
27
- });
28
- existing.title = toolCall.title;
29
- existing.kind = toolCall.kind;
30
- existing.status = status;
31
- existing.raw_input = toolCall.raw_input;
32
- existing.locations = toolCall.locations;
33
- existing.meta = toolCall.meta;
34
- if (toolCall.content.length > 0) {
35
- existing.content = toolCall.content;
36
- }
188
+ emitToolCallUpdate(session, toolUseId, fields, "refresh");
37
189
  }
38
190
  export function ensureToolCallVisible(session, toolUseId, toolName, input) {
39
191
  const existing = session.toolCalls.get(toolUseId);
@@ -41,8 +193,7 @@ export function ensureToolCallVisible(session, toolUseId, toolName, input) {
41
193
  return existing;
42
194
  }
43
195
  const toolCall = createToolCall(toolUseId, toolName, input);
44
- session.toolCalls.set(toolUseId, toolCall);
45
- emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
196
+ emitInitialToolCall(session, toolCall);
46
197
  return toolCall;
47
198
  }
48
199
  export function emitPlanIfTodoWrite(session, name, input) {
@@ -68,34 +219,15 @@ export function emitPlanIfTodoWrite(session, name, input) {
68
219
  }
69
220
  }
70
221
  export function emitToolResultUpdate(session, toolUseId, isError, rawContent, rawResult = rawContent) {
71
- const base = session.toolCalls.get(toolUseId);
72
- const fields = buildToolResultFields(isError, rawContent, base, rawResult);
73
- const update = { tool_call_id: toolUseId, fields };
74
- emitSessionUpdate(session.sessionId, { type: "tool_call_update", tool_call_update: update });
75
- if (base) {
76
- base.status = fields.status ?? base.status;
77
- if (fields.raw_output) {
78
- base.raw_output = fields.raw_output;
79
- }
80
- if (fields.content) {
81
- base.content = fields.content;
82
- }
83
- if (fields.output_metadata) {
84
- base.output_metadata = fields.output_metadata;
85
- }
86
- }
222
+ const fields = buildToolResultFields(isError, rawContent, session.toolCalls.get(toolUseId), rawResult);
223
+ emitToolCallUpdate(session, toolUseId, fields, "result");
87
224
  }
88
225
  export function finalizeOpenToolCalls(session, status) {
89
226
  for (const [toolUseId, toolCall] of session.toolCalls) {
90
227
  if (toolCall.status !== "pending" && toolCall.status !== "in_progress") {
91
228
  continue;
92
229
  }
93
- const fields = { status };
94
- emitSessionUpdate(session.sessionId, {
95
- type: "tool_call_update",
96
- tool_call_update: { tool_call_id: toolUseId, fields },
97
- });
98
- toolCall.status = status;
230
+ emitToolCallUpdate(session, toolUseId, { status }, "finalize");
99
231
  }
100
232
  }
101
233
  export function emitToolProgressUpdate(session, toolUseId, toolName) {
@@ -109,12 +241,7 @@ export function emitToolProgressUpdate(session, toolUseId, toolName) {
109
241
  existing.status === "failed") {
110
242
  return;
111
243
  }
112
- const fields = { status: "in_progress" };
113
- emitSessionUpdate(session.sessionId, {
114
- type: "tool_call_update",
115
- tool_call_update: { tool_call_id: toolUseId, fields },
116
- });
117
- existing.status = "in_progress";
244
+ emitToolCallUpdate(session, toolUseId, { status: "in_progress" }, "progress");
118
245
  }
119
246
  export function emitToolSummaryUpdate(session, toolUseId, summary) {
120
247
  const base = session.toolCalls.get(toolUseId);
@@ -126,12 +253,7 @@ export function emitToolSummaryUpdate(session, toolUseId, summary) {
126
253
  raw_output: summary,
127
254
  content: [{ type: "content", content: { type: "text", text: summary } }],
128
255
  };
129
- emitSessionUpdate(session.sessionId, {
130
- type: "tool_call_update",
131
- tool_call_update: { tool_call_id: toolUseId, fields },
132
- });
133
- base.status = fields.status ?? base.status;
134
- base.raw_output = summary;
256
+ emitToolCallUpdate(session, toolUseId, fields, "summary");
135
257
  }
136
258
  export function setToolCallStatus(session, toolUseId, status, message) {
137
259
  const base = session.toolCalls.get(toolUseId);
@@ -143,14 +265,7 @@ export function setToolCallStatus(session, toolUseId, status, message) {
143
265
  fields.raw_output = message;
144
266
  fields.content = [{ type: "content", content: { type: "text", text: message } }];
145
267
  }
146
- emitSessionUpdate(session.sessionId, {
147
- type: "tool_call_update",
148
- tool_call_update: { tool_call_id: toolUseId, fields },
149
- });
150
- base.status = status;
151
- if (fields.raw_output) {
152
- base.raw_output = fields.raw_output;
153
- }
268
+ emitToolCallUpdate(session, toolUseId, fields, "status");
154
269
  }
155
270
  export function resolveTaskToolUseId(session, msg) {
156
271
  const direct = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
@@ -1,6 +1,7 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
- import { writeEvent, emitSessionUpdate } from "./events.js";
3
- import { setToolCallStatus } from "./tool_calls.js";
2
+ import { emitPermissionRequestEvent, emitQuestionRequestEvent, } from "./events.js";
3
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
4
+ import { emitToolCallUpdate, setToolCallStatus } from "./tool_calls.js";
4
5
  export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
5
6
  export const QUESTION_CHOICE_KIND = "question_choice";
6
7
  export const EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode";
@@ -31,7 +32,20 @@ export async function requestExitPlanModeApproval(session, toolUseId, inputData,
31
32
  toolName: EXIT_PLAN_MODE_TOOL_NAME,
32
33
  inputData,
33
34
  });
34
- writeEvent({ event: "permission_request", session_id: session.sessionId, request });
35
+ bridgeLogger.info({
36
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
37
+ eventName: "permission_request_created",
38
+ message: "exit plan mode permission request created",
39
+ outcome: "start",
40
+ sessionId: session.sessionId,
41
+ toolCallId: toolUseId,
42
+ count: request.options.length,
43
+ fields: {
44
+ tool_name: EXIT_PLAN_MODE_TOOL_NAME,
45
+ option_count: request.options.length,
46
+ },
47
+ });
48
+ emitPermissionRequestEvent(session.sessionId, request);
35
49
  });
36
50
  if (outcome.outcome !== "selected" || outcome.option_id === "reject") {
37
51
  setToolCallStatus(session, toolUseId, "failed", "Plan rejected");
@@ -159,16 +173,7 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
159
173
  status: "in_progress",
160
174
  raw_input: promptToolCall.raw_input,
161
175
  };
162
- emitSessionUpdate(session.sessionId, {
163
- type: "tool_call_update",
164
- tool_call_update: { tool_call_id: toolUseId, fields },
165
- });
166
- const tracked = session.toolCalls.get(toolUseId);
167
- if (tracked) {
168
- tracked.title = promptToolCall.title;
169
- tracked.status = "in_progress";
170
- tracked.raw_input = promptToolCall.raw_input;
171
- }
176
+ emitToolCallUpdate(session, toolUseId, fields, "status");
172
177
  const request = buildQuestionRequest(promptToolCall, prompt, index, prompts.length);
173
178
  const outcome = await new Promise((resolve) => {
174
179
  session.pendingQuestions.set(toolUseId, {
@@ -176,7 +181,21 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
176
181
  toolName: ASK_USER_QUESTION_TOOL_NAME,
177
182
  inputData,
178
183
  });
179
- writeEvent({ event: "question_request", session_id: session.sessionId, request });
184
+ bridgeLogger.info({
185
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
186
+ eventName: "question_request_created",
187
+ message: "ask user question request created",
188
+ outcome: "start",
189
+ sessionId: session.sessionId,
190
+ toolCallId: toolUseId,
191
+ count: request.prompt.options.length,
192
+ fields: {
193
+ tool_name: ASK_USER_QUESTION_TOOL_NAME,
194
+ question_index: index,
195
+ total_questions: prompts.length,
196
+ },
197
+ });
198
+ emitQuestionRequestEvent(session.sessionId, request);
180
199
  });
181
200
  if (outcome.outcome !== "answered") {
182
201
  setToolCallStatus(session, toolUseId, "failed", "Question cancelled");
@@ -201,15 +220,7 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
201
220
  raw_output: summary,
202
221
  content: [{ type: "content", content: { type: "text", text: summary } }],
203
222
  };
204
- emitSessionUpdate(session.sessionId, {
205
- type: "tool_call_update",
206
- tool_call_update: { tool_call_id: toolUseId, fields: progressFields },
207
- });
208
- if (tracked) {
209
- tracked.status = progressFields.status ?? tracked.status;
210
- tracked.raw_output = summary;
211
- tracked.content = progressFields.content ?? tracked.content;
212
- }
223
+ emitToolCallUpdate(session, toolUseId, progressFields, "summary");
213
224
  }
214
225
  return {
215
226
  behavior: "allow",