@nowcrew/daemon 0.5.29 → 0.5.31
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 +17 -1
- package/dist/config.js +13 -1
- package/dist/execution-journal.js +96 -5
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +52 -29
- package/dist/host-execution-coordinator.js +241 -0
- package/dist/local-executor.js +78 -0
- package/dist/machine-info.js +16 -1
- package/dist/remote/claude-bridge.js +402 -0
- package/dist/remote/claude-channel.js +164 -0
- package/dist/remote/codex-client.js +408 -0
- package/dist/remote/codex-runtime.js +77 -0
- package/dist/remote/config.js +83 -0
- package/dist/remote/gateway.js +572 -0
- package/dist/remote/protocol.js +178 -0
- package/dist/remote/remote-cli.js +233 -0
- package/dist/remote/session-discovery.js +249 -0
- package/dist/remote/wrapper.js +40 -0
- package/dist/runner.js +4 -0
- package/dist/runtime-startup-gate.js +91 -0
- package/dist/runtimes/codex-app-server-runner.js +226 -29
- package/dist/serve.js +70 -8
- package/dist/shared-execution-slots.js +48 -33
- package/package.json +1 -1
package/dist/local-executor.js
CHANGED
|
@@ -16,6 +16,8 @@ import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder
|
|
|
16
16
|
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
17
17
|
import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
|
|
18
18
|
import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
19
|
+
import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
|
|
20
|
+
import { dslog } from "./slog.js";
|
|
19
21
|
function truncateUtf8(value, maxBytes) {
|
|
20
22
|
if (maxBytes <= 0)
|
|
21
23
|
return "";
|
|
@@ -198,6 +200,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
198
200
|
}), dependencies.cancellation);
|
|
199
201
|
let materialized = null;
|
|
200
202
|
let knownAttachmentDirectory = null;
|
|
203
|
+
let startupReservation = null;
|
|
201
204
|
try {
|
|
202
205
|
if (isDeepSeekCodex && !providerConfig.providerApiKey) {
|
|
203
206
|
throw new Error("DeepSeek API key is not configured for this Agent");
|
|
@@ -288,6 +291,28 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
288
291
|
const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
|
|
289
292
|
if (dependencies.cancellation?.isRequested())
|
|
290
293
|
throw new RuntimeCancelledError();
|
|
294
|
+
startupReservation = dependencies.startupGate?.reserve(runtime.name) ?? null;
|
|
295
|
+
if (startupReservation !== null) {
|
|
296
|
+
const startupQueueEnteredAt = Date.now();
|
|
297
|
+
dslog("runtime.start_queued", "runtime 等待启动许可", {
|
|
298
|
+
execution_id: input.executionId,
|
|
299
|
+
runtime: runtime.name,
|
|
300
|
+
queued: startupReservation.isQueued(),
|
|
301
|
+
});
|
|
302
|
+
try {
|
|
303
|
+
await awaitWithCancellation(startupReservation.ready, dependencies.cancellation);
|
|
304
|
+
dslog("runtime.start_granted", "runtime 获得启动许可", {
|
|
305
|
+
execution_id: input.executionId,
|
|
306
|
+
runtime: runtime.name,
|
|
307
|
+
startup_queue_ms: Date.now() - startupQueueEnteredAt,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
startupReservation.release();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const runtimeLaunchAt = Date.now();
|
|
291
316
|
const child = await launchRuntime({
|
|
292
317
|
runtime: runtime.name,
|
|
293
318
|
bin: runtime.name,
|
|
@@ -321,11 +346,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
321
346
|
const externalOutput = new ExternalAnswerDecoder();
|
|
322
347
|
// 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
|
|
323
348
|
const consoleFormatter = createConsoleFormatter();
|
|
349
|
+
let runtimeReady = false;
|
|
350
|
+
let resolveRuntimeReady;
|
|
351
|
+
const runtimeReadySignal = new Promise((resolve) => { resolveRuntimeReady = resolve; });
|
|
352
|
+
let runtimeReadyNotification = Promise.resolve();
|
|
353
|
+
const markRuntimeReady = () => {
|
|
354
|
+
if (runtimeReady)
|
|
355
|
+
return;
|
|
356
|
+
runtimeReady = true;
|
|
357
|
+
startupReservation?.release();
|
|
358
|
+
dslog("runtime.start_ready", "runtime 已完成初始化", {
|
|
359
|
+
execution_id: input.executionId,
|
|
360
|
+
runtime: runtime.name,
|
|
361
|
+
startup_ms: Date.now() - runtimeLaunchAt,
|
|
362
|
+
});
|
|
363
|
+
runtimeReadyNotification = Promise.resolve(callbacks.onRuntimeReady?.(runtime.name));
|
|
364
|
+
resolveRuntimeReady();
|
|
365
|
+
};
|
|
324
366
|
const readline = createInterface({ input: child.stdout });
|
|
325
367
|
readline.on("line", (line) => {
|
|
326
368
|
const event = parseLine(line);
|
|
327
369
|
if (!event)
|
|
328
370
|
return;
|
|
371
|
+
if (isRuntimeReadyEvent(runtime.name, event))
|
|
372
|
+
markRuntimeReady();
|
|
329
373
|
const meta = extractRunMeta(event);
|
|
330
374
|
if (meta.sessionId)
|
|
331
375
|
sessionId = meta.sessionId;
|
|
@@ -358,6 +402,39 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
358
402
|
});
|
|
359
403
|
let runtimeExit;
|
|
360
404
|
try {
|
|
405
|
+
if (startupReservation !== null) {
|
|
406
|
+
let startupTimer;
|
|
407
|
+
let startupOutcome;
|
|
408
|
+
try {
|
|
409
|
+
startupOutcome = await awaitWithCancellation(Promise.race([
|
|
410
|
+
runtimeReadySignal.then(() => ({ kind: "ready" })),
|
|
411
|
+
child.exit.then((exit) => ({ kind: "exit", exit })),
|
|
412
|
+
new Promise((resolve) => {
|
|
413
|
+
startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 60_000);
|
|
414
|
+
}),
|
|
415
|
+
]), dependencies.cancellation);
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
if (startupTimer !== undefined)
|
|
419
|
+
clearTimeout(startupTimer);
|
|
420
|
+
}
|
|
421
|
+
if (startupOutcome.kind !== "ready") {
|
|
422
|
+
if (startupOutcome.kind === "timeout") {
|
|
423
|
+
dslog("runtime.start_timeout", "runtime 启动超时", {
|
|
424
|
+
level: "ERROR",
|
|
425
|
+
execution_id: input.executionId,
|
|
426
|
+
runtime: runtime.name,
|
|
427
|
+
startup_timeout_ms: dependencies.startupTimeoutMs ?? 60_000,
|
|
428
|
+
});
|
|
429
|
+
await child.cancel?.();
|
|
430
|
+
}
|
|
431
|
+
startupReservation.release();
|
|
432
|
+
throw new Error(startupOutcome.kind === "timeout"
|
|
433
|
+
? `${runtime.name} startup timed out`
|
|
434
|
+
: `${runtime.name} exited before signaling ready (exit ${startupOutcome.exit.exitCode})`);
|
|
435
|
+
}
|
|
436
|
+
await runtimeReadyNotification;
|
|
437
|
+
}
|
|
361
438
|
runtimeExit = await awaitWithCancellation(child.exit, dependencies.cancellation);
|
|
362
439
|
}
|
|
363
440
|
catch (error) {
|
|
@@ -415,6 +492,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
415
492
|
};
|
|
416
493
|
}
|
|
417
494
|
finally {
|
|
495
|
+
startupReservation?.release();
|
|
418
496
|
const attachmentDirectories = new Set([
|
|
419
497
|
...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
|
|
420
498
|
...(materialized === null ? [] : [materialized.directory]),
|
package/dist/machine-info.js
CHANGED
|
@@ -25,6 +25,7 @@ export const DAEMON_CAPABILITIES = [
|
|
|
25
25
|
"execution_external_output_v1",
|
|
26
26
|
"execution_attachments_v1",
|
|
27
27
|
"execution_answer_stream_v1",
|
|
28
|
+
"execution_machine_queue_v1",
|
|
28
29
|
];
|
|
29
30
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
30
31
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
@@ -117,7 +118,21 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
117
118
|
capabilities: DAEMON_CAPABILITIES,
|
|
118
119
|
...(backend.supported ? {
|
|
119
120
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
120
|
-
executionLimits: Object.freeze({
|
|
121
|
+
executionLimits: Object.freeze({
|
|
122
|
+
maxPromptBytes: executionLimits.maxPromptBytes,
|
|
123
|
+
maxTimeoutMs: executionLimits.maxTimeoutMs,
|
|
124
|
+
maxEventBytes: executionLimits.maxEventBytes,
|
|
125
|
+
maxParallelPerAgent: executionLimits.maxParallelPerAgent,
|
|
126
|
+
maxQueuedPerAgent: executionLimits.maxQueuedPerAgent,
|
|
127
|
+
}),
|
|
128
|
+
executionScheduler: Object.freeze({
|
|
129
|
+
maxParallelTotal: executionLimits.maxParallelTotal,
|
|
130
|
+
maxQueuedTotal: executionLimits.maxQueuedTotal,
|
|
131
|
+
maxStartingTotal: executionLimits.maxStartingTotal,
|
|
132
|
+
maxStartingPerRuntime: executionLimits.maxStartingPerRuntime,
|
|
133
|
+
startupGapMs: executionLimits.startupGapMs,
|
|
134
|
+
startupTimeoutMs: executionLimits.startupTimeoutMs,
|
|
135
|
+
}),
|
|
121
136
|
} : {}),
|
|
122
137
|
agentHandles,
|
|
123
138
|
};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { toConsoleLines } from "../console.js";
|
|
7
|
+
import { ChannelToGatewaySchema, GatewayToChannelSchema, REMOTE_PROTOCOL_VERSION, } from "./protocol.js";
|
|
8
|
+
import { resolveClaudeWrapperSession } from "./wrapper.js";
|
|
9
|
+
class InputStream {
|
|
10
|
+
values = [];
|
|
11
|
+
waiters = [];
|
|
12
|
+
stopped = false;
|
|
13
|
+
push(value) {
|
|
14
|
+
if (this.stopped)
|
|
15
|
+
return;
|
|
16
|
+
const waiter = this.waiters.shift();
|
|
17
|
+
if (waiter)
|
|
18
|
+
waiter({ value, done: false });
|
|
19
|
+
else
|
|
20
|
+
this.values.push(value);
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
this.stopped = true;
|
|
24
|
+
for (const waiter of this.waiters.splice(0))
|
|
25
|
+
waiter({ value: undefined, done: true });
|
|
26
|
+
}
|
|
27
|
+
[Symbol.asyncIterator]() {
|
|
28
|
+
return {
|
|
29
|
+
next: () => {
|
|
30
|
+
const value = this.values.shift();
|
|
31
|
+
if (value)
|
|
32
|
+
return Promise.resolve({ value, done: false });
|
|
33
|
+
if (this.stopped)
|
|
34
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
35
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const AskQuestionInputSchema = z.object({
|
|
41
|
+
questions: z.array(z.object({
|
|
42
|
+
question: z.string().min(1),
|
|
43
|
+
header: z.string().default("Question"),
|
|
44
|
+
options: z.array(z.object({
|
|
45
|
+
label: z.string(),
|
|
46
|
+
description: z.string().default(""),
|
|
47
|
+
}).passthrough()).default([]),
|
|
48
|
+
}).passthrough()).min(1).max(4),
|
|
49
|
+
}).passthrough();
|
|
50
|
+
function takeValue(args, index, option) {
|
|
51
|
+
const value = args[index + 1];
|
|
52
|
+
if (!value || value.startsWith("-"))
|
|
53
|
+
throw new Error(`${option} requires a value`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function splitTools(value) {
|
|
57
|
+
return value.split(/[ ,]+/).map((part) => part.trim()).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
export function parseClaudeBridgeArgs(userArgs) {
|
|
60
|
+
const resolved = resolveClaudeWrapperSession(userArgs);
|
|
61
|
+
const options = {};
|
|
62
|
+
const prompts = [];
|
|
63
|
+
let title;
|
|
64
|
+
const args = resolved.args;
|
|
65
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
66
|
+
const arg = args[index];
|
|
67
|
+
const inline = (name) => arg.startsWith(`${name}=`) ? arg.slice(name.length + 1) : null;
|
|
68
|
+
if (["--session-id", "--resume", "-r"].includes(arg)) {
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg.startsWith("--session-id=") || arg.startsWith("--resume="))
|
|
73
|
+
continue;
|
|
74
|
+
if (arg === "--model" || arg === "--effort" || arg === "--permission-mode" || arg === "--name" || arg === "-n"
|
|
75
|
+
|| arg === "--add-dir" || arg === "--allowedTools" || arg === "--allowed-tools"
|
|
76
|
+
|| arg === "--disallowedTools" || arg === "--disallowed-tools" || arg === "--settings") {
|
|
77
|
+
const value = takeValue(args, index, arg);
|
|
78
|
+
index += 1;
|
|
79
|
+
if (arg === "--model")
|
|
80
|
+
options.model = value;
|
|
81
|
+
else if (arg === "--effort")
|
|
82
|
+
options.effort = z.enum(["low", "medium", "high", "xhigh", "max"]).parse(value);
|
|
83
|
+
else if (arg === "--permission-mode")
|
|
84
|
+
options.permissionMode = z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"]).parse(value);
|
|
85
|
+
else if (arg === "--name" || arg === "-n")
|
|
86
|
+
title = value;
|
|
87
|
+
else if (arg === "--add-dir")
|
|
88
|
+
(options.additionalDirectories ??= []).push(value);
|
|
89
|
+
else if (arg === "--allowedTools" || arg === "--allowed-tools")
|
|
90
|
+
options.allowedTools = splitTools(value);
|
|
91
|
+
else if (arg === "--disallowedTools" || arg === "--disallowed-tools")
|
|
92
|
+
options.disallowedTools = splitTools(value);
|
|
93
|
+
else
|
|
94
|
+
options.settings = value;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const model = inline("--model");
|
|
98
|
+
const effort = inline("--effort");
|
|
99
|
+
const permission = inline("--permission-mode");
|
|
100
|
+
const name = inline("--name");
|
|
101
|
+
if (model !== null)
|
|
102
|
+
options.model = model;
|
|
103
|
+
else if (effort !== null)
|
|
104
|
+
options.effort = z.enum(["low", "medium", "high", "xhigh", "max"]).parse(effort);
|
|
105
|
+
else if (permission !== null)
|
|
106
|
+
options.permissionMode = z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"]).parse(permission);
|
|
107
|
+
else if (name !== null)
|
|
108
|
+
title = name;
|
|
109
|
+
else if (arg === "--dangerously-skip-permissions") {
|
|
110
|
+
options.permissionMode = "bypassPermissions";
|
|
111
|
+
options.allowDangerouslySkipPermissions = true;
|
|
112
|
+
}
|
|
113
|
+
else if (arg.startsWith("-")) {
|
|
114
|
+
throw new Error(`Claude mobile bridge does not support ${arg}`);
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
prompts.push(arg);
|
|
118
|
+
}
|
|
119
|
+
const hasResume = userArgs.some((value) => value === "--resume" || value === "-r" || value.startsWith("--resume="));
|
|
120
|
+
return {
|
|
121
|
+
sessionId: resolved.sessionId,
|
|
122
|
+
resume: hasResume,
|
|
123
|
+
...(title === undefined ? {} : { title }),
|
|
124
|
+
...(prompts.length === 0 ? {} : { initialPrompt: prompts.join(" ") }),
|
|
125
|
+
options,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function userMessage(sessionId, text) {
|
|
129
|
+
return {
|
|
130
|
+
type: "user",
|
|
131
|
+
message: { role: "user", content: text },
|
|
132
|
+
parent_tool_use_id: null,
|
|
133
|
+
session_id: sessionId,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function preview(input) {
|
|
137
|
+
const command = typeof input.command === "string" ? input.command : null;
|
|
138
|
+
const path = typeof input.file_path === "string" ? input.file_path : null;
|
|
139
|
+
return (command ?? path ?? JSON.stringify(input)).slice(0, 16_000);
|
|
140
|
+
}
|
|
141
|
+
function printMessage(message) {
|
|
142
|
+
let printedText = false;
|
|
143
|
+
for (const line of toConsoleLines(message)) {
|
|
144
|
+
if (line.stream === "result" || !line.text.trim())
|
|
145
|
+
continue;
|
|
146
|
+
if (line.stream === "text")
|
|
147
|
+
printedText = true;
|
|
148
|
+
const prefix = line.stream === "thinking" ? "Thinking: "
|
|
149
|
+
: line.stream === "tool" ? "Tool: "
|
|
150
|
+
: line.stream === "tool_result" ? "Result: "
|
|
151
|
+
: line.stream === "error" ? "Error: " : "";
|
|
152
|
+
process.stdout.write(`\n${prefix}${line.text}\n`);
|
|
153
|
+
}
|
|
154
|
+
return printedText;
|
|
155
|
+
}
|
|
156
|
+
export async function runClaudeBridge(userArgs, context) {
|
|
157
|
+
const parsed = parseClaudeBridgeArgs(userArgs);
|
|
158
|
+
const generation = randomUUID();
|
|
159
|
+
const inputStream = new InputStream();
|
|
160
|
+
const inputs = [];
|
|
161
|
+
const interactions = new Map();
|
|
162
|
+
const terminalInteractions = [];
|
|
163
|
+
let terminalHandler = null;
|
|
164
|
+
let active = null;
|
|
165
|
+
let stopped = false;
|
|
166
|
+
let socket = null;
|
|
167
|
+
let reconnectTimer;
|
|
168
|
+
let assistantPrinted = false;
|
|
169
|
+
const url = new URL("/ws/channel", context.gatewayUrl);
|
|
170
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
171
|
+
const send = (value) => {
|
|
172
|
+
if (socket?.readyState === WebSocket.OPEN)
|
|
173
|
+
socket.send(JSON.stringify(ChannelToGatewaySchema.parse(value)));
|
|
174
|
+
};
|
|
175
|
+
const register = () => send({
|
|
176
|
+
type: "channel.register",
|
|
177
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
178
|
+
sessionId: parsed.sessionId,
|
|
179
|
+
cwd: context.cwd,
|
|
180
|
+
pid: process.pid,
|
|
181
|
+
generation,
|
|
182
|
+
...(parsed.title === undefined ? {} : { title: parsed.title }),
|
|
183
|
+
});
|
|
184
|
+
const finishInteraction = (requestId, value, local) => {
|
|
185
|
+
const interaction = interactions.get(requestId);
|
|
186
|
+
if (!interaction)
|
|
187
|
+
return;
|
|
188
|
+
interactions.delete(requestId);
|
|
189
|
+
const index = terminalInteractions.indexOf(requestId);
|
|
190
|
+
if (index >= 0)
|
|
191
|
+
terminalInteractions.splice(index, 1);
|
|
192
|
+
terminalHandler = null;
|
|
193
|
+
interaction.resolve(value);
|
|
194
|
+
if (local)
|
|
195
|
+
send({
|
|
196
|
+
type: "bridge.resolved",
|
|
197
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
198
|
+
sessionId: parsed.sessionId,
|
|
199
|
+
requestId,
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const connectGateway = () => new Promise((resolve, reject) => {
|
|
203
|
+
const next = new WebSocket(url, { headers: { authorization: `Bearer ${context.token}` } });
|
|
204
|
+
socket = next;
|
|
205
|
+
next.once("open", () => {
|
|
206
|
+
register();
|
|
207
|
+
if (active)
|
|
208
|
+
send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: true });
|
|
209
|
+
for (const interaction of interactions.values())
|
|
210
|
+
send(interaction.requestFrame);
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
next.on("message", (raw) => {
|
|
214
|
+
let decoded;
|
|
215
|
+
try {
|
|
216
|
+
decoded = JSON.parse(raw.toString());
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const frame = GatewayToChannelSchema.safeParse(decoded);
|
|
222
|
+
if (!frame.success)
|
|
223
|
+
return;
|
|
224
|
+
if (frame.data.type === "channel.prompt")
|
|
225
|
+
enqueue({ text: frame.data.text, requestId: frame.data.requestId });
|
|
226
|
+
else if (frame.data.type === "bridge.approval_response") {
|
|
227
|
+
finishInteraction(frame.data.requestId, { decision: frame.data.decision }, false);
|
|
228
|
+
}
|
|
229
|
+
else if (frame.data.type === "bridge.question_response") {
|
|
230
|
+
finishInteraction(frame.data.requestId, { answers: frame.data.answers }, false);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
next.once("error", reject);
|
|
234
|
+
next.on("close", () => {
|
|
235
|
+
if (socket === next)
|
|
236
|
+
socket = null;
|
|
237
|
+
if (!stopped)
|
|
238
|
+
reconnectTimer = setTimeout(() => { void connectGateway().catch(() => { }); }, 1_000);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
const sendNext = () => {
|
|
242
|
+
if (active || inputs.length === 0 || stopped)
|
|
243
|
+
return;
|
|
244
|
+
active = inputs.shift();
|
|
245
|
+
assistantPrinted = false;
|
|
246
|
+
inputStream.push(userMessage(parsed.sessionId, active.text));
|
|
247
|
+
send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: true });
|
|
248
|
+
};
|
|
249
|
+
function enqueue(value) {
|
|
250
|
+
inputs.push(value);
|
|
251
|
+
sendNext();
|
|
252
|
+
}
|
|
253
|
+
const waitForInteraction = (interaction, render, handleLine) => {
|
|
254
|
+
const promise = new Promise((resolve) => {
|
|
255
|
+
interactions.set(interaction.id, { ...interaction, resolve });
|
|
256
|
+
});
|
|
257
|
+
terminalInteractions.push(interaction.id);
|
|
258
|
+
render();
|
|
259
|
+
terminalHandler = (line) => {
|
|
260
|
+
const result = handleLine(line);
|
|
261
|
+
if (result)
|
|
262
|
+
finishInteraction(interaction.id, result, true);
|
|
263
|
+
};
|
|
264
|
+
return promise;
|
|
265
|
+
};
|
|
266
|
+
const canUseTool = async (toolName, input, { signal }) => {
|
|
267
|
+
const requestId = randomUUID();
|
|
268
|
+
if (toolName === "AskUserQuestion") {
|
|
269
|
+
const parsedQuestions = AskQuestionInputSchema.safeParse(input);
|
|
270
|
+
if (!parsedQuestions.success)
|
|
271
|
+
return { behavior: "deny", message: "Unsupported question format" };
|
|
272
|
+
const questions = parsedQuestions.data.questions.map((question, index) => ({
|
|
273
|
+
id: `q${index}`,
|
|
274
|
+
header: question.header,
|
|
275
|
+
question: question.question,
|
|
276
|
+
isSecret: false,
|
|
277
|
+
options: question.options,
|
|
278
|
+
}));
|
|
279
|
+
const requestFrame = {
|
|
280
|
+
type: "bridge.question",
|
|
281
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
282
|
+
sessionId: parsed.sessionId,
|
|
283
|
+
requestId,
|
|
284
|
+
questions,
|
|
285
|
+
};
|
|
286
|
+
send(requestFrame);
|
|
287
|
+
const waiting = waitForInteraction({ id: requestId, kind: "question", questions, requestFrame }, () => {
|
|
288
|
+
process.stdout.write("\nInput needed:\n");
|
|
289
|
+
for (const [index, question] of questions.entries()) {
|
|
290
|
+
process.stdout.write(`${index + 1}. ${question.header}: ${question.question}\n`);
|
|
291
|
+
for (const option of question.options)
|
|
292
|
+
process.stdout.write(` - ${option.label}: ${option.description}\n`);
|
|
293
|
+
}
|
|
294
|
+
process.stdout.write("Answer: ");
|
|
295
|
+
}, (line) => {
|
|
296
|
+
const values = line.split(";").map((value) => value.trim());
|
|
297
|
+
return { answers: Object.fromEntries(questions.map((question, index) => [question.id, [values[index] ?? ""]])) };
|
|
298
|
+
});
|
|
299
|
+
signal.addEventListener("abort", () => finishInteraction(requestId, { decision: "deny" }, true), { once: true });
|
|
300
|
+
const result = await waiting;
|
|
301
|
+
if (!result.answers)
|
|
302
|
+
return { behavior: "deny", message: "User input cancelled" };
|
|
303
|
+
const answers = Object.fromEntries(questions.map((question) => [
|
|
304
|
+
question.question,
|
|
305
|
+
result.answers?.[question.id]?.join(", ") ?? "",
|
|
306
|
+
]));
|
|
307
|
+
return { behavior: "allow", updatedInput: { ...input, answers } };
|
|
308
|
+
}
|
|
309
|
+
const requestFrame = {
|
|
310
|
+
type: "bridge.approval",
|
|
311
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
312
|
+
sessionId: parsed.sessionId,
|
|
313
|
+
requestId,
|
|
314
|
+
tool: toolName,
|
|
315
|
+
preview: preview(input),
|
|
316
|
+
};
|
|
317
|
+
send(requestFrame);
|
|
318
|
+
const waiting = waitForInteraction({ id: requestId, kind: "approval", requestFrame }, () => process.stdout.write(`\nApproval required: ${toolName}\n${preview(input)}\nAllow? `), (line) => /^(y|yes|allow)$/i.test(line.trim()) ? { decision: "allow" }
|
|
319
|
+
: /^(n|no|deny)$/i.test(line.trim()) ? { decision: "deny" } : null);
|
|
320
|
+
signal.addEventListener("abort", () => finishInteraction(requestId, { decision: "deny" }, true), { once: true });
|
|
321
|
+
const result = await waiting;
|
|
322
|
+
return result.decision === "allow"
|
|
323
|
+
? { behavior: "allow", updatedInput: input }
|
|
324
|
+
: { behavior: "deny", message: "User denied this action" };
|
|
325
|
+
};
|
|
326
|
+
await connectGateway();
|
|
327
|
+
const conversation = query({
|
|
328
|
+
prompt: inputStream,
|
|
329
|
+
options: {
|
|
330
|
+
cwd: context.cwd,
|
|
331
|
+
canUseTool,
|
|
332
|
+
settingSources: ["user", "project", "local"],
|
|
333
|
+
includePartialMessages: false,
|
|
334
|
+
...(parsed.resume ? { resume: parsed.sessionId } : { sessionId: parsed.sessionId }),
|
|
335
|
+
...parsed.options,
|
|
336
|
+
...(process.env.NOWCREW_CLAUDE_BIN ? { pathToClaudeCodeExecutable: process.env.NOWCREW_CLAUDE_BIN } : {}),
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
340
|
+
terminal.setPrompt("› ");
|
|
341
|
+
terminal.on("line", (line) => {
|
|
342
|
+
if (terminalHandler && terminalInteractions.length > 0)
|
|
343
|
+
terminalHandler(line);
|
|
344
|
+
else if (line.trim())
|
|
345
|
+
enqueue({ text: line.trim(), requestId: null });
|
|
346
|
+
terminal.prompt();
|
|
347
|
+
});
|
|
348
|
+
terminal.on("close", () => { stopped = true; conversation.close(); });
|
|
349
|
+
process.stdout.write(`Claude mobile session ${parsed.sessionId}\n`);
|
|
350
|
+
terminal.prompt();
|
|
351
|
+
if (parsed.initialPrompt)
|
|
352
|
+
enqueue({ text: parsed.initialPrompt, requestId: null });
|
|
353
|
+
const stop = () => {
|
|
354
|
+
if (stopped)
|
|
355
|
+
return;
|
|
356
|
+
stopped = true;
|
|
357
|
+
if (reconnectTimer)
|
|
358
|
+
clearTimeout(reconnectTimer);
|
|
359
|
+
inputStream.close();
|
|
360
|
+
conversation.close();
|
|
361
|
+
terminal.close();
|
|
362
|
+
socket?.close();
|
|
363
|
+
for (const id of [...interactions.keys()])
|
|
364
|
+
finishInteraction(id, { decision: "deny" }, true);
|
|
365
|
+
};
|
|
366
|
+
process.once("SIGINT", stop);
|
|
367
|
+
process.once("SIGTERM", stop);
|
|
368
|
+
try {
|
|
369
|
+
for await (const message of conversation) {
|
|
370
|
+
if (message.type === "assistant")
|
|
371
|
+
assistantPrinted = printMessage(message) || assistantPrinted;
|
|
372
|
+
else
|
|
373
|
+
printMessage(message);
|
|
374
|
+
if (message.type !== "result")
|
|
375
|
+
continue;
|
|
376
|
+
const result = message.subtype === "success" && "result" in message && typeof message.result === "string"
|
|
377
|
+
? message.result.trim()
|
|
378
|
+
: "";
|
|
379
|
+
if (!assistantPrinted && result)
|
|
380
|
+
process.stdout.write(`\n${result}\n`);
|
|
381
|
+
const completedInput = active;
|
|
382
|
+
if (completedInput?.requestId && result)
|
|
383
|
+
send({
|
|
384
|
+
type: "channel.reply",
|
|
385
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
386
|
+
sessionId: parsed.sessionId,
|
|
387
|
+
requestId: completedInput.requestId,
|
|
388
|
+
text: result,
|
|
389
|
+
});
|
|
390
|
+
active = null;
|
|
391
|
+
send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: false });
|
|
392
|
+
terminal.prompt();
|
|
393
|
+
sendNext();
|
|
394
|
+
}
|
|
395
|
+
return stopped ? 0 : 1;
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
process.off("SIGINT", stop);
|
|
399
|
+
process.off("SIGTERM", stop);
|
|
400
|
+
stop();
|
|
401
|
+
}
|
|
402
|
+
}
|