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