weacpx 0.2.2 → 0.3.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 +218 -288
- package/dist/bridge/bridge-main.js +995 -322
- package/dist/cli.js +33066 -6978
- package/package.json +6 -3
|
@@ -56,6 +56,14 @@ function encodeBridgePromptSegmentEvent(event) {
|
|
|
56
56
|
return `${JSON.stringify(event)}
|
|
57
57
|
`;
|
|
58
58
|
}
|
|
59
|
+
function encodeBridgeSessionProgressEvent(event) {
|
|
60
|
+
return `${JSON.stringify(event)}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
function encodeBridgeSessionNoteEvent(event) {
|
|
64
|
+
return `${JSON.stringify(event)}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
59
67
|
|
|
60
68
|
// src/transport/prompt-output.ts
|
|
61
69
|
function getPromptText(result) {
|
|
@@ -205,12 +213,14 @@ var init_spawn_command = __esm(() => {
|
|
|
205
213
|
});
|
|
206
214
|
|
|
207
215
|
// src/transport/streaming-prompt.ts
|
|
208
|
-
function createStreamingPromptState() {
|
|
216
|
+
function createStreamingPromptState(formatToolCalls = false) {
|
|
209
217
|
return {
|
|
210
218
|
buffer: "",
|
|
211
219
|
segments: [],
|
|
212
220
|
hasAgentMessage: false,
|
|
213
221
|
pendingLine: "",
|
|
222
|
+
formatToolCalls,
|
|
223
|
+
emittedToolCallIds: new Set,
|
|
214
224
|
finalize() {
|
|
215
225
|
if (this.pendingLine.trim().length > 0) {
|
|
216
226
|
parseStreamingChunks(this, this.pendingLine);
|
|
@@ -242,11 +252,29 @@ function parseStreamingChunks(state, line) {
|
|
|
242
252
|
} catch {
|
|
243
253
|
return;
|
|
244
254
|
}
|
|
245
|
-
|
|
255
|
+
if (event.method !== "session/update")
|
|
256
|
+
return;
|
|
257
|
+
const update = event.params?.update;
|
|
258
|
+
if (!update)
|
|
259
|
+
return;
|
|
260
|
+
if (state.formatToolCalls && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update")) {
|
|
261
|
+
const formatted = formatToolCallEvent(update, update.sessionUpdate);
|
|
262
|
+
if (formatted) {
|
|
263
|
+
const toolCallId = update.toolCallId;
|
|
264
|
+
if (toolCallId) {
|
|
265
|
+
if (state.emittedToolCallIds.has(toolCallId))
|
|
266
|
+
return;
|
|
267
|
+
state.emittedToolCallIds.add(toolCallId);
|
|
268
|
+
}
|
|
269
|
+
state.segments.push(formatted);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const isMessageChunk = update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
|
|
246
274
|
if (!isMessageChunk)
|
|
247
275
|
return;
|
|
248
276
|
state.hasAgentMessage = true;
|
|
249
|
-
const chunk =
|
|
277
|
+
const chunk = update.content.text ?? "";
|
|
250
278
|
if (chunk.length === 0)
|
|
251
279
|
return;
|
|
252
280
|
state.buffer += chunk;
|
|
@@ -261,6 +289,450 @@ function parseStreamingChunks(state, line) {
|
|
|
261
289
|
}
|
|
262
290
|
}
|
|
263
291
|
}
|
|
292
|
+
function formatToolCallEvent(update, sessionUpdate) {
|
|
293
|
+
if (!update)
|
|
294
|
+
return null;
|
|
295
|
+
const kind = update.kind ?? "";
|
|
296
|
+
const title = update.title ?? "";
|
|
297
|
+
if (title.length === 0)
|
|
298
|
+
return null;
|
|
299
|
+
const emoji = KIND_EMOJI[kind] ?? "\uD83D\uDD27";
|
|
300
|
+
const command = getToolDisplayCommand(update);
|
|
301
|
+
if (command) {
|
|
302
|
+
return `${emoji} ${truncateToolDisplay(command)}`;
|
|
303
|
+
}
|
|
304
|
+
if (sessionUpdate === "tool_call_update" || isGenericToolTitle(kind, title))
|
|
305
|
+
return null;
|
|
306
|
+
return `${emoji} ${title}`;
|
|
307
|
+
}
|
|
308
|
+
function getToolDisplayCommand(update) {
|
|
309
|
+
if (!update)
|
|
310
|
+
return null;
|
|
311
|
+
const command = update.rawInput?.command;
|
|
312
|
+
if (typeof command === "string" && command.length > 0) {
|
|
313
|
+
return command;
|
|
314
|
+
}
|
|
315
|
+
const parsedCmd = update.rawInput?.parsed_cmd;
|
|
316
|
+
if (parsedCmd && parsedCmd.length > 0) {
|
|
317
|
+
const parts = [];
|
|
318
|
+
for (const entry of parsedCmd) {
|
|
319
|
+
if (entry && typeof entry.cmd === "string" && entry.cmd.length > 0) {
|
|
320
|
+
parts.push(entry.cmd);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (parts.length > 0) {
|
|
324
|
+
return parts.join(" ");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
function truncateToolDisplay(text) {
|
|
330
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
331
|
+
}
|
|
332
|
+
function isGenericToolTitle(kind, title) {
|
|
333
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
334
|
+
if (kind === "execute" && ["bash", "shell", "sh", "powershell", "cmd"].includes(normalizedTitle)) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
if (kind === "search" && ["grep", "rg", "search"].includes(normalizedTitle)) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (kind === "read" && ["read", "cat"].includes(normalizedTitle)) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
var KIND_EMOJI;
|
|
346
|
+
var init_streaming_prompt = __esm(() => {
|
|
347
|
+
KIND_EMOJI = {
|
|
348
|
+
read: "\uD83D\uDCD6",
|
|
349
|
+
search: "\uD83D\uDD0D",
|
|
350
|
+
execute: "\uD83D\uDCBB",
|
|
351
|
+
edit: "✏️"
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// src/recovery/discover-parent-package-paths.ts
|
|
356
|
+
import { spawn } from "node:child_process";
|
|
357
|
+
import { access } from "node:fs/promises";
|
|
358
|
+
import { homedir } from "node:os";
|
|
359
|
+
import { dirname, join } from "node:path";
|
|
360
|
+
function deriveParentPackageName(platformPackage) {
|
|
361
|
+
return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
|
|
362
|
+
}
|
|
363
|
+
async function discoverParentPackagePaths(platformPackage, seedPath, deps = {}) {
|
|
364
|
+
const env = deps.env ?? process.env;
|
|
365
|
+
const home = deps.home ?? homedir();
|
|
366
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
367
|
+
const fsExists = deps.fsExists ?? defaultFsExists;
|
|
368
|
+
const resolveFromCwd = deps.resolveFromCwd ?? defaultResolveFromCwd;
|
|
369
|
+
const queryRoot = deps.queryPackageManagerRoot ?? defaultQueryPackageManagerRoot;
|
|
370
|
+
const parentName = deriveParentPackageName(platformPackage);
|
|
371
|
+
const rawCandidates = [];
|
|
372
|
+
const bunGlobalRoot = env.BUN_INSTALL ? join(env.BUN_INSTALL, "install", "global", "node_modules") : join(home, ".bun", "install", "global", "node_modules");
|
|
373
|
+
const [npmRoot, pnpmRoot, yarnRoot] = await Promise.all([
|
|
374
|
+
queryRoot("npm"),
|
|
375
|
+
queryRoot("pnpm"),
|
|
376
|
+
queryRoot("yarn")
|
|
377
|
+
]);
|
|
378
|
+
const classify = (p) => {
|
|
379
|
+
if (isUnder(p, bunGlobalRoot))
|
|
380
|
+
return "bun";
|
|
381
|
+
if (pnpmRoot && isUnder(p, pnpmRoot))
|
|
382
|
+
return "pnpm";
|
|
383
|
+
if (yarnRoot && isUnder(p, yarnRoot))
|
|
384
|
+
return "yarn";
|
|
385
|
+
return "npm";
|
|
386
|
+
};
|
|
387
|
+
if (seedPath) {
|
|
388
|
+
rawCandidates.push({ path: seedPath, manager: classify(seedPath) });
|
|
389
|
+
}
|
|
390
|
+
for (const name of [parentName, platformPackage]) {
|
|
391
|
+
const resolved = resolveFromCwd(name, cwd);
|
|
392
|
+
if (resolved)
|
|
393
|
+
rawCandidates.push({ path: resolved, manager: classify(resolved) });
|
|
394
|
+
}
|
|
395
|
+
rawCandidates.push({ path: join(bunGlobalRoot, parentName), manager: "bun" });
|
|
396
|
+
if (npmRoot)
|
|
397
|
+
rawCandidates.push({ path: join(npmRoot, parentName), manager: "npm" });
|
|
398
|
+
if (pnpmRoot)
|
|
399
|
+
rawCandidates.push({ path: join(pnpmRoot, parentName), manager: "pnpm" });
|
|
400
|
+
if (yarnRoot)
|
|
401
|
+
rawCandidates.push({ path: join(yarnRoot, parentName), manager: "yarn" });
|
|
402
|
+
const seen = new Set;
|
|
403
|
+
const verified = [];
|
|
404
|
+
for (const candidate of rawCandidates) {
|
|
405
|
+
if (seen.has(candidate.path))
|
|
406
|
+
continue;
|
|
407
|
+
seen.add(candidate.path);
|
|
408
|
+
if (await fsExists(join(candidate.path, "package.json"))) {
|
|
409
|
+
verified.push(candidate);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return verified;
|
|
413
|
+
}
|
|
414
|
+
function isUnder(child, parent) {
|
|
415
|
+
const c = child.replace(/[\\/]+$/, "");
|
|
416
|
+
const p = parent.replace(/[\\/]+$/, "");
|
|
417
|
+
return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
|
|
418
|
+
}
|
|
419
|
+
async function defaultFsExists(path) {
|
|
420
|
+
try {
|
|
421
|
+
await access(path);
|
|
422
|
+
return true;
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function defaultResolveFromCwd(name, cwd) {
|
|
428
|
+
try {
|
|
429
|
+
const pkgJson = __require.resolve(`${name}/package.json`, {
|
|
430
|
+
paths: [cwd, ...__require.resolve.paths(name) ?? []]
|
|
431
|
+
});
|
|
432
|
+
return dirname(pkgJson);
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function defaultQueryPackageManagerRoot(tool) {
|
|
438
|
+
const spec = tool === "yarn" ? { cmd: "yarn", args: ["global", "dir"], postfix: "node_modules" } : { cmd: tool, args: ["root", "-g"], postfix: null };
|
|
439
|
+
return await new Promise((resolve) => {
|
|
440
|
+
let settled = false;
|
|
441
|
+
const done = (value) => {
|
|
442
|
+
if (settled)
|
|
443
|
+
return;
|
|
444
|
+
settled = true;
|
|
445
|
+
resolve(value);
|
|
446
|
+
};
|
|
447
|
+
let child;
|
|
448
|
+
try {
|
|
449
|
+
child = spawn(spec.cmd, spec.args, {
|
|
450
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
451
|
+
shell: process.platform === "win32"
|
|
452
|
+
});
|
|
453
|
+
} catch {
|
|
454
|
+
done(null);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let stdout = "";
|
|
458
|
+
const timer = setTimeout(() => {
|
|
459
|
+
try {
|
|
460
|
+
child.kill();
|
|
461
|
+
} catch {}
|
|
462
|
+
done(null);
|
|
463
|
+
}, 2000);
|
|
464
|
+
timer.unref?.();
|
|
465
|
+
child.stdout.on("data", (chunk) => {
|
|
466
|
+
stdout += String(chunk);
|
|
467
|
+
});
|
|
468
|
+
child.on("error", () => {
|
|
469
|
+
clearTimeout(timer);
|
|
470
|
+
done(null);
|
|
471
|
+
});
|
|
472
|
+
child.on("close", (code) => {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
if (code !== 0)
|
|
475
|
+
return done(null);
|
|
476
|
+
const trimmed = stdout.trim().split(/\r?\n/).pop()?.trim() ?? "";
|
|
477
|
+
if (!trimmed)
|
|
478
|
+
return done(null);
|
|
479
|
+
done(spec.postfix ? join(trimmed, spec.postfix) : trimmed);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
var init_discover_parent_package_paths = () => {};
|
|
484
|
+
|
|
485
|
+
// src/process/terminate-process-tree.ts
|
|
486
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
487
|
+
async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
488
|
+
process.kill(targetPid, signal);
|
|
489
|
+
}, isProcessRunning = defaultIsProcessRunning) {
|
|
490
|
+
if (pid <= 0) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (platform === "win32") {
|
|
494
|
+
try {
|
|
495
|
+
await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
496
|
+
} catch {}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const targetPid = pid > 0 ? -pid : pid;
|
|
500
|
+
try {
|
|
501
|
+
killProcess(targetPid, "SIGTERM");
|
|
502
|
+
} catch {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const deadline = Date.now() + 5000;
|
|
506
|
+
while (Date.now() < deadline) {
|
|
507
|
+
if (!isProcessRunning(targetPid)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
killProcess(targetPid, "SIGKILL");
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
function defaultIsProcessRunning(pid) {
|
|
517
|
+
try {
|
|
518
|
+
process.kill(pid, 0);
|
|
519
|
+
return true;
|
|
520
|
+
} catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function defaultRunProcessCommand(command, args) {
|
|
525
|
+
return await new Promise((resolve, reject) => {
|
|
526
|
+
const child = spawn2(command, args, { stdio: "ignore" });
|
|
527
|
+
child.on("error", reject);
|
|
528
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
var init_terminate_process_tree = () => {};
|
|
532
|
+
|
|
533
|
+
// src/transport/acpx-queue-owner-launcher.ts
|
|
534
|
+
import { createHash } from "node:crypto";
|
|
535
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
536
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
537
|
+
import { homedir as homedir2 } from "node:os";
|
|
538
|
+
import { join as join2 } from "node:path";
|
|
539
|
+
function buildWeacpxMcpServerSpec(input) {
|
|
540
|
+
const { command, args } = splitCommandLine(input.weacpxCommand);
|
|
541
|
+
return {
|
|
542
|
+
name: "weacpx-orchestration",
|
|
543
|
+
type: "stdio",
|
|
544
|
+
command,
|
|
545
|
+
args: [
|
|
546
|
+
...args,
|
|
547
|
+
"mcp-stdio",
|
|
548
|
+
"--coordinator-session",
|
|
549
|
+
input.coordinatorSession,
|
|
550
|
+
...input.sourceHandle ? ["--source-handle", input.sourceHandle] : []
|
|
551
|
+
]
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function buildQueueOwnerPayload(input) {
|
|
555
|
+
return {
|
|
556
|
+
sessionId: input.sessionId,
|
|
557
|
+
permissionMode: input.permissionMode,
|
|
558
|
+
nonInteractivePermissions: input.nonInteractivePermissions,
|
|
559
|
+
ttlMs: input.ttlMs ?? 300000,
|
|
560
|
+
maxQueueDepth: input.maxQueueDepth ?? 16,
|
|
561
|
+
mcpServers: input.mcpServers
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
class AcpxQueueOwnerLauncher {
|
|
566
|
+
acpxCommand;
|
|
567
|
+
weacpxCommand;
|
|
568
|
+
spawnOwner;
|
|
569
|
+
terminateOwner;
|
|
570
|
+
baseEnv;
|
|
571
|
+
ttlMs;
|
|
572
|
+
maxQueueDepth;
|
|
573
|
+
launchLocks = new Map;
|
|
574
|
+
constructor(options) {
|
|
575
|
+
this.acpxCommand = options.acpxCommand;
|
|
576
|
+
this.weacpxCommand = options.weacpxCommand ?? resolveDefaultWeacpxCommand(options.baseEnv ?? process.env);
|
|
577
|
+
this.spawnOwner = options.spawnOwner ?? defaultQueueOwnerSpawner;
|
|
578
|
+
this.terminateOwner = options.terminateOwner ?? createDefaultQueueOwnerTerminator(options.acpxCommand);
|
|
579
|
+
this.baseEnv = options.baseEnv ?? process.env;
|
|
580
|
+
this.ttlMs = options.ttlMs;
|
|
581
|
+
this.maxQueueDepth = options.maxQueueDepth;
|
|
582
|
+
}
|
|
583
|
+
async launch(input) {
|
|
584
|
+
const key = input.acpxRecordId;
|
|
585
|
+
const previous = this.launchLocks.get(key) ?? Promise.resolve();
|
|
586
|
+
const next = previous.then(() => this.doLaunch(input), () => this.doLaunch(input));
|
|
587
|
+
this.launchLocks.set(key, next.catch(() => {}));
|
|
588
|
+
return next;
|
|
589
|
+
}
|
|
590
|
+
async doLaunch(input) {
|
|
591
|
+
await this.terminateOwner(input.acpxRecordId);
|
|
592
|
+
const payload = buildQueueOwnerPayload({
|
|
593
|
+
sessionId: input.acpxRecordId,
|
|
594
|
+
permissionMode: input.permissionMode,
|
|
595
|
+
nonInteractivePermissions: input.nonInteractivePermissions,
|
|
596
|
+
ttlMs: this.ttlMs,
|
|
597
|
+
maxQueueDepth: this.maxQueueDepth,
|
|
598
|
+
mcpServers: [buildWeacpxMcpServerSpec({
|
|
599
|
+
weacpxCommand: this.weacpxCommand,
|
|
600
|
+
coordinatorSession: input.coordinatorSession,
|
|
601
|
+
...input.sourceHandle ? { sourceHandle: input.sourceHandle } : {}
|
|
602
|
+
})]
|
|
603
|
+
});
|
|
604
|
+
const spawnSpec = resolveSpawnCommand(this.acpxCommand, ["__queue-owner"]);
|
|
605
|
+
await this.spawnOwner(spawnSpec.command, spawnSpec.args, {
|
|
606
|
+
env: {
|
|
607
|
+
...stringEnv(this.baseEnv),
|
|
608
|
+
ACPX_QUEUE_OWNER_PAYLOAD: JSON.stringify(payload)
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function splitCommandLine(value) {
|
|
614
|
+
const parts = [];
|
|
615
|
+
let current = "";
|
|
616
|
+
let quote = null;
|
|
617
|
+
let escaping = false;
|
|
618
|
+
for (const char of value) {
|
|
619
|
+
if (escaping) {
|
|
620
|
+
current += char;
|
|
621
|
+
escaping = false;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (char === "\\" && quote !== "'") {
|
|
625
|
+
escaping = true;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (quote) {
|
|
629
|
+
if (char === quote) {
|
|
630
|
+
quote = null;
|
|
631
|
+
} else {
|
|
632
|
+
current += char;
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (char === "'" || char === '"') {
|
|
637
|
+
quote = char;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (/\s/.test(char)) {
|
|
641
|
+
if (current.length > 0) {
|
|
642
|
+
parts.push(current);
|
|
643
|
+
current = "";
|
|
644
|
+
}
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
current += char;
|
|
648
|
+
}
|
|
649
|
+
if (escaping) {
|
|
650
|
+
current += "\\";
|
|
651
|
+
}
|
|
652
|
+
if (quote) {
|
|
653
|
+
throw new Error("weacpx MCP command has an unterminated quote");
|
|
654
|
+
}
|
|
655
|
+
if (current.length > 0) {
|
|
656
|
+
parts.push(current);
|
|
657
|
+
}
|
|
658
|
+
if (parts.length === 0) {
|
|
659
|
+
throw new Error("weacpx MCP command must not be empty");
|
|
660
|
+
}
|
|
661
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
662
|
+
}
|
|
663
|
+
function stringEnv(env) {
|
|
664
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
665
|
+
}
|
|
666
|
+
async function defaultQueueOwnerSpawner(command, args, options) {
|
|
667
|
+
await new Promise((resolve, reject) => {
|
|
668
|
+
const child = spawn3(command, args, {
|
|
669
|
+
detached: true,
|
|
670
|
+
stdio: "ignore",
|
|
671
|
+
env: options.env,
|
|
672
|
+
windowsHide: true
|
|
673
|
+
});
|
|
674
|
+
child.once("error", reject);
|
|
675
|
+
child.once("spawn", () => {
|
|
676
|
+
child.unref();
|
|
677
|
+
resolve();
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function createDefaultQueueOwnerTerminator(_acpxCommand) {
|
|
682
|
+
return async (sessionId) => {
|
|
683
|
+
await terminateAcpxQueueOwner(sessionId);
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
async function terminateAcpxQueueOwner(sessionId) {
|
|
687
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
688
|
+
let owner;
|
|
689
|
+
try {
|
|
690
|
+
owner = JSON.parse(await readFile(lockPath, "utf8"));
|
|
691
|
+
} catch {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
695
|
+
await terminateProcessTree(owner.pid);
|
|
696
|
+
}
|
|
697
|
+
await unlink(lockPath).catch(() => {});
|
|
698
|
+
}
|
|
699
|
+
function queueLockFilePath(sessionId) {
|
|
700
|
+
return join2(homedir2(), ".acpx", "queues", `${shortHash(sessionId, 24)}.lock`);
|
|
701
|
+
}
|
|
702
|
+
function shortHash(value, length) {
|
|
703
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
704
|
+
}
|
|
705
|
+
function resolveDefaultWeacpxCommand(env) {
|
|
706
|
+
if (env.WEACPX_CLI_COMMAND?.trim()) {
|
|
707
|
+
return env.WEACPX_CLI_COMMAND.trim();
|
|
708
|
+
}
|
|
709
|
+
if (env.WEACPX_DAEMON_ARG0?.trim()) {
|
|
710
|
+
return `${quoteCommandPart(process.execPath)} ${quoteCommandPart(env.WEACPX_DAEMON_ARG0.trim())}`;
|
|
711
|
+
}
|
|
712
|
+
if (process.argv[1]) {
|
|
713
|
+
return `${quoteCommandPart(process.execPath)} ${quoteCommandPart(process.argv[1])}`;
|
|
714
|
+
}
|
|
715
|
+
return "weacpx";
|
|
716
|
+
}
|
|
717
|
+
function quoteCommandPart(value) {
|
|
718
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
719
|
+
}
|
|
720
|
+
var init_acpx_queue_owner_launcher = __esm(() => {
|
|
721
|
+
init_spawn_command();
|
|
722
|
+
init_terminate_process_tree();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// src/transport/permission-mode-flag.ts
|
|
726
|
+
function permissionModeToFlag(permissionMode) {
|
|
727
|
+
switch (permissionMode) {
|
|
728
|
+
case "approve-reads":
|
|
729
|
+
return "--approve-reads";
|
|
730
|
+
case "deny-all":
|
|
731
|
+
return "--deny-all";
|
|
732
|
+
case "approve-all":
|
|
733
|
+
return "--approve-all";
|
|
734
|
+
}
|
|
735
|
+
}
|
|
264
736
|
|
|
265
737
|
// src/bridge/bridge-main.ts
|
|
266
738
|
import { createInterface } from "node:readline";
|
|
@@ -308,299 +780,206 @@ class BridgeRequestScheduler {
|
|
|
308
780
|
}
|
|
309
781
|
}
|
|
310
782
|
|
|
311
|
-
// src/bridge/bridge-
|
|
312
|
-
|
|
783
|
+
// src/bridge/bridge-runtime.ts
|
|
784
|
+
init_spawn_command();
|
|
785
|
+
init_prompt_output();
|
|
786
|
+
init_streaming_prompt();
|
|
787
|
+
import { copyFile, readdir } from "node:fs/promises";
|
|
788
|
+
import { homedir as homedir3 } from "node:os";
|
|
789
|
+
import { dirname as dirname2, join as join3, win32 } from "node:path";
|
|
790
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
791
|
+
|
|
792
|
+
// src/bridge/parse-missing-optional-dep.ts
|
|
793
|
+
var PATTERN = /You can try manually installing ["']([^"']+)["']/;
|
|
794
|
+
var VALID_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
795
|
+
function parseMissingOptionalDep(text) {
|
|
796
|
+
const match = PATTERN.exec(text);
|
|
797
|
+
if (!match || !match[1])
|
|
798
|
+
return null;
|
|
799
|
+
const pkg = match[1];
|
|
800
|
+
if (!VALID_NAME.test(pkg))
|
|
801
|
+
return null;
|
|
802
|
+
return { package: pkg };
|
|
313
803
|
}
|
|
314
|
-
var BRIDGE_METHODS = new Set([
|
|
315
|
-
"ping",
|
|
316
|
-
"shutdown",
|
|
317
|
-
"updatePermissionPolicy",
|
|
318
|
-
"hasSession",
|
|
319
|
-
"ensureSession",
|
|
320
|
-
"prompt",
|
|
321
|
-
"setMode",
|
|
322
|
-
"cancel"
|
|
323
|
-
]);
|
|
324
|
-
var SESSION_SCOPED_METHODS = new Set(["hasSession", "ensureSession", "prompt", "setMode", "cancel"]);
|
|
325
804
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
805
|
+
// src/bridge/bridge-runtime.ts
|
|
806
|
+
init_discover_parent_package_paths();
|
|
807
|
+
init_acpx_queue_owner_launcher();
|
|
808
|
+
class EnsureSessionFailedError extends Error {
|
|
809
|
+
kind;
|
|
810
|
+
data;
|
|
811
|
+
constructor(message, kind, data) {
|
|
812
|
+
super(message);
|
|
813
|
+
this.name = "EnsureSessionFailedError";
|
|
814
|
+
this.kind = kind;
|
|
815
|
+
this.data = data;
|
|
331
816
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
details: {
|
|
354
|
-
exitCode: error.exitCode,
|
|
355
|
-
stdout: error.stdout,
|
|
356
|
-
stderr: error.stderr
|
|
357
|
-
}
|
|
358
|
-
} : {}
|
|
359
|
-
}
|
|
360
|
-
})}
|
|
361
|
-
`;
|
|
362
|
-
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
class BridgeRuntime {
|
|
820
|
+
command;
|
|
821
|
+
run;
|
|
822
|
+
runSessionCreate;
|
|
823
|
+
options;
|
|
824
|
+
runPromptCommand;
|
|
825
|
+
repairSessionIndex;
|
|
826
|
+
queueOwnerLauncher;
|
|
827
|
+
acpxVerboseSupported = undefined;
|
|
828
|
+
constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner, repairSessionIndex = tryRepairAcpxSessionIndex, queueOwnerLauncher = new AcpxQueueOwnerLauncher({
|
|
829
|
+
acpxCommand: command
|
|
830
|
+
})) {
|
|
831
|
+
this.command = command;
|
|
832
|
+
this.run = run;
|
|
833
|
+
this.runSessionCreate = runSessionCreate;
|
|
834
|
+
this.options = options;
|
|
835
|
+
this.runPromptCommand = runPromptCommand;
|
|
836
|
+
this.repairSessionIndex = repairSessionIndex;
|
|
837
|
+
this.queueOwnerLauncher = queueOwnerLauncher;
|
|
363
838
|
}
|
|
364
|
-
async
|
|
365
|
-
|
|
366
|
-
|
|
839
|
+
async updatePermissionPolicy(policy) {
|
|
840
|
+
this.options.permissionMode = policy.permissionMode;
|
|
841
|
+
this.options.nonInteractivePermissions = policy.nonInteractivePermissions;
|
|
842
|
+
return {};
|
|
843
|
+
}
|
|
844
|
+
async hasSession(input) {
|
|
845
|
+
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
846
|
+
"sessions",
|
|
847
|
+
"show",
|
|
848
|
+
input.name
|
|
849
|
+
]));
|
|
850
|
+
const result = await this.run(spawnSpec.command, spawnSpec.args);
|
|
851
|
+
return { exists: result.code === 0 };
|
|
852
|
+
}
|
|
853
|
+
async ensureSession(input, onProgress) {
|
|
854
|
+
onProgress?.("spawn");
|
|
855
|
+
const onStderrLine = onProgress ? (line) => {
|
|
856
|
+
const trimmed = line.replace(/\r$/, "").trimEnd();
|
|
857
|
+
if (trimmed.length === 0)
|
|
858
|
+
return;
|
|
859
|
+
onProgress({ kind: "note", text: trimmed });
|
|
860
|
+
} : undefined;
|
|
861
|
+
const runWithVerboseFallback = async (tailArgs, runner) => {
|
|
862
|
+
const useVerbose = this.acpxVerboseSupported !== false;
|
|
863
|
+
const spec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: useVerbose }));
|
|
864
|
+
const result = await runner(spec.command, spec.args);
|
|
865
|
+
if (result.code === 0) {
|
|
866
|
+
if (useVerbose)
|
|
867
|
+
this.acpxVerboseSupported = true;
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
if (useVerbose && isUnknownVerboseOption(result.stderr, result.stdout)) {
|
|
871
|
+
this.acpxVerboseSupported = false;
|
|
872
|
+
const retrySpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: false }));
|
|
873
|
+
return await runner(retrySpec.command, retrySpec.args);
|
|
874
|
+
}
|
|
875
|
+
return result;
|
|
876
|
+
};
|
|
877
|
+
const ensured = await runWithVerboseFallback(["sessions", "ensure", "--name", input.name], (command, args) => this.run(command, args, { onStderrLine }));
|
|
878
|
+
if (ensured.code === 0) {
|
|
879
|
+
onProgress?.("ready");
|
|
880
|
+
return {};
|
|
367
881
|
}
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
882
|
+
const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
|
|
883
|
+
const existing = await this.run(existingSpec.command, existingSpec.args);
|
|
884
|
+
if (existing.code === 0) {
|
|
885
|
+
onProgress?.("ready");
|
|
886
|
+
return {};
|
|
371
887
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
888
|
+
onProgress?.("initializing");
|
|
889
|
+
const created = await runWithVerboseFallback(["sessions", "new", "--name", input.name], (command, args) => this.runSessionCreate(command, args, input.cwd, { onStderrLine }));
|
|
890
|
+
if (created.code === 0) {
|
|
891
|
+
onProgress?.("ready");
|
|
892
|
+
return {};
|
|
375
893
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
case "ping":
|
|
894
|
+
const output = created.stderr || created.stdout || "";
|
|
895
|
+
if (output.includes("EPERM") && await this.repairSessionIndex()) {
|
|
896
|
+
const repaired = await this.run(existingSpec.command, existingSpec.args);
|
|
897
|
+
if (repaired.code === 0) {
|
|
898
|
+
onProgress?.("ready");
|
|
382
899
|
return {};
|
|
383
|
-
|
|
384
|
-
return await this.runtime.shutdown();
|
|
385
|
-
case "updatePermissionPolicy":
|
|
386
|
-
return await this.runtime.updatePermissionPolicy({
|
|
387
|
-
permissionMode: requirePermissionMode(params, "permissionMode"),
|
|
388
|
-
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
|
|
389
|
-
});
|
|
390
|
-
case "hasSession":
|
|
391
|
-
return await this.runtime.hasSession({
|
|
392
|
-
agent: requireString(params, "agent"),
|
|
393
|
-
agentCommand: asOptionalString(params.agentCommand),
|
|
394
|
-
cwd: requireString(params, "cwd"),
|
|
395
|
-
name: requireString(params, "name")
|
|
396
|
-
});
|
|
397
|
-
case "ensureSession":
|
|
398
|
-
return await this.runtime.ensureSession({
|
|
399
|
-
agent: requireString(params, "agent"),
|
|
400
|
-
agentCommand: asOptionalString(params.agentCommand),
|
|
401
|
-
cwd: requireString(params, "cwd"),
|
|
402
|
-
name: requireString(params, "name")
|
|
403
|
-
});
|
|
404
|
-
case "prompt":
|
|
405
|
-
return await this.runtime.prompt({
|
|
406
|
-
agent: requireString(params, "agent"),
|
|
407
|
-
agentCommand: asOptionalString(params.agentCommand),
|
|
408
|
-
cwd: requireString(params, "cwd"),
|
|
409
|
-
name: requireString(params, "name"),
|
|
410
|
-
text: requireString(params, "text")
|
|
411
|
-
}, (event) => {
|
|
412
|
-
if (event.type === "prompt.segment") {
|
|
413
|
-
writeLine?.(encodeBridgePromptSegmentEvent({
|
|
414
|
-
id: requestId,
|
|
415
|
-
event: "prompt.segment",
|
|
416
|
-
text: event.text
|
|
417
|
-
}));
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
case "setMode":
|
|
421
|
-
return await this.runtime.setMode({
|
|
422
|
-
agent: requireString(params, "agent"),
|
|
423
|
-
agentCommand: asOptionalString(params.agentCommand),
|
|
424
|
-
cwd: requireString(params, "cwd"),
|
|
425
|
-
name: requireString(params, "name"),
|
|
426
|
-
modeId: requireString(params, "modeId")
|
|
427
|
-
});
|
|
428
|
-
case "cancel":
|
|
429
|
-
return await this.runtime.cancel({
|
|
430
|
-
agent: requireString(params, "agent"),
|
|
431
|
-
agentCommand: asOptionalString(params.agentCommand),
|
|
432
|
-
cwd: requireString(params, "cwd"),
|
|
433
|
-
name: requireString(params, "name")
|
|
434
|
-
});
|
|
435
|
-
default:
|
|
436
|
-
throw new Error(`unsupported bridge method: ${method}`);
|
|
900
|
+
}
|
|
437
901
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
902
|
+
const rawMessage = output || ensured.stderr || ensured.stdout || "failed to create session";
|
|
903
|
+
const parseInput = rawMessage.split(/\r\n|\r|\n/).filter((line) => !/^\s*\[acpx\]/.test(line)).join(`
|
|
904
|
+
`);
|
|
905
|
+
const parsed = parseMissingOptionalDep(parseInput);
|
|
906
|
+
if (parsed) {
|
|
907
|
+
const parentPackagePath = this.resolveParentPackagePath(input, parsed.package);
|
|
908
|
+
throw new EnsureSessionFailedError(rawMessage, "missing_optional_dep", {
|
|
909
|
+
package: parsed.package,
|
|
910
|
+
parentPackagePath
|
|
911
|
+
});
|
|
445
912
|
}
|
|
446
|
-
|
|
447
|
-
return typeof id === "string" && id.length > 0 ? id : "unknown";
|
|
448
|
-
} catch {
|
|
449
|
-
return "unknown";
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
function parseBridgeRequest(line) {
|
|
453
|
-
let raw;
|
|
454
|
-
try {
|
|
455
|
-
raw = JSON.parse(line);
|
|
456
|
-
} catch {
|
|
457
|
-
throw new BridgeInvalidRequestError("request must be valid JSON");
|
|
458
|
-
}
|
|
459
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
460
|
-
throw new BridgeInvalidRequestError("request must be a JSON object");
|
|
461
|
-
}
|
|
462
|
-
const request = raw;
|
|
463
|
-
const id = request.id;
|
|
464
|
-
const method = request.method;
|
|
465
|
-
const params = request.params;
|
|
466
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
467
|
-
throw new BridgeInvalidRequestError("id must be a non-empty string");
|
|
468
|
-
}
|
|
469
|
-
if (typeof method !== "string" || method.length === 0) {
|
|
470
|
-
throw new BridgeInvalidRequestError("method must be a non-empty string");
|
|
471
|
-
}
|
|
472
|
-
if (!BRIDGE_METHODS.has(method)) {
|
|
473
|
-
throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
|
|
474
|
-
}
|
|
475
|
-
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
476
|
-
throw new BridgeInvalidRequestError("params must be an object");
|
|
477
|
-
}
|
|
478
|
-
return {
|
|
479
|
-
id,
|
|
480
|
-
method,
|
|
481
|
-
params
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
function getSessionName(params) {
|
|
485
|
-
return asNonEmptyString(params.name);
|
|
486
|
-
}
|
|
487
|
-
function getSessionScheduleKey(params) {
|
|
488
|
-
const name = asNonEmptyString(params.name);
|
|
489
|
-
const cwd = asNonEmptyString(params.cwd);
|
|
490
|
-
const agentIdentity = asNonEmptyString(params.agentCommand) ?? asNonEmptyString(params.agent);
|
|
491
|
-
if (!name || !cwd || !agentIdentity) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
return JSON.stringify([agentIdentity, cwd, name]);
|
|
495
|
-
}
|
|
496
|
-
function asNonEmptyString(value) {
|
|
497
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
function requireString(params, key) {
|
|
503
|
-
const value = params[key];
|
|
504
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
505
|
-
throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
|
|
506
|
-
}
|
|
507
|
-
return value;
|
|
508
|
-
}
|
|
509
|
-
function requirePermissionMode(params, key) {
|
|
510
|
-
const value = params[key];
|
|
511
|
-
if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
|
|
512
|
-
return value;
|
|
913
|
+
throw new EnsureSessionFailedError(rawMessage, "generic");
|
|
513
914
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
915
|
+
resolveParentPackagePath(input, platformPackage) {
|
|
916
|
+
const guess = deriveParentPackageName(platformPackage);
|
|
917
|
+
const candidates = [input.agentCommand, input.agent, guess].filter((c) => Boolean(c));
|
|
918
|
+
for (const candidate of candidates) {
|
|
919
|
+
try {
|
|
920
|
+
const resolved = __require.resolve(`${candidate}/package.json`, {
|
|
921
|
+
paths: [process.cwd(), ...__require.resolve.paths(candidate) ?? []]
|
|
922
|
+
});
|
|
923
|
+
return dirname2(resolved);
|
|
924
|
+
} catch {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
526
929
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
class BridgeRuntime {
|
|
539
|
-
command;
|
|
540
|
-
run;
|
|
541
|
-
runSessionCreate;
|
|
542
|
-
options;
|
|
543
|
-
runPromptCommand;
|
|
544
|
-
constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner) {
|
|
545
|
-
this.command = command;
|
|
546
|
-
this.run = run;
|
|
547
|
-
this.runSessionCreate = runSessionCreate;
|
|
548
|
-
this.options = options;
|
|
549
|
-
this.runPromptCommand = runPromptCommand;
|
|
930
|
+
async prompt(input, onEvent) {
|
|
931
|
+
await this.launchMcpQueueOwnerIfNeeded(input);
|
|
932
|
+
const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
|
|
933
|
+
"prompt",
|
|
934
|
+
"-s",
|
|
935
|
+
input.name,
|
|
936
|
+
input.text
|
|
937
|
+
]));
|
|
938
|
+
const formatToolCalls = input.replyMode === "verbose";
|
|
939
|
+
const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent, { formatToolCalls }) : await this.run(spawnSpec.command, spawnSpec.args);
|
|
940
|
+
return { text: getPromptText(result) };
|
|
550
941
|
}
|
|
551
|
-
async
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
942
|
+
async launchMcpQueueOwnerIfNeeded(input) {
|
|
943
|
+
if (!input.mcpCoordinatorSession) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const record = await this.readSessionRecord(input);
|
|
947
|
+
await this.queueOwnerLauncher.launch({
|
|
948
|
+
acpxRecordId: record.acpxRecordId,
|
|
949
|
+
coordinatorSession: input.mcpCoordinatorSession,
|
|
950
|
+
...input.mcpSourceHandle ? { sourceHandle: input.mcpSourceHandle } : {},
|
|
951
|
+
permissionMode: this.options.permissionMode ?? "approve-all",
|
|
952
|
+
nonInteractivePermissions: this.options.nonInteractivePermissions ?? "deny"
|
|
953
|
+
});
|
|
555
954
|
}
|
|
556
|
-
async
|
|
955
|
+
async readSessionRecord(input) {
|
|
557
956
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
558
957
|
"sessions",
|
|
559
958
|
"show",
|
|
560
959
|
input.name
|
|
561
960
|
]));
|
|
562
961
|
const result = await this.run(spawnSpec.command, spawnSpec.args);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
async ensureSession(input) {
|
|
566
|
-
const ensuredSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
567
|
-
"sessions",
|
|
568
|
-
"ensure",
|
|
569
|
-
"--name",
|
|
570
|
-
input.name
|
|
571
|
-
]));
|
|
572
|
-
const ensured = await this.run(ensuredSpec.command, ensuredSpec.args);
|
|
573
|
-
if (ensured.code === 0) {
|
|
574
|
-
return {};
|
|
575
|
-
}
|
|
576
|
-
const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
|
|
577
|
-
const existing = await this.run(existingSpec.command, existingSpec.args);
|
|
578
|
-
if (existing.code === 0) {
|
|
579
|
-
return {};
|
|
580
|
-
}
|
|
581
|
-
const createSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]));
|
|
582
|
-
const created = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
|
|
583
|
-
if (created.code === 0) {
|
|
584
|
-
return {};
|
|
962
|
+
if (result.code !== 0) {
|
|
963
|
+
throw new Error(result.stderr || result.stdout || "sessions show failed");
|
|
585
964
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (
|
|
590
|
-
|
|
965
|
+
try {
|
|
966
|
+
const parsed = JSON.parse(result.stdout);
|
|
967
|
+
let acpxRecordId;
|
|
968
|
+
if (typeof parsed.acpxRecordId === "string") {
|
|
969
|
+
acpxRecordId = parsed.acpxRecordId;
|
|
970
|
+
} else if (typeof parsed.id === "string") {
|
|
971
|
+
acpxRecordId = parsed.id;
|
|
972
|
+
}
|
|
973
|
+
if (acpxRecordId) {
|
|
974
|
+
return { acpxRecordId };
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
const firstLine = result.stdout.trim().split(/\r?\n/, 1)[0];
|
|
978
|
+
if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
|
|
979
|
+
return { acpxRecordId: firstLine };
|
|
591
980
|
}
|
|
592
981
|
}
|
|
593
|
-
throw new Error(
|
|
594
|
-
}
|
|
595
|
-
async prompt(input, onEvent) {
|
|
596
|
-
const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
|
|
597
|
-
"prompt",
|
|
598
|
-
"-s",
|
|
599
|
-
input.name,
|
|
600
|
-
input.text
|
|
601
|
-
]));
|
|
602
|
-
const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent) : await this.run(spawnSpec.command, spawnSpec.args);
|
|
603
|
-
return { text: getPromptText(result) };
|
|
982
|
+
throw new Error("failed to resolve acpx session record id");
|
|
604
983
|
}
|
|
605
984
|
async setMode(input) {
|
|
606
985
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
@@ -630,10 +1009,25 @@ class BridgeRuntime {
|
|
|
630
1009
|
message: result.stdout.trim()
|
|
631
1010
|
};
|
|
632
1011
|
}
|
|
1012
|
+
async removeSession(input) {
|
|
1013
|
+
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
1014
|
+
"sessions",
|
|
1015
|
+
"close",
|
|
1016
|
+
input.name
|
|
1017
|
+
]));
|
|
1018
|
+
const result = await this.run(spawnSpec.command, spawnSpec.args);
|
|
1019
|
+
if (result.code === 0) {
|
|
1020
|
+
return {};
|
|
1021
|
+
}
|
|
1022
|
+
if (isMissingBridgeSessionError(result.stderr, result.stdout)) {
|
|
1023
|
+
return {};
|
|
1024
|
+
}
|
|
1025
|
+
throw new Error(result.stderr || result.stdout || "sessions close failed");
|
|
1026
|
+
}
|
|
633
1027
|
async shutdown() {
|
|
634
1028
|
return {};
|
|
635
1029
|
}
|
|
636
|
-
buildSessionArgs(input, tail) {
|
|
1030
|
+
buildSessionArgs(input, tail, options = {}) {
|
|
637
1031
|
const prefix = [
|
|
638
1032
|
"--format",
|
|
639
1033
|
"quiet",
|
|
@@ -641,6 +1035,9 @@ class BridgeRuntime {
|
|
|
641
1035
|
input.cwd,
|
|
642
1036
|
...this.buildPermissionArgs()
|
|
643
1037
|
];
|
|
1038
|
+
if (options.verbose) {
|
|
1039
|
+
prefix.push("--verbose");
|
|
1040
|
+
}
|
|
644
1041
|
if (input.agentCommand) {
|
|
645
1042
|
return [...prefix, "--agent", input.agentCommand, ...tail];
|
|
646
1043
|
}
|
|
@@ -663,29 +1060,47 @@ class BridgeRuntime {
|
|
|
663
1060
|
buildPermissionArgs() {
|
|
664
1061
|
const permissionMode = this.options.permissionMode ?? "approve-all";
|
|
665
1062
|
const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
|
|
666
|
-
const modeFlag = permissionMode
|
|
1063
|
+
const modeFlag = permissionModeToFlag(permissionMode);
|
|
667
1064
|
return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
|
|
668
1065
|
}
|
|
669
1066
|
}
|
|
670
|
-
|
|
671
|
-
return
|
|
672
|
-
const child =
|
|
1067
|
+
function spawnCapture(command, args, options) {
|
|
1068
|
+
return new Promise((resolve, reject) => {
|
|
1069
|
+
const child = spawn4(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1070
|
+
child.stdout.setEncoding("utf8");
|
|
1071
|
+
child.stderr.setEncoding("utf8");
|
|
673
1072
|
let stdout = "";
|
|
674
1073
|
let stderr = "";
|
|
1074
|
+
let stderrTail = "";
|
|
675
1075
|
child.stdout.on("data", (chunk) => {
|
|
676
1076
|
stdout += String(chunk);
|
|
677
1077
|
});
|
|
678
1078
|
child.stderr.on("data", (chunk) => {
|
|
679
|
-
|
|
1079
|
+
const text = String(chunk);
|
|
1080
|
+
stderr += text;
|
|
1081
|
+
if (!options?.onStderrLine)
|
|
1082
|
+
return;
|
|
1083
|
+
stderrTail += text;
|
|
1084
|
+
const matches = stderrTail.split(/\r\n|\r|\n/);
|
|
1085
|
+
stderrTail = matches.pop() ?? "";
|
|
1086
|
+
for (const line of matches) {
|
|
1087
|
+
options.onStderrLine(line);
|
|
1088
|
+
}
|
|
680
1089
|
});
|
|
681
1090
|
child.on("error", reject);
|
|
682
1091
|
child.on("close", (code) => {
|
|
1092
|
+
if (options?.onStderrLine && stderrTail.length > 0) {
|
|
1093
|
+
options.onStderrLine(stderrTail);
|
|
1094
|
+
}
|
|
683
1095
|
resolve({ code: code ?? 1, stdout, stderr });
|
|
684
1096
|
});
|
|
685
1097
|
});
|
|
686
1098
|
}
|
|
1099
|
+
async function defaultRunner(command, args, options) {
|
|
1100
|
+
return await spawnCapture(command, args, options);
|
|
1101
|
+
}
|
|
687
1102
|
async function runStreamingPrompt(command, args, onEvent, options = {}) {
|
|
688
|
-
const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) =>
|
|
1103
|
+
const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) => spawn4(spawnCommand, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
|
|
689
1104
|
const setIntervalFn = options.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
|
|
690
1105
|
const clearIntervalFn = options.clearIntervalFn ?? ((timer) => clearInterval(timer));
|
|
691
1106
|
const maxSegmentWaitMs = options.maxSegmentWaitMs ?? 30000;
|
|
@@ -695,7 +1110,7 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
|
|
|
695
1110
|
const child = spawnPrompt(command, args);
|
|
696
1111
|
let stdout = "";
|
|
697
1112
|
let stderr = "";
|
|
698
|
-
const state = createStreamingPromptState();
|
|
1113
|
+
const state = createStreamingPromptState(options.formatToolCalls ?? false);
|
|
699
1114
|
let lastReplyAt = now();
|
|
700
1115
|
const flushBuffer = () => {
|
|
701
1116
|
const remaining = state.buffer.trim();
|
|
@@ -737,68 +1152,326 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
|
|
|
737
1152
|
});
|
|
738
1153
|
});
|
|
739
1154
|
}
|
|
740
|
-
async function defaultPromptRunner(command, args, onEvent) {
|
|
741
|
-
return await runStreamingPrompt(command, args, onEvent);
|
|
1155
|
+
async function defaultPromptRunner(command, args, onEvent, options) {
|
|
1156
|
+
return await runStreamingPrompt(command, args, onEvent, options);
|
|
742
1157
|
}
|
|
743
|
-
async function shellSessionCreateRunner(command, args, cwd) {
|
|
744
|
-
return await
|
|
745
|
-
const child = spawn(command, args, {
|
|
746
|
-
cwd,
|
|
747
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
748
|
-
});
|
|
749
|
-
let stdout = "";
|
|
750
|
-
let stderr = "";
|
|
751
|
-
child.stdout.on("data", (chunk) => {
|
|
752
|
-
stdout += String(chunk);
|
|
753
|
-
});
|
|
754
|
-
child.stderr.on("data", (chunk) => {
|
|
755
|
-
stderr += String(chunk);
|
|
756
|
-
});
|
|
757
|
-
child.on("error", reject);
|
|
758
|
-
child.on("close", (code) => {
|
|
759
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
760
|
-
});
|
|
761
|
-
});
|
|
1158
|
+
async function shellSessionCreateRunner(command, args, cwd, options) {
|
|
1159
|
+
return await spawnCapture(command, args, { cwd, onStderrLine: options?.onStderrLine });
|
|
762
1160
|
}
|
|
763
|
-
|
|
764
|
-
|
|
1161
|
+
function selectLatestAcpxSessionIndexTmp(files) {
|
|
1162
|
+
let latestTmp = null;
|
|
1163
|
+
let latestTime = 0;
|
|
1164
|
+
for (const file of files) {
|
|
1165
|
+
const match = file.match(/^index\.json\.\d+\.(\d+)\.tmp$/);
|
|
1166
|
+
if (!match) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const timestamp = Number(match[1]);
|
|
1170
|
+
if (timestamp > latestTime) {
|
|
1171
|
+
latestTime = timestamp;
|
|
1172
|
+
latestTmp = file;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return latestTmp;
|
|
1176
|
+
}
|
|
1177
|
+
async function tryRepairAcpxSessionIndex(deps = {}) {
|
|
1178
|
+
const platform = deps.platform ?? process.platform;
|
|
1179
|
+
if (platform !== "win32") {
|
|
765
1180
|
return false;
|
|
766
1181
|
}
|
|
767
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ??
|
|
1182
|
+
const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir3();
|
|
768
1183
|
if (!home) {
|
|
769
1184
|
return false;
|
|
770
1185
|
}
|
|
771
|
-
const
|
|
772
|
-
const
|
|
1186
|
+
const pathJoin = platform === "win32" ? win32.join : join3;
|
|
1187
|
+
const sessionsDir = pathJoin(home, ".acpx", "sessions");
|
|
1188
|
+
const indexPath = pathJoin(sessionsDir, "index.json");
|
|
1189
|
+
const readdirFn = deps.readdirFn ?? readdir;
|
|
1190
|
+
const copyFileFn = deps.copyFileFn ?? copyFile;
|
|
773
1191
|
let files;
|
|
774
1192
|
try {
|
|
775
|
-
files = await
|
|
1193
|
+
files = await readdirFn(sessionsDir);
|
|
776
1194
|
} catch {
|
|
777
1195
|
return false;
|
|
778
1196
|
}
|
|
779
|
-
const
|
|
780
|
-
if (tmpFiles.length === 0) {
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
|
-
let latestTmp = "";
|
|
784
|
-
let latestTime = 0;
|
|
785
|
-
for (const f of tmpFiles) {
|
|
786
|
-
const match = f.match(/^index\.json\.\d+\.(\d+)\.tmp$/);
|
|
787
|
-
if (match && Number(match[1]) > latestTime) {
|
|
788
|
-
latestTime = Number(match[1]);
|
|
789
|
-
latestTmp = f;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
1197
|
+
const latestTmp = selectLatestAcpxSessionIndexTmp(files);
|
|
792
1198
|
if (!latestTmp) {
|
|
793
1199
|
return false;
|
|
794
1200
|
}
|
|
795
1201
|
try {
|
|
796
|
-
await
|
|
1202
|
+
await copyFileFn(pathJoin(sessionsDir, latestTmp), indexPath);
|
|
797
1203
|
return true;
|
|
798
1204
|
} catch {
|
|
799
1205
|
return false;
|
|
800
1206
|
}
|
|
801
1207
|
}
|
|
1208
|
+
function isUnknownVerboseOption(stderr, stdout) {
|
|
1209
|
+
const combined = `${stderr}
|
|
1210
|
+
${stdout}`;
|
|
1211
|
+
return /(unknown|unrecognized)\b[^\n]*--verbose/i.test(combined);
|
|
1212
|
+
}
|
|
1213
|
+
function isMissingBridgeSessionError(stderr, stdout) {
|
|
1214
|
+
const combined = `${stderr}
|
|
1215
|
+
${stdout}`.toLowerCase();
|
|
1216
|
+
return combined.includes("no named session") || combined.includes("no cwd session") || combined.includes("session not found") || combined.includes("unknown session") || combined.includes("no acpx session found");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/bridge/bridge-server.ts
|
|
1220
|
+
class BridgeInvalidRequestError extends Error {
|
|
1221
|
+
}
|
|
1222
|
+
var BRIDGE_METHODS = new Set([
|
|
1223
|
+
"ping",
|
|
1224
|
+
"shutdown",
|
|
1225
|
+
"updatePermissionPolicy",
|
|
1226
|
+
"hasSession",
|
|
1227
|
+
"ensureSession",
|
|
1228
|
+
"prompt",
|
|
1229
|
+
"setMode",
|
|
1230
|
+
"cancel",
|
|
1231
|
+
"removeSession"
|
|
1232
|
+
]);
|
|
1233
|
+
var SESSION_SCOPED_METHODS = new Set([
|
|
1234
|
+
"hasSession",
|
|
1235
|
+
"ensureSession",
|
|
1236
|
+
"prompt",
|
|
1237
|
+
"setMode",
|
|
1238
|
+
"cancel",
|
|
1239
|
+
"removeSession"
|
|
1240
|
+
]);
|
|
1241
|
+
|
|
1242
|
+
class BridgeServer {
|
|
1243
|
+
runtime;
|
|
1244
|
+
scheduler = new BridgeRequestScheduler;
|
|
1245
|
+
constructor(runtime) {
|
|
1246
|
+
this.runtime = runtime;
|
|
1247
|
+
}
|
|
1248
|
+
async handleLine(line, writeLine) {
|
|
1249
|
+
let requestId = extractRequestId(line);
|
|
1250
|
+
try {
|
|
1251
|
+
const request = parseBridgeRequest(line);
|
|
1252
|
+
requestId = request.id;
|
|
1253
|
+
const result = await this.dispatchRequest(request.id, request.method, request.params, writeLine);
|
|
1254
|
+
return `${JSON.stringify({
|
|
1255
|
+
id: request.id,
|
|
1256
|
+
ok: true,
|
|
1257
|
+
result
|
|
1258
|
+
})}
|
|
1259
|
+
`;
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1262
|
+
const ensureSessionFields = error instanceof EnsureSessionFailedError ? { kind: error.kind, ...error.data ? { data: error.data } : {} } : {};
|
|
1263
|
+
const promptDetails = error instanceof PromptCommandError ? { details: { exitCode: error.exitCode, stdout: error.stdout, stderr: error.stderr } } : {};
|
|
1264
|
+
return `${JSON.stringify({
|
|
1265
|
+
id: requestId,
|
|
1266
|
+
ok: false,
|
|
1267
|
+
error: {
|
|
1268
|
+
code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
|
|
1269
|
+
message,
|
|
1270
|
+
...ensureSessionFields,
|
|
1271
|
+
...promptDetails
|
|
1272
|
+
}
|
|
1273
|
+
})}
|
|
1274
|
+
`;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async dispatchRequest(requestId, method, params, writeLine) {
|
|
1278
|
+
if (!SESSION_SCOPED_METHODS.has(method)) {
|
|
1279
|
+
return await this.dispatch(requestId, method, params, writeLine);
|
|
1280
|
+
}
|
|
1281
|
+
const sessionName = getSessionName(params);
|
|
1282
|
+
if (!sessionName) {
|
|
1283
|
+
return await this.dispatch(requestId, method, params, writeLine);
|
|
1284
|
+
}
|
|
1285
|
+
const sessionKey = getSessionScheduleKey(params);
|
|
1286
|
+
if (!sessionKey) {
|
|
1287
|
+
return await this.dispatch(requestId, method, params, writeLine);
|
|
1288
|
+
}
|
|
1289
|
+
const lane = method === "cancel" ? "control" : "normal";
|
|
1290
|
+
return await this.scheduler.run(sessionKey, lane, () => this.dispatch(requestId, method, params, writeLine));
|
|
1291
|
+
}
|
|
1292
|
+
async dispatch(requestId, method, params, writeLine) {
|
|
1293
|
+
switch (method) {
|
|
1294
|
+
case "ping":
|
|
1295
|
+
return {};
|
|
1296
|
+
case "shutdown":
|
|
1297
|
+
return await this.runtime.shutdown();
|
|
1298
|
+
case "updatePermissionPolicy":
|
|
1299
|
+
return await this.runtime.updatePermissionPolicy({
|
|
1300
|
+
permissionMode: requirePermissionMode(params, "permissionMode"),
|
|
1301
|
+
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
|
|
1302
|
+
});
|
|
1303
|
+
case "hasSession":
|
|
1304
|
+
return await this.runtime.hasSession({
|
|
1305
|
+
agent: requireString(params, "agent"),
|
|
1306
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1307
|
+
cwd: requireString(params, "cwd"),
|
|
1308
|
+
name: requireString(params, "name")
|
|
1309
|
+
});
|
|
1310
|
+
case "ensureSession":
|
|
1311
|
+
return await this.runtime.ensureSession({
|
|
1312
|
+
agent: requireString(params, "agent"),
|
|
1313
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1314
|
+
cwd: requireString(params, "cwd"),
|
|
1315
|
+
name: requireString(params, "name"),
|
|
1316
|
+
mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
|
|
1317
|
+
mcpSourceHandle: asOptionalString(params.mcpSourceHandle)
|
|
1318
|
+
}, (progress) => {
|
|
1319
|
+
if (typeof progress === "string") {
|
|
1320
|
+
writeLine?.(encodeBridgeSessionProgressEvent({
|
|
1321
|
+
id: requestId,
|
|
1322
|
+
event: "session.progress",
|
|
1323
|
+
stage: progress
|
|
1324
|
+
}));
|
|
1325
|
+
} else if (progress.kind === "note") {
|
|
1326
|
+
writeLine?.(encodeBridgeSessionNoteEvent({
|
|
1327
|
+
id: requestId,
|
|
1328
|
+
event: "session.note",
|
|
1329
|
+
text: progress.text
|
|
1330
|
+
}));
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
case "prompt":
|
|
1334
|
+
return await this.runtime.prompt({
|
|
1335
|
+
agent: requireString(params, "agent"),
|
|
1336
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1337
|
+
cwd: requireString(params, "cwd"),
|
|
1338
|
+
name: requireString(params, "name"),
|
|
1339
|
+
mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
|
|
1340
|
+
mcpSourceHandle: asOptionalString(params.mcpSourceHandle),
|
|
1341
|
+
text: requireString(params, "text"),
|
|
1342
|
+
replyMode: asOptionalReplyMode(params.replyMode)
|
|
1343
|
+
}, (event) => {
|
|
1344
|
+
if (event.type === "prompt.segment") {
|
|
1345
|
+
writeLine?.(encodeBridgePromptSegmentEvent({
|
|
1346
|
+
id: requestId,
|
|
1347
|
+
event: "prompt.segment",
|
|
1348
|
+
text: event.text
|
|
1349
|
+
}));
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
case "setMode":
|
|
1353
|
+
return await this.runtime.setMode({
|
|
1354
|
+
agent: requireString(params, "agent"),
|
|
1355
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1356
|
+
cwd: requireString(params, "cwd"),
|
|
1357
|
+
name: requireString(params, "name"),
|
|
1358
|
+
modeId: requireString(params, "modeId")
|
|
1359
|
+
});
|
|
1360
|
+
case "cancel":
|
|
1361
|
+
return await this.runtime.cancel({
|
|
1362
|
+
agent: requireString(params, "agent"),
|
|
1363
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1364
|
+
cwd: requireString(params, "cwd"),
|
|
1365
|
+
name: requireString(params, "name")
|
|
1366
|
+
});
|
|
1367
|
+
case "removeSession":
|
|
1368
|
+
return await this.runtime.removeSession({
|
|
1369
|
+
agent: requireString(params, "agent"),
|
|
1370
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
1371
|
+
cwd: requireString(params, "cwd"),
|
|
1372
|
+
name: requireString(params, "name")
|
|
1373
|
+
});
|
|
1374
|
+
default:
|
|
1375
|
+
throw new Error(`unsupported bridge method: ${method}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
function extractRequestId(line) {
|
|
1380
|
+
try {
|
|
1381
|
+
const raw = JSON.parse(line);
|
|
1382
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1383
|
+
return "unknown";
|
|
1384
|
+
}
|
|
1385
|
+
const id = raw.id;
|
|
1386
|
+
return typeof id === "string" && id.length > 0 ? id : "unknown";
|
|
1387
|
+
} catch {
|
|
1388
|
+
return "unknown";
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function parseBridgeRequest(line) {
|
|
1392
|
+
let raw;
|
|
1393
|
+
try {
|
|
1394
|
+
raw = JSON.parse(line);
|
|
1395
|
+
} catch {
|
|
1396
|
+
throw new BridgeInvalidRequestError("request must be valid JSON");
|
|
1397
|
+
}
|
|
1398
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1399
|
+
throw new BridgeInvalidRequestError("request must be a JSON object");
|
|
1400
|
+
}
|
|
1401
|
+
const request = raw;
|
|
1402
|
+
const id = request.id;
|
|
1403
|
+
const method = request.method;
|
|
1404
|
+
const params = request.params;
|
|
1405
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1406
|
+
throw new BridgeInvalidRequestError("id must be a non-empty string");
|
|
1407
|
+
}
|
|
1408
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
1409
|
+
throw new BridgeInvalidRequestError("method must be a non-empty string");
|
|
1410
|
+
}
|
|
1411
|
+
if (!BRIDGE_METHODS.has(method)) {
|
|
1412
|
+
throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
|
|
1413
|
+
}
|
|
1414
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
1415
|
+
throw new BridgeInvalidRequestError("params must be an object");
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
id,
|
|
1419
|
+
method,
|
|
1420
|
+
params
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function getSessionName(params) {
|
|
1424
|
+
return asNonEmptyString(params.name);
|
|
1425
|
+
}
|
|
1426
|
+
function getSessionScheduleKey(params) {
|
|
1427
|
+
const name = asNonEmptyString(params.name);
|
|
1428
|
+
const cwd = asNonEmptyString(params.cwd);
|
|
1429
|
+
const agentIdentity = asNonEmptyString(params.agentCommand) ?? asNonEmptyString(params.agent);
|
|
1430
|
+
if (!name || !cwd || !agentIdentity) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
return JSON.stringify([agentIdentity, cwd, name]);
|
|
1434
|
+
}
|
|
1435
|
+
function asNonEmptyString(value) {
|
|
1436
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
return value;
|
|
1440
|
+
}
|
|
1441
|
+
function requireString(params, key) {
|
|
1442
|
+
const value = params[key];
|
|
1443
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1444
|
+
throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
|
|
1445
|
+
}
|
|
1446
|
+
return value;
|
|
1447
|
+
}
|
|
1448
|
+
function requirePermissionMode(params, key) {
|
|
1449
|
+
const value = params[key];
|
|
1450
|
+
if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
|
|
1451
|
+
return value;
|
|
1452
|
+
}
|
|
1453
|
+
throw new BridgeInvalidRequestError(`${key} must be approve-all, approve-reads, or deny-all`);
|
|
1454
|
+
}
|
|
1455
|
+
function requireNonInteractivePermissions(params, key) {
|
|
1456
|
+
const value = params[key];
|
|
1457
|
+
if (value === "deny" || value === "fail") {
|
|
1458
|
+
return value;
|
|
1459
|
+
}
|
|
1460
|
+
throw new BridgeInvalidRequestError(`${key} must be deny or fail`);
|
|
1461
|
+
}
|
|
1462
|
+
function asOptionalString(value) {
|
|
1463
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
return value;
|
|
1467
|
+
}
|
|
1468
|
+
var VALID_REPLY_MODES = new Set(["stream", "final", "verbose"]);
|
|
1469
|
+
function asOptionalReplyMode(value) {
|
|
1470
|
+
if (typeof value !== "string" || !VALID_REPLY_MODES.has(value)) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
return value;
|
|
1474
|
+
}
|
|
802
1475
|
|
|
803
1476
|
// src/bridge/bridge-main.ts
|
|
804
1477
|
async function processBridgeInput(options) {
|