@ccpocket/bridge 0.1.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 +54 -0
- package/dist/claude-process.d.ts +108 -0
- package/dist/claude-process.js +471 -0
- package/dist/claude-process.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-process.d.ts +46 -0
- package/dist/codex-process.js +420 -0
- package/dist/codex-process.js.map +1 -0
- package/dist/debug-trace-store.d.ts +15 -0
- package/dist/debug-trace-store.js +78 -0
- package/dist/debug-trace-store.js.map +1 -0
- package/dist/firebase-auth.d.ts +35 -0
- package/dist/firebase-auth.js +132 -0
- package/dist/firebase-auth.js.map +1 -0
- package/dist/gallery-store.d.ts +66 -0
- package/dist/gallery-store.js +310 -0
- package/dist/gallery-store.js.map +1 -0
- package/dist/image-store.d.ts +22 -0
- package/dist/image-store.js +113 -0
- package/dist/image-store.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +6 -0
- package/dist/mdns.js +42 -0
- package/dist/mdns.js.map +1 -0
- package/dist/parser.d.ts +381 -0
- package/dist/parser.js +218 -0
- package/dist/parser.js.map +1 -0
- package/dist/project-history.d.ts +10 -0
- package/dist/project-history.js +73 -0
- package/dist/project-history.js.map +1 -0
- package/dist/prompt-history-backup.d.ts +15 -0
- package/dist/prompt-history-backup.js +46 -0
- package/dist/prompt-history-backup.js.map +1 -0
- package/dist/push-relay.d.ts +27 -0
- package/dist/push-relay.js +69 -0
- package/dist/push-relay.js.map +1 -0
- package/dist/recording-store.d.ts +51 -0
- package/dist/recording-store.js +158 -0
- package/dist/recording-store.js.map +1 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +98 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/sdk-process.d.ts +151 -0
- package/dist/sdk-process.js +740 -0
- package/dist/sdk-process.js.map +1 -0
- package/dist/session.d.ts +126 -0
- package/dist/session.js +550 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions-index.d.ts +86 -0
- package/dist/sessions-index.js +1027 -0
- package/dist/sessions-index.js.map +1 -0
- package/dist/setup-launchd.d.ts +8 -0
- package/dist/setup-launchd.js +109 -0
- package/dist/setup-launchd.js.map +1 -0
- package/dist/startup-info.d.ts +8 -0
- package/dist/startup-info.js +78 -0
- package/dist/startup-info.js.map +1 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +236 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.js +39 -0
- package/dist/version.js.map +1 -0
- package/dist/websocket.d.ts +71 -0
- package/dist/websocket.js +1487 -0
- package/dist/websocket.js.map +1 -0
- package/dist/worktree-store.d.ts +25 -0
- package/dist/worktree-store.js +59 -0
- package/dist/worktree-store.js.map +1 -0
- package/dist/worktree.d.ts +43 -0
- package/dist/worktree.js +295 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { normalizeToolResultContent, } from "./parser.js";
|
|
5
|
+
// Tools that are auto-approved in acceptEdits mode
|
|
6
|
+
export const ACCEPT_EDITS_AUTO_APPROVE = new Set([
|
|
7
|
+
"Read", "Glob", "Grep",
|
|
8
|
+
"Edit", "Write", "NotebookEdit",
|
|
9
|
+
"TaskCreate", "TaskUpdate", "TaskList", "TaskGet",
|
|
10
|
+
"EnterPlanMode", "AskUserQuestion",
|
|
11
|
+
"WebSearch", "WebFetch",
|
|
12
|
+
"Task", "Skill",
|
|
13
|
+
]);
|
|
14
|
+
const FILE_EDIT_TOOLS = new Set([
|
|
15
|
+
"Edit",
|
|
16
|
+
"Write",
|
|
17
|
+
"MultiEdit",
|
|
18
|
+
"NotebookEdit",
|
|
19
|
+
]);
|
|
20
|
+
function toFiniteNumber(value) {
|
|
21
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
22
|
+
return undefined;
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function isFileEditToolName(toolName) {
|
|
26
|
+
return FILE_EDIT_TOOLS.has(toolName);
|
|
27
|
+
}
|
|
28
|
+
export function extractTokenUsage(usage) {
|
|
29
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
const obj = usage;
|
|
33
|
+
const inputTokens = toFiniteNumber(obj.input_tokens)
|
|
34
|
+
?? toFiniteNumber(obj.inputTokens);
|
|
35
|
+
const outputTokens = toFiniteNumber(obj.output_tokens)
|
|
36
|
+
?? toFiniteNumber(obj.outputTokens);
|
|
37
|
+
const cachedReadTokens = toFiniteNumber(obj.cached_input_tokens)
|
|
38
|
+
?? toFiniteNumber(obj.cache_read_input_tokens)
|
|
39
|
+
?? toFiniteNumber(obj.cachedInputTokens)
|
|
40
|
+
?? toFiniteNumber(obj.cacheReadInputTokens);
|
|
41
|
+
return {
|
|
42
|
+
...(inputTokens != null ? { inputTokens } : {}),
|
|
43
|
+
...(cachedReadTokens != null ? { cachedInputTokens: cachedReadTokens } : {}),
|
|
44
|
+
...(outputTokens != null ? { outputTokens } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse a permission rule in ToolName(ruleContent) format.
|
|
49
|
+
* Matches the CLI's internal pzT() function: /^([^(]+)\(([^)]+)\)$/
|
|
50
|
+
*/
|
|
51
|
+
export function parseRule(rule) {
|
|
52
|
+
const match = rule.match(/^([^(]+)\(([^)]+)\)$/);
|
|
53
|
+
if (!match || !match[1] || !match[2])
|
|
54
|
+
return { toolName: rule };
|
|
55
|
+
return { toolName: match[1], ruleContent: match[2] };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a tool invocation matches any session allow rule.
|
|
59
|
+
*/
|
|
60
|
+
export function matchesSessionRule(toolName, input, rules) {
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
const parsed = parseRule(rule);
|
|
63
|
+
if (parsed.toolName !== toolName)
|
|
64
|
+
continue;
|
|
65
|
+
// No ruleContent -> matches any invocation of this tool
|
|
66
|
+
if (!parsed.ruleContent)
|
|
67
|
+
return true;
|
|
68
|
+
// Bash: prefix matching with ":*" suffix
|
|
69
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
70
|
+
if (parsed.ruleContent.endsWith(":*")) {
|
|
71
|
+
const prefix = parsed.ruleContent.slice(0, -2);
|
|
72
|
+
const firstWord = input.command.trim().split(/\s+/)[0] ?? "";
|
|
73
|
+
if (firstWord === prefix)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (input.command === parsed.ruleContent)
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a session allow rule string from a tool name and input.
|
|
86
|
+
* Bash: uses first word as prefix (e.g., "Bash(npm:*)")
|
|
87
|
+
* Others: tool name only (e.g., "Edit")
|
|
88
|
+
*/
|
|
89
|
+
export function buildSessionRule(toolName, input) {
|
|
90
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
91
|
+
const firstWord = input.command.trim().split(/\s+/)[0] ?? "";
|
|
92
|
+
if (firstWord)
|
|
93
|
+
return `${toolName}(${firstWord}:*)`;
|
|
94
|
+
}
|
|
95
|
+
return toolName;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Convert SDK messages to the ServerMessage format used by the WebSocket protocol.
|
|
99
|
+
* Exported for testing.
|
|
100
|
+
*/
|
|
101
|
+
export function sdkMessageToServerMessage(msg) {
|
|
102
|
+
switch (msg.type) {
|
|
103
|
+
case "system": {
|
|
104
|
+
const sys = msg;
|
|
105
|
+
if (sys.subtype === "init") {
|
|
106
|
+
return {
|
|
107
|
+
type: "system",
|
|
108
|
+
subtype: "init",
|
|
109
|
+
sessionId: msg.session_id,
|
|
110
|
+
model: sys.model,
|
|
111
|
+
...(sys.slash_commands ? { slashCommands: sys.slash_commands } : {}),
|
|
112
|
+
...(sys.skills ? { skills: sys.skills } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
case "assistant": {
|
|
118
|
+
const ast = msg;
|
|
119
|
+
return {
|
|
120
|
+
type: "assistant",
|
|
121
|
+
message: ast.message,
|
|
122
|
+
...(ast.uuid ? { messageUuid: ast.uuid } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case "user": {
|
|
126
|
+
const usr = msg;
|
|
127
|
+
// Filter out meta messages early (e.g., skill loading prompts).
|
|
128
|
+
// Following Happy Coder's approach: isMeta messages are not user-facing.
|
|
129
|
+
if (usr.isMeta)
|
|
130
|
+
return null;
|
|
131
|
+
const content = usr.message?.content;
|
|
132
|
+
if (!Array.isArray(content))
|
|
133
|
+
return null;
|
|
134
|
+
const results = content.filter((c) => c.type === "tool_result");
|
|
135
|
+
if (results.length > 0) {
|
|
136
|
+
const first = results[0];
|
|
137
|
+
const rawContent = first.content;
|
|
138
|
+
return {
|
|
139
|
+
type: "tool_result",
|
|
140
|
+
toolUseId: first.tool_use_id,
|
|
141
|
+
content: normalizeToolResultContent(rawContent),
|
|
142
|
+
...(Array.isArray(rawContent) ? { rawContentBlocks: rawContent } : {}),
|
|
143
|
+
...(usr.uuid ? { userMessageUuid: usr.uuid } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// User text input (first prompt of each turn)
|
|
147
|
+
const texts = content
|
|
148
|
+
.filter((c) => c.type === "text")
|
|
149
|
+
.map((c) => c.text);
|
|
150
|
+
if (texts.length > 0) {
|
|
151
|
+
return {
|
|
152
|
+
type: "user_input",
|
|
153
|
+
text: texts.join("\n"),
|
|
154
|
+
...(usr.uuid ? { userMessageUuid: usr.uuid } : {}),
|
|
155
|
+
...(usr.isSynthetic ? { isSynthetic: true } : {}),
|
|
156
|
+
...(usr.isMeta ? { isMeta: true } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
case "result": {
|
|
162
|
+
const res = msg;
|
|
163
|
+
const tokenUsage = extractTokenUsage(res.usage);
|
|
164
|
+
if (res.subtype === "success") {
|
|
165
|
+
return {
|
|
166
|
+
type: "result",
|
|
167
|
+
subtype: "success",
|
|
168
|
+
result: res.result,
|
|
169
|
+
cost: res.total_cost_usd,
|
|
170
|
+
duration: res.duration_ms,
|
|
171
|
+
sessionId: msg.session_id,
|
|
172
|
+
stopReason: res.stop_reason,
|
|
173
|
+
...tokenUsage,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// All other result subtypes are errors
|
|
177
|
+
const errorText = Array.isArray(res.errors) ? res.errors.join("\n") : "Unknown error";
|
|
178
|
+
// Suppress spurious CLI runtime errors (SDK bug: Bun API referenced on Node.js)
|
|
179
|
+
if (errorText.includes("Bun is not defined")) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
type: "result",
|
|
184
|
+
subtype: "error",
|
|
185
|
+
error: errorText,
|
|
186
|
+
sessionId: msg.session_id,
|
|
187
|
+
stopReason: res.stop_reason,
|
|
188
|
+
...tokenUsage,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case "stream_event": {
|
|
192
|
+
const stream = msg;
|
|
193
|
+
const event = stream.event;
|
|
194
|
+
if (event.type === "content_block_delta") {
|
|
195
|
+
const delta = event.delta;
|
|
196
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
197
|
+
return { type: "stream_delta", text: delta.text };
|
|
198
|
+
}
|
|
199
|
+
if (delta.type === "thinking_delta" && delta.thinking) {
|
|
200
|
+
return { type: "thinking_delta", text: delta.thinking };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
case "tool_use_summary": {
|
|
206
|
+
const summary = msg;
|
|
207
|
+
return {
|
|
208
|
+
type: "tool_use_summary",
|
|
209
|
+
summary: summary.summary,
|
|
210
|
+
precedingToolUseIds: summary.preceding_tool_use_ids,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export class SdkProcess extends EventEmitter {
|
|
218
|
+
queryInstance = null;
|
|
219
|
+
_status = "idle";
|
|
220
|
+
_sessionId = null;
|
|
221
|
+
pendingPermissions = new Map();
|
|
222
|
+
_permissionMode;
|
|
223
|
+
get permissionMode() { return this._permissionMode; }
|
|
224
|
+
sessionAllowRules = new Set();
|
|
225
|
+
initTimeoutId = null;
|
|
226
|
+
// User message channel
|
|
227
|
+
userMessageResolve = null;
|
|
228
|
+
stopped = false;
|
|
229
|
+
pendingInput = null;
|
|
230
|
+
_projectPath = null;
|
|
231
|
+
toolCallsSinceLastResult = 0;
|
|
232
|
+
fileEditsSinceLastResult = 0;
|
|
233
|
+
get status() {
|
|
234
|
+
return this._status;
|
|
235
|
+
}
|
|
236
|
+
get isWaitingForInput() {
|
|
237
|
+
return this.userMessageResolve !== null;
|
|
238
|
+
}
|
|
239
|
+
get sessionId() {
|
|
240
|
+
return this._sessionId;
|
|
241
|
+
}
|
|
242
|
+
get isRunning() {
|
|
243
|
+
return this.queryInstance !== null;
|
|
244
|
+
}
|
|
245
|
+
start(projectPath, options) {
|
|
246
|
+
if (this.queryInstance) {
|
|
247
|
+
this.stop();
|
|
248
|
+
}
|
|
249
|
+
this._projectPath = projectPath;
|
|
250
|
+
if (!existsSync(projectPath)) {
|
|
251
|
+
mkdirSync(projectPath, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
this.stopped = false;
|
|
254
|
+
this._sessionId = null;
|
|
255
|
+
this.pendingPermissions.clear();
|
|
256
|
+
this._permissionMode = options?.permissionMode;
|
|
257
|
+
this.sessionAllowRules.clear();
|
|
258
|
+
this.toolCallsSinceLastResult = 0;
|
|
259
|
+
this.fileEditsSinceLastResult = 0;
|
|
260
|
+
if (options?.initialInput) {
|
|
261
|
+
this.pendingInput = { text: options.initialInput };
|
|
262
|
+
}
|
|
263
|
+
this.setStatus("starting");
|
|
264
|
+
console.log(`[sdk-process] Starting SDK query (cwd: ${projectPath}, mode: ${options?.permissionMode ?? "default"})`);
|
|
265
|
+
// In -p mode with --input-format stream-json, Claude CLI won't emit
|
|
266
|
+
// system/init until the first user input. Set a fallback timeout to
|
|
267
|
+
// transition to "idle" if init hasn't arrived, since the process IS
|
|
268
|
+
// ready to accept input at that point.
|
|
269
|
+
if (this.initTimeoutId)
|
|
270
|
+
clearTimeout(this.initTimeoutId);
|
|
271
|
+
this.initTimeoutId = setTimeout(() => {
|
|
272
|
+
if (this._status === "starting") {
|
|
273
|
+
console.log("[sdk-process] Init timeout: setting status to idle (process ready for input)");
|
|
274
|
+
this.setStatus("idle");
|
|
275
|
+
}
|
|
276
|
+
this.initTimeoutId = null;
|
|
277
|
+
}, 3000);
|
|
278
|
+
this.queryInstance = query({
|
|
279
|
+
prompt: this.createUserMessageStream(),
|
|
280
|
+
options: {
|
|
281
|
+
cwd: projectPath,
|
|
282
|
+
resume: options?.sessionId,
|
|
283
|
+
continue: options?.continueMode,
|
|
284
|
+
permissionMode: options?.permissionMode ?? "default",
|
|
285
|
+
...(options?.model ? { model: options.model } : {}),
|
|
286
|
+
...(options?.effort ? { effort: options.effort } : {}),
|
|
287
|
+
...(options?.maxTurns != null ? { maxTurns: options.maxTurns } : {}),
|
|
288
|
+
...(options?.maxBudgetUsd != null ? { maxBudgetUsd: options.maxBudgetUsd } : {}),
|
|
289
|
+
...(options?.fallbackModel ? { fallbackModel: options.fallbackModel } : {}),
|
|
290
|
+
...(options?.forkSession != null ? { forkSession: options.forkSession } : {}),
|
|
291
|
+
...(options?.persistSession != null ? { persistSession: options.persistSession } : {}),
|
|
292
|
+
hooks: {
|
|
293
|
+
PostToolUse: [{
|
|
294
|
+
hooks: [async (input) => {
|
|
295
|
+
this.handlePostToolUseHook(input);
|
|
296
|
+
return { continue: true };
|
|
297
|
+
}],
|
|
298
|
+
}],
|
|
299
|
+
},
|
|
300
|
+
includePartialMessages: true,
|
|
301
|
+
canUseTool: this.handleCanUseTool.bind(this),
|
|
302
|
+
settingSources: ["user", "project", "local"],
|
|
303
|
+
enableFileCheckpointing: true,
|
|
304
|
+
...(options?.resumeSessionAt ? { resumeSessionAt: options.resumeSessionAt } : {}),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// Background message processing
|
|
308
|
+
this.processMessages().catch((err) => {
|
|
309
|
+
if (this.stopped) {
|
|
310
|
+
// Suppress errors from intentional stop (SDK bug: Bun API referenced on Node.js)
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
console.error("[sdk-process] Message processing error:", err);
|
|
314
|
+
this.emitMessage({ type: "error", message: `SDK error: ${err instanceof Error ? err.message : String(err)}` });
|
|
315
|
+
this.setStatus("idle");
|
|
316
|
+
this.emit("exit", 1);
|
|
317
|
+
});
|
|
318
|
+
// Proactively fetch supported commands via SDK API (non-blocking)
|
|
319
|
+
this.fetchSupportedCommands();
|
|
320
|
+
}
|
|
321
|
+
stop() {
|
|
322
|
+
if (this.initTimeoutId) {
|
|
323
|
+
clearTimeout(this.initTimeoutId);
|
|
324
|
+
this.initTimeoutId = null;
|
|
325
|
+
}
|
|
326
|
+
this.stopped = true;
|
|
327
|
+
this.pendingInput = null;
|
|
328
|
+
if (this.queryInstance) {
|
|
329
|
+
console.log("[sdk-process] Stopping query");
|
|
330
|
+
this.queryInstance.close();
|
|
331
|
+
this.queryInstance = null;
|
|
332
|
+
}
|
|
333
|
+
this.pendingPermissions.clear();
|
|
334
|
+
this.userMessageResolve = null;
|
|
335
|
+
this.toolCallsSinceLastResult = 0;
|
|
336
|
+
this.fileEditsSinceLastResult = 0;
|
|
337
|
+
this.setStatus("idle");
|
|
338
|
+
}
|
|
339
|
+
interrupt() {
|
|
340
|
+
if (this.queryInstance) {
|
|
341
|
+
console.log("[sdk-process] Interrupting query");
|
|
342
|
+
this.pendingInput = null;
|
|
343
|
+
this.queryInstance.interrupt().catch((err) => {
|
|
344
|
+
console.error("[sdk-process] Interrupt error:", err);
|
|
345
|
+
});
|
|
346
|
+
this.pendingPermissions.clear();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
sendInput(text) {
|
|
350
|
+
if (!this.userMessageResolve) {
|
|
351
|
+
// Queue the message instead of dropping it. The async generator
|
|
352
|
+
// (createUserMessageStream) checks pendingInput on each iteration,
|
|
353
|
+
// so the message will be delivered once the SDK is ready.
|
|
354
|
+
// NOTE: This is a single-slot queue — if called multiple times before
|
|
355
|
+
// the resolver is set, only the last message is preserved.
|
|
356
|
+
this.pendingInput = { text };
|
|
357
|
+
console.log("[sdk-process] Queued input (waiting for resolver)");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const resolve = this.userMessageResolve;
|
|
361
|
+
this.userMessageResolve = null;
|
|
362
|
+
resolve({
|
|
363
|
+
type: "user",
|
|
364
|
+
session_id: this._sessionId ?? "",
|
|
365
|
+
message: {
|
|
366
|
+
role: "user",
|
|
367
|
+
content: [{ type: "text", text }],
|
|
368
|
+
},
|
|
369
|
+
parent_tool_use_id: null,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Send a message with an image attachment.
|
|
374
|
+
* @param text - The text message
|
|
375
|
+
* @param image - Base64-encoded image data with mime type
|
|
376
|
+
*/
|
|
377
|
+
sendInputWithImage(text, image) {
|
|
378
|
+
if (!this.userMessageResolve) {
|
|
379
|
+
// Queue the message with image instead of dropping it.
|
|
380
|
+
this.pendingInput = { text, image };
|
|
381
|
+
console.log("[sdk-process] Queued input with image (waiting for resolver)");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const resolve = this.userMessageResolve;
|
|
385
|
+
this.userMessageResolve = null;
|
|
386
|
+
const content = [];
|
|
387
|
+
// Add image block first (Claude processes images before text)
|
|
388
|
+
content.push({
|
|
389
|
+
type: "image",
|
|
390
|
+
source: {
|
|
391
|
+
type: "base64",
|
|
392
|
+
media_type: image.mimeType,
|
|
393
|
+
data: image.base64,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
// Add text block
|
|
397
|
+
content.push({ type: "text", text });
|
|
398
|
+
console.log(`[sdk-process] Sending message with image (${image.mimeType}, ${Math.round(image.base64.length / 1024)}KB base64)`);
|
|
399
|
+
resolve({
|
|
400
|
+
type: "user",
|
|
401
|
+
session_id: this._sessionId ?? "",
|
|
402
|
+
message: {
|
|
403
|
+
role: "user",
|
|
404
|
+
content,
|
|
405
|
+
},
|
|
406
|
+
parent_tool_use_id: null,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Approve a pending permission request.
|
|
411
|
+
* With the SDK, this actually blocks tool execution until approved.
|
|
412
|
+
*/
|
|
413
|
+
approve(toolUseId, updatedInput) {
|
|
414
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
415
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
416
|
+
if (!pending) {
|
|
417
|
+
console.log("[sdk-process] approve() called but no pending permission requests");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const mergedInput = updatedInput
|
|
421
|
+
? { ...pending.input, ...updatedInput }
|
|
422
|
+
: pending.input;
|
|
423
|
+
this.pendingPermissions.delete(id);
|
|
424
|
+
pending.resolve({
|
|
425
|
+
behavior: "allow",
|
|
426
|
+
updatedInput: mergedInput,
|
|
427
|
+
});
|
|
428
|
+
if (this.pendingPermissions.size === 0) {
|
|
429
|
+
this.setStatus("running");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Approve a pending permission request and add a session-scoped allow rule.
|
|
434
|
+
*/
|
|
435
|
+
approveAlways(toolUseId) {
|
|
436
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
437
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
438
|
+
if (pending) {
|
|
439
|
+
const rule = buildSessionRule(pending.toolName, pending.input);
|
|
440
|
+
this.sessionAllowRules.add(rule);
|
|
441
|
+
console.log(`[sdk-process] Added session allow rule: ${rule}`);
|
|
442
|
+
}
|
|
443
|
+
this.approve(id);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Reject a pending permission request.
|
|
447
|
+
* The SDK's canUseTool will return deny, which tells Claude the tool was rejected.
|
|
448
|
+
*/
|
|
449
|
+
reject(toolUseId, message) {
|
|
450
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
451
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
452
|
+
if (!pending) {
|
|
453
|
+
console.log("[sdk-process] reject() called but no pending permission requests");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.pendingPermissions.delete(id);
|
|
457
|
+
pending.resolve({
|
|
458
|
+
behavior: "deny",
|
|
459
|
+
message: message ?? "User rejected this action",
|
|
460
|
+
});
|
|
461
|
+
if (this.pendingPermissions.size === 0) {
|
|
462
|
+
this.setStatus("running");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Answer an AskUserQuestion tool call.
|
|
467
|
+
* The SDK handles this through canUseTool with updatedInput.
|
|
468
|
+
*/
|
|
469
|
+
answer(toolUseId, result) {
|
|
470
|
+
const pending = this.pendingPermissions.get(toolUseId);
|
|
471
|
+
if (!pending || pending.toolName !== "AskUserQuestion") {
|
|
472
|
+
console.log("[sdk-process] answer() called but no pending AskUserQuestion");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.pendingPermissions.delete(toolUseId);
|
|
476
|
+
pending.resolve({
|
|
477
|
+
behavior: "allow",
|
|
478
|
+
updatedInput: {
|
|
479
|
+
...pending.input,
|
|
480
|
+
answers: { ...(pending.input.answers ?? {}), result },
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
if (this.pendingPermissions.size === 0) {
|
|
484
|
+
this.setStatus("running");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Update permission mode for the current session.
|
|
489
|
+
* Only available while the query instance is active.
|
|
490
|
+
*/
|
|
491
|
+
async setPermissionMode(mode) {
|
|
492
|
+
if (!this.queryInstance) {
|
|
493
|
+
throw new Error("No active query instance");
|
|
494
|
+
}
|
|
495
|
+
await this.queryInstance.setPermissionMode(mode);
|
|
496
|
+
this._permissionMode = mode;
|
|
497
|
+
this.emitMessage({
|
|
498
|
+
type: "system",
|
|
499
|
+
subtype: "set_permission_mode",
|
|
500
|
+
permissionMode: mode,
|
|
501
|
+
sessionId: this._sessionId ?? undefined,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Rewind files to their state at the specified user message.
|
|
506
|
+
* Requires enableFileCheckpointing to be enabled (done in start()).
|
|
507
|
+
*/
|
|
508
|
+
async rewindFiles(userMessageId, dryRun) {
|
|
509
|
+
if (!this.queryInstance) {
|
|
510
|
+
return { canRewind: false, error: "No active query instance" };
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const result = await this.queryInstance.rewindFiles(userMessageId, { dryRun });
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
return { canRewind: false, error: err instanceof Error ? err.message : String(err) };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// ---- Private ----
|
|
521
|
+
/**
|
|
522
|
+
* Proactively fetch supported commands from the SDK.
|
|
523
|
+
* This may resolve before the first user input, providing slash commands
|
|
524
|
+
* without waiting for system/init.
|
|
525
|
+
*/
|
|
526
|
+
fetchSupportedCommands() {
|
|
527
|
+
if (!this.queryInstance)
|
|
528
|
+
return;
|
|
529
|
+
const TIMEOUT_MS = 10_000;
|
|
530
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
531
|
+
setTimeout(() => resolve(null), TIMEOUT_MS);
|
|
532
|
+
});
|
|
533
|
+
Promise.race([
|
|
534
|
+
this.queryInstance.supportedCommands(),
|
|
535
|
+
timeoutPromise,
|
|
536
|
+
])
|
|
537
|
+
.then((result) => {
|
|
538
|
+
if (this.stopped || !result)
|
|
539
|
+
return;
|
|
540
|
+
const slashCommands = result.map((cmd) => cmd.name);
|
|
541
|
+
console.log(`[sdk-process] supportedCommands() returned ${slashCommands.length} commands`);
|
|
542
|
+
this.emitMessage({
|
|
543
|
+
type: "system",
|
|
544
|
+
subtype: "supported_commands",
|
|
545
|
+
slashCommands,
|
|
546
|
+
});
|
|
547
|
+
})
|
|
548
|
+
.catch((err) => {
|
|
549
|
+
console.log(`[sdk-process] supportedCommands() failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
firstPendingId() {
|
|
553
|
+
const first = this.pendingPermissions.keys().next();
|
|
554
|
+
return first.done ? undefined : first.value;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Returns a snapshot of a pending permission request.
|
|
558
|
+
* Used by the bridge to support Clear & Accept flows.
|
|
559
|
+
*/
|
|
560
|
+
getPendingPermission(toolUseId) {
|
|
561
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
562
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
563
|
+
if (!pending || !id)
|
|
564
|
+
return undefined;
|
|
565
|
+
return {
|
|
566
|
+
toolUseId: id,
|
|
567
|
+
toolName: pending.toolName,
|
|
568
|
+
input: { ...pending.input },
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async *createUserMessageStream() {
|
|
572
|
+
while (!this.stopped) {
|
|
573
|
+
if (this.pendingInput) {
|
|
574
|
+
const { text, image } = this.pendingInput;
|
|
575
|
+
this.pendingInput = null;
|
|
576
|
+
console.log(`[sdk-process] Sending pending input${image ? " with image" : ""}`);
|
|
577
|
+
const content = [];
|
|
578
|
+
if (image) {
|
|
579
|
+
content.push({
|
|
580
|
+
type: "image",
|
|
581
|
+
source: {
|
|
582
|
+
type: "base64",
|
|
583
|
+
media_type: image.mimeType,
|
|
584
|
+
data: image.base64,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
content.push({ type: "text", text });
|
|
589
|
+
yield {
|
|
590
|
+
type: "user",
|
|
591
|
+
session_id: this._sessionId ?? "",
|
|
592
|
+
message: {
|
|
593
|
+
role: "user",
|
|
594
|
+
content,
|
|
595
|
+
},
|
|
596
|
+
parent_tool_use_id: null,
|
|
597
|
+
};
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const msg = await new Promise((resolve) => {
|
|
601
|
+
this.userMessageResolve = resolve;
|
|
602
|
+
});
|
|
603
|
+
if (this.stopped)
|
|
604
|
+
break;
|
|
605
|
+
yield msg;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async processMessages() {
|
|
609
|
+
if (!this.queryInstance)
|
|
610
|
+
return;
|
|
611
|
+
for await (const message of this.queryInstance) {
|
|
612
|
+
if (this.stopped)
|
|
613
|
+
break;
|
|
614
|
+
// Convert SDK message to ServerMessage
|
|
615
|
+
let serverMsg = sdkMessageToServerMessage(message);
|
|
616
|
+
if (serverMsg?.type === "result") {
|
|
617
|
+
if (this.toolCallsSinceLastResult > 0 || this.fileEditsSinceLastResult > 0) {
|
|
618
|
+
serverMsg = {
|
|
619
|
+
...serverMsg,
|
|
620
|
+
...(this.toolCallsSinceLastResult > 0
|
|
621
|
+
? { toolCalls: this.toolCallsSinceLastResult }
|
|
622
|
+
: {}),
|
|
623
|
+
...(this.fileEditsSinceLastResult > 0
|
|
624
|
+
? { fileEdits: this.fileEditsSinceLastResult }
|
|
625
|
+
: {}),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
this.toolCallsSinceLastResult = 0;
|
|
629
|
+
this.fileEditsSinceLastResult = 0;
|
|
630
|
+
}
|
|
631
|
+
if (serverMsg) {
|
|
632
|
+
this.emitMessage(serverMsg);
|
|
633
|
+
}
|
|
634
|
+
// Extract session ID from system/init
|
|
635
|
+
if (message.type === "system" && "subtype" in message && message.subtype === "init") {
|
|
636
|
+
if (this.initTimeoutId) {
|
|
637
|
+
clearTimeout(this.initTimeoutId);
|
|
638
|
+
this.initTimeoutId = null;
|
|
639
|
+
}
|
|
640
|
+
this._sessionId = message.session_id;
|
|
641
|
+
this.setStatus("idle");
|
|
642
|
+
}
|
|
643
|
+
// Update status from message type
|
|
644
|
+
this.updateStatusFromMessage(message);
|
|
645
|
+
}
|
|
646
|
+
// Query finished
|
|
647
|
+
this.queryInstance = null;
|
|
648
|
+
this.setStatus("idle");
|
|
649
|
+
this.emit("exit", 0);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Core permission handler: called by SDK before each tool execution.
|
|
653
|
+
* Returns a Promise that resolves when the user approves/rejects.
|
|
654
|
+
*/
|
|
655
|
+
async handleCanUseTool(toolName, input, options) {
|
|
656
|
+
// AskUserQuestion: always forward to client for response
|
|
657
|
+
if (toolName === "AskUserQuestion") {
|
|
658
|
+
return this.waitForPermission(options.toolUseID, toolName, input, options.signal);
|
|
659
|
+
}
|
|
660
|
+
// Auto-approve check: session allow rules
|
|
661
|
+
if (matchesSessionRule(toolName, input, this.sessionAllowRules)) {
|
|
662
|
+
return { behavior: "allow", updatedInput: input };
|
|
663
|
+
}
|
|
664
|
+
// SDK handles permissionMode internally, but canUseTool is only called
|
|
665
|
+
// for tools that the SDK thinks need permission. We emit the request
|
|
666
|
+
// to the mobile client and wait.
|
|
667
|
+
return this.waitForPermission(options.toolUseID, toolName, input, options.signal);
|
|
668
|
+
}
|
|
669
|
+
waitForPermission(toolUseId, toolName, input, signal) {
|
|
670
|
+
// Emit permission request to client
|
|
671
|
+
this.emitMessage({
|
|
672
|
+
type: "permission_request",
|
|
673
|
+
toolUseId,
|
|
674
|
+
toolName,
|
|
675
|
+
input,
|
|
676
|
+
});
|
|
677
|
+
this.setStatus("waiting_approval");
|
|
678
|
+
return new Promise((resolve) => {
|
|
679
|
+
this.pendingPermissions.set(toolUseId, { resolve, toolName, input });
|
|
680
|
+
// Handle abort (timeout)
|
|
681
|
+
if (signal.aborted) {
|
|
682
|
+
this.pendingPermissions.delete(toolUseId);
|
|
683
|
+
resolve({ behavior: "deny", message: "Permission request aborted" });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
signal.addEventListener("abort", () => {
|
|
687
|
+
if (this.pendingPermissions.has(toolUseId)) {
|
|
688
|
+
this.pendingPermissions.delete(toolUseId);
|
|
689
|
+
resolve({ behavior: "deny", message: "Permission request timed out" });
|
|
690
|
+
}
|
|
691
|
+
}, { once: true });
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
updateStatusFromMessage(msg) {
|
|
695
|
+
switch (msg.type) {
|
|
696
|
+
case "system":
|
|
697
|
+
// Already handled in processMessages for init
|
|
698
|
+
break;
|
|
699
|
+
case "assistant":
|
|
700
|
+
if (this.pendingPermissions.size === 0) {
|
|
701
|
+
this.setStatus("running");
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
case "user":
|
|
705
|
+
if (this.pendingPermissions.size === 0) {
|
|
706
|
+
this.setStatus("running");
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
case "result":
|
|
710
|
+
this.pendingPermissions.clear();
|
|
711
|
+
this.setStatus("idle");
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
handlePostToolUseHook(input) {
|
|
716
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const hookInput = input;
|
|
720
|
+
const toolName = hookInput.tool_name;
|
|
721
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.toolCallsSinceLastResult += 1;
|
|
725
|
+
if (isFileEditToolName(toolName)) {
|
|
726
|
+
this.fileEditsSinceLastResult += 1;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
setStatus(status) {
|
|
730
|
+
if (this._status !== status) {
|
|
731
|
+
this._status = status;
|
|
732
|
+
this.emit("status", status);
|
|
733
|
+
this.emitMessage({ type: "status", status });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
emitMessage(msg) {
|
|
737
|
+
this.emit("message", msg);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=sdk-process.js.map
|