@agentproto/cli 0.1.0-alpha.6 → 0.1.0-alpha.9
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 +280 -17
- package/dist/cli.mjs +9312 -4132
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1932 -149
- package/dist/index.mjs.map +1 -1
- package/dist/registry/builtins.d.ts +14 -0
- package/dist/registry/builtins.mjs +329 -0
- package/dist/registry/builtins.mjs.map +1 -0
- package/dist/registry/manifest.d.ts +88 -0
- package/dist/registry/manifest.mjs +42 -0
- package/dist/registry/manifest.mjs.map +1 -0
- package/dist/registry/plugins.d.ts +23 -0
- package/dist/registry/plugins.mjs +201 -0
- package/dist/registry/plugins.mjs.map +1 -0
- package/dist/registry/runtime.d.ts +70 -0
- package/dist/registry/runtime.mjs +52 -0
- package/dist/registry/runtime.mjs.map +1 -0
- package/dist/util/credentials.d.ts +71 -0
- package/dist/util/credentials.mjs +88 -0
- package/dist/util/credentials.mjs.map +1 -0
- package/package.json +36 -5
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as childProc from 'child_process';
|
|
1
2
|
import { execFile, spawn } from 'child_process';
|
|
2
3
|
import { createHash, randomUUID, randomBytes } from 'crypto';
|
|
3
|
-
import { mkdtemp, writeFile, chmod, rm, mkdir, readFile, readdir, stat } from 'fs/promises';
|
|
4
|
+
import { mkdtemp, writeFile, chmod, rm, mkdir, readFile, unlink, readdir, stat } from 'fs/promises';
|
|
4
5
|
import { tmpdir, userInfo, hostname, homedir, platform } from 'os';
|
|
5
6
|
import { resolve, join, dirname, isAbsolute, normalize, relative, basename } from 'path';
|
|
6
7
|
import { promisify, parseArgs } from 'util';
|
|
7
|
-
import { promises, existsSync } from 'fs';
|
|
8
|
+
import { promises, existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
8
9
|
import { createInterface } from 'readline/promises';
|
|
9
10
|
import { stdout, stdin } from 'process';
|
|
10
11
|
import { createAgentCliRuntime } from '@agentproto/driver-agent-cli';
|
|
@@ -16,11 +17,11 @@ import matter from 'gray-matter';
|
|
|
16
17
|
import { EventEmitter } from 'events';
|
|
17
18
|
import { createServer } from 'http';
|
|
18
19
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
20
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
19
21
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
20
22
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
21
23
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
22
24
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
23
|
-
import WebSocket from 'ws';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @agentproto/cli v0.1.0-alpha
|
|
@@ -84,9 +85,12 @@ async function listInstalledAdapters(opts) {
|
|
|
84
85
|
};
|
|
85
86
|
out.push(info);
|
|
86
87
|
} catch (err) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
if (/Cannot find package|ERR_MODULE_NOT_FOUND/i.test(msg) && msg.includes(`@agentproto/adapter-${slug}`)) ; else {
|
|
90
|
+
console.warn(
|
|
91
|
+
`[agentproto/cli] listInstalledAdapters: skipping broken adapter '${slug}': ${msg}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -102,8 +106,8 @@ async function collectAgentprotoNamespaceRoots(start) {
|
|
|
102
106
|
for (let depth = 0; depth < 16; depth++) {
|
|
103
107
|
for (const candidate of candidatesAt(cur)) {
|
|
104
108
|
try {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
109
|
+
const stat3 = await promises.stat(candidate);
|
|
110
|
+
if (stat3.isDirectory()) roots.push(candidate);
|
|
107
111
|
} catch {
|
|
108
112
|
}
|
|
109
113
|
}
|
|
@@ -259,10 +263,10 @@ async function runExternalStep(step, ledger, head) {
|
|
|
259
263
|
`);
|
|
260
264
|
const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
261
265
|
const args = platform() === "win32" ? ["/c", "start", step.url] : [step.url];
|
|
262
|
-
await new Promise((
|
|
266
|
+
await new Promise((resolve4) => {
|
|
263
267
|
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
264
|
-
child.once("error", () =>
|
|
265
|
-
child.once("spawn", () =>
|
|
268
|
+
child.once("error", () => resolve4());
|
|
269
|
+
child.once("spawn", () => resolve4());
|
|
266
270
|
});
|
|
267
271
|
let value = "";
|
|
268
272
|
if (step.callback?.param) {
|
|
@@ -335,7 +339,7 @@ async function promptString(question, opts) {
|
|
|
335
339
|
process.stdin.setRawMode?.(true);
|
|
336
340
|
let buf = "";
|
|
337
341
|
process.stdout.write(prompt);
|
|
338
|
-
return new Promise((
|
|
342
|
+
return new Promise((resolve4) => {
|
|
339
343
|
const onData = (chunk) => {
|
|
340
344
|
for (const code of chunk) {
|
|
341
345
|
if (code === 13 || code === 10) {
|
|
@@ -343,7 +347,7 @@ async function promptString(question, opts) {
|
|
|
343
347
|
process.stdin.setRawMode?.(false);
|
|
344
348
|
process.stdout.write("\n");
|
|
345
349
|
rl.close();
|
|
346
|
-
|
|
350
|
+
resolve4(buf || opts.defaultValue || "");
|
|
347
351
|
return;
|
|
348
352
|
}
|
|
349
353
|
if (code === 3) {
|
|
@@ -425,7 +429,7 @@ async function resolveSelectOptions(options) {
|
|
|
425
429
|
});
|
|
426
430
|
}
|
|
427
431
|
async function runShellCapturing(cmd, opts) {
|
|
428
|
-
return new Promise((
|
|
432
|
+
return new Promise((resolve4) => {
|
|
429
433
|
const child = spawn("bash", ["-lc", cmd], {
|
|
430
434
|
stdio: opts.interactive ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
431
435
|
});
|
|
@@ -443,11 +447,11 @@ async function runShellCapturing(cmd, opts) {
|
|
|
443
447
|
}, opts.timeoutMs);
|
|
444
448
|
child.once("error", () => {
|
|
445
449
|
clearTimeout(timer);
|
|
446
|
-
|
|
450
|
+
resolve4({ exitCode: 127, stdout, stderr: stderr || "spawn error" });
|
|
447
451
|
});
|
|
448
452
|
child.once("exit", (code) => {
|
|
449
453
|
clearTimeout(timer);
|
|
450
|
-
|
|
454
|
+
resolve4({ exitCode: code ?? 0, stdout, stderr });
|
|
451
455
|
});
|
|
452
456
|
});
|
|
453
457
|
}
|
|
@@ -481,9 +485,315 @@ async function saveLedger(path, ledger) {
|
|
|
481
485
|
await chmod(path, 384).catch(() => {
|
|
482
486
|
});
|
|
483
487
|
}
|
|
488
|
+
async function runInstallProfile(slug, args) {
|
|
489
|
+
const { values } = parseArgs({
|
|
490
|
+
args: [...args],
|
|
491
|
+
allowPositionals: true,
|
|
492
|
+
strict: true,
|
|
493
|
+
options: {
|
|
494
|
+
force: { type: "boolean", short: "f" },
|
|
495
|
+
"dry-run": { type: "boolean" },
|
|
496
|
+
"skip-setup": { type: "boolean" },
|
|
497
|
+
cwd: { type: "string" },
|
|
498
|
+
package: { type: "string" }
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
if (!slug.startsWith("runtime-profile/")) {
|
|
502
|
+
process.stderr.write(
|
|
503
|
+
`agentproto install: expected slug starting with 'runtime-profile/', got '${slug}'.
|
|
504
|
+
`
|
|
505
|
+
);
|
|
506
|
+
return 2;
|
|
507
|
+
}
|
|
508
|
+
const profileName = slug.slice("runtime-profile/".length);
|
|
509
|
+
if (!/^[a-z][a-z0-9-]*$/.test(profileName)) {
|
|
510
|
+
process.stderr.write(
|
|
511
|
+
`agentproto install: invalid profile name '${profileName}'. Lower-kebab only.
|
|
512
|
+
`
|
|
513
|
+
);
|
|
514
|
+
return 2;
|
|
515
|
+
}
|
|
516
|
+
let packageName;
|
|
517
|
+
if (typeof values.package === "string" && values.package) {
|
|
518
|
+
packageName = values.package;
|
|
519
|
+
} else {
|
|
520
|
+
const aliased = await readProfileAlias(profileName);
|
|
521
|
+
packageName = aliased ?? `@agentproto/runtime-profile-${profileName}`;
|
|
522
|
+
}
|
|
523
|
+
let mod;
|
|
524
|
+
try {
|
|
525
|
+
mod = await import(packageName);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
528
|
+
process.stderr.write(
|
|
529
|
+
`agentproto install: could not load profile package '${packageName}'. Install it first with: npm i -g ${packageName}
|
|
530
|
+
cause: ${cause}
|
|
531
|
+
`
|
|
532
|
+
);
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
const filesDir = typeof mod.FILES_DIR === "string" ? mod.FILES_DIR : null;
|
|
536
|
+
const loadProfileManifest = mod.loadProfileManifest;
|
|
537
|
+
if (!filesDir || !loadProfileManifest) {
|
|
538
|
+
process.stderr.write(
|
|
539
|
+
`agentproto install: profile package '${packageName}' does not export FILES_DIR + loadProfileManifest.
|
|
540
|
+
`
|
|
541
|
+
);
|
|
542
|
+
return 1;
|
|
543
|
+
}
|
|
544
|
+
const manifest = await loadProfileManifest();
|
|
545
|
+
const cwd = values.cwd ? resolve(values.cwd) : process.cwd();
|
|
546
|
+
const dryRun = values["dry-run"] === true;
|
|
547
|
+
const force = values.force === true;
|
|
548
|
+
process.stdout.write(
|
|
549
|
+
`agentproto install ${slug} v${manifest.version} \u2192 ${cwd}
|
|
550
|
+
${manifest.description}
|
|
551
|
+
`
|
|
552
|
+
);
|
|
553
|
+
const ledgerPath = ledgerPathFor2(profileName);
|
|
554
|
+
const prevLedger = await loadLedger2(ledgerPath);
|
|
555
|
+
const newLedger = {
|
|
556
|
+
slug,
|
|
557
|
+
version: manifest.version,
|
|
558
|
+
installedAt: prevLedger?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
559
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
560
|
+
files: []
|
|
561
|
+
};
|
|
562
|
+
let applied = 0;
|
|
563
|
+
let skipped = 0;
|
|
564
|
+
let failed = 0;
|
|
565
|
+
for (const entry of manifest.files) {
|
|
566
|
+
const src = resolve(filesDir, entry.src);
|
|
567
|
+
const dest = resolve(cwd, entry.dest);
|
|
568
|
+
const prev = prevLedger?.files.find((f) => f.dest === entry.dest) ?? null;
|
|
569
|
+
try {
|
|
570
|
+
const srcBuf = await readFile(src);
|
|
571
|
+
const result = await applyStrategy({
|
|
572
|
+
strategy: entry.strategy,
|
|
573
|
+
srcBuf,
|
|
574
|
+
dest,
|
|
575
|
+
prev,
|
|
576
|
+
force,
|
|
577
|
+
dryRun
|
|
578
|
+
});
|
|
579
|
+
newLedger.files.push({
|
|
580
|
+
dest: entry.dest,
|
|
581
|
+
strategy: entry.strategy,
|
|
582
|
+
hashAfter: result.hashAfter,
|
|
583
|
+
appliedAt: result.wrote ? (/* @__PURE__ */ new Date()).toISOString() : prev?.appliedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
584
|
+
});
|
|
585
|
+
if (result.wrote) {
|
|
586
|
+
applied += 1;
|
|
587
|
+
process.stdout.write(` ${result.action.padEnd(10)} ${entry.dest}
|
|
588
|
+
`);
|
|
589
|
+
if (entry.executable && !dryRun) {
|
|
590
|
+
await chmod(dest, 493);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
skipped += 1;
|
|
594
|
+
process.stdout.write(` ${result.action.padEnd(10)} ${entry.dest}
|
|
595
|
+
`);
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
failed += 1;
|
|
599
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
600
|
+
process.stderr.write(` FAIL ${entry.dest} \u2014 ${msg}
|
|
601
|
+
`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!dryRun) {
|
|
605
|
+
await saveLedger2(ledgerPath, newLedger);
|
|
606
|
+
}
|
|
607
|
+
process.stdout.write(
|
|
608
|
+
`agentproto install ${slug}: ${applied} applied, ${skipped} skipped, ${failed} failed${dryRun ? " (dry-run)" : ""}
|
|
609
|
+
`
|
|
610
|
+
);
|
|
611
|
+
return failed === 0 ? 0 : 1;
|
|
612
|
+
}
|
|
613
|
+
async function applyStrategy(opts) {
|
|
614
|
+
const { strategy, srcBuf, dest, prev, force, dryRun } = opts;
|
|
615
|
+
const destExists = await fileExists(dest);
|
|
616
|
+
if (strategy === "preserve") {
|
|
617
|
+
if (destExists) {
|
|
618
|
+
const currentHash = await hashFile(dest);
|
|
619
|
+
return { wrote: false, action: "preserve", hashAfter: currentHash };
|
|
620
|
+
}
|
|
621
|
+
if (!dryRun) {
|
|
622
|
+
await ensureDir(dirname(dest));
|
|
623
|
+
await writeFile(dest, srcBuf);
|
|
624
|
+
}
|
|
625
|
+
return { wrote: true, action: "create", hashAfter: sha256(srcBuf) };
|
|
626
|
+
}
|
|
627
|
+
if (strategy === "overwrite") {
|
|
628
|
+
const srcHash = sha256(srcBuf);
|
|
629
|
+
if (destExists && !force) {
|
|
630
|
+
const destHash = await hashFile(dest);
|
|
631
|
+
if (destHash === srcHash) {
|
|
632
|
+
return { wrote: false, action: "unchanged", hashAfter: destHash };
|
|
633
|
+
}
|
|
634
|
+
if (prev && prev.hashAfter !== destHash) {
|
|
635
|
+
return {
|
|
636
|
+
wrote: false,
|
|
637
|
+
action: "user-edit",
|
|
638
|
+
hashAfter: destHash
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (!dryRun) {
|
|
643
|
+
await ensureDir(dirname(dest));
|
|
644
|
+
await writeFile(dest, srcBuf);
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
wrote: true,
|
|
648
|
+
action: destExists ? "update" : "create",
|
|
649
|
+
hashAfter: srcHash
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (strategy === "merge-json-deep") {
|
|
653
|
+
const srcJson = parseJsonOrThrow(srcBuf.toString("utf8"), "src");
|
|
654
|
+
let merged = srcJson;
|
|
655
|
+
if (destExists) {
|
|
656
|
+
const destRaw = await readFile(dest, "utf8");
|
|
657
|
+
const destJson = parseJsonOrThrow(destRaw, "dest");
|
|
658
|
+
merged = deepMerge(destJson, srcJson);
|
|
659
|
+
}
|
|
660
|
+
const out = `${JSON.stringify(merged, null, 2)}
|
|
661
|
+
`;
|
|
662
|
+
const outBuf = Buffer.from(out, "utf8");
|
|
663
|
+
const outHash = sha256(outBuf);
|
|
664
|
+
if (destExists) {
|
|
665
|
+
const destHash = await hashFile(dest);
|
|
666
|
+
if (destHash === outHash) {
|
|
667
|
+
return { wrote: false, action: "unchanged", hashAfter: destHash };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!dryRun) {
|
|
671
|
+
await ensureDir(dirname(dest));
|
|
672
|
+
await writeFile(dest, outBuf);
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
wrote: true,
|
|
676
|
+
action: destExists ? "merge" : "create",
|
|
677
|
+
hashAfter: outHash
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (strategy === "append") {
|
|
681
|
+
const srcText = srcBuf.toString("utf8");
|
|
682
|
+
if (destExists) {
|
|
683
|
+
const destText = await readFile(dest, "utf8");
|
|
684
|
+
if (destText.includes(srcText)) {
|
|
685
|
+
return {
|
|
686
|
+
wrote: false,
|
|
687
|
+
action: "unchanged",
|
|
688
|
+
hashAfter: sha256(Buffer.from(destText, "utf8"))
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const out = `${destText.endsWith("\n") ? destText : `${destText}
|
|
692
|
+
`}${srcText}`;
|
|
693
|
+
const outBuf = Buffer.from(out, "utf8");
|
|
694
|
+
if (!dryRun) {
|
|
695
|
+
await ensureDir(dirname(dest));
|
|
696
|
+
await writeFile(dest, outBuf);
|
|
697
|
+
}
|
|
698
|
+
return { wrote: true, action: "append", hashAfter: sha256(outBuf) };
|
|
699
|
+
}
|
|
700
|
+
if (!dryRun) {
|
|
701
|
+
await ensureDir(dirname(dest));
|
|
702
|
+
await writeFile(dest, srcBuf);
|
|
703
|
+
}
|
|
704
|
+
return { wrote: true, action: "create", hashAfter: sha256(srcBuf) };
|
|
705
|
+
}
|
|
706
|
+
throw new Error(`Unknown strategy: ${strategy}`);
|
|
707
|
+
}
|
|
708
|
+
function deepMerge(target, source) {
|
|
709
|
+
if (target && source && typeof target === "object" && typeof source === "object" && !Array.isArray(target) && !Array.isArray(source)) {
|
|
710
|
+
const out = { ...target };
|
|
711
|
+
for (const [k, v] of Object.entries(source)) {
|
|
712
|
+
out[k] = deepMerge(target[k], v);
|
|
713
|
+
}
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
718
|
+
const out = [];
|
|
719
|
+
for (const item of [...target, ...source]) {
|
|
720
|
+
const key = JSON.stringify(item);
|
|
721
|
+
if (seen.has(key)) continue;
|
|
722
|
+
seen.add(key);
|
|
723
|
+
out.push(item);
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
return source ?? target;
|
|
728
|
+
}
|
|
729
|
+
function parseJsonOrThrow(raw, side) {
|
|
730
|
+
try {
|
|
731
|
+
return JSON.parse(raw);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
734
|
+
throw new Error(`merge-json-deep ${side} not valid JSON: ${msg}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function sha256(buf) {
|
|
738
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
739
|
+
}
|
|
740
|
+
async function hashFile(path) {
|
|
741
|
+
const buf = await readFile(path);
|
|
742
|
+
return sha256(buf);
|
|
743
|
+
}
|
|
744
|
+
async function fileExists(path) {
|
|
745
|
+
try {
|
|
746
|
+
await stat(path);
|
|
747
|
+
return true;
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function ensureDir(dir) {
|
|
753
|
+
try {
|
|
754
|
+
await stat(dir);
|
|
755
|
+
} catch {
|
|
756
|
+
await mkdir(dir, { recursive: true });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function ledgerPathFor2(profileName) {
|
|
760
|
+
return join(homedir(), ".agentproto", "profiles", `${profileName}.json`);
|
|
761
|
+
}
|
|
762
|
+
async function loadLedger2(path) {
|
|
763
|
+
try {
|
|
764
|
+
const raw = await readFile(path, "utf8");
|
|
765
|
+
return JSON.parse(raw);
|
|
766
|
+
} catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function saveLedger2(path, ledger) {
|
|
771
|
+
await ensureDir(dirname(path));
|
|
772
|
+
await writeFile(path, `${JSON.stringify(ledger, null, 2)}
|
|
773
|
+
`);
|
|
774
|
+
}
|
|
775
|
+
async function readProfileAlias(profileName) {
|
|
776
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
777
|
+
const path = join(base, "config.json");
|
|
778
|
+
try {
|
|
779
|
+
const raw = await readFile(path, "utf8");
|
|
780
|
+
const parsed = JSON.parse(raw);
|
|
781
|
+
const aliased = parsed.profileAliases?.[profileName];
|
|
782
|
+
return typeof aliased === "string" ? aliased : null;
|
|
783
|
+
} catch {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
484
787
|
|
|
485
788
|
// src/commands/install.ts
|
|
486
789
|
async function runInstall(args) {
|
|
790
|
+
const slugPeek = args.find((a) => !a.startsWith("-"));
|
|
791
|
+
if (slugPeek?.startsWith("runtime-profile/")) {
|
|
792
|
+
return runInstallProfile(
|
|
793
|
+
slugPeek,
|
|
794
|
+
args.filter((a) => a !== slugPeek)
|
|
795
|
+
);
|
|
796
|
+
}
|
|
487
797
|
const { values, positionals } = parseArgs({
|
|
488
798
|
args: [...args],
|
|
489
799
|
allowPositionals: true,
|
|
@@ -776,7 +1086,7 @@ async function runDownloadInstaller(opts) {
|
|
|
776
1086
|
}
|
|
777
1087
|
if (extractCode !== 0) return extractCode;
|
|
778
1088
|
const srcBin = join(extractDir, opts.extractBin);
|
|
779
|
-
const binDir = process.env["AGENTPROTO_BIN_DIR"] ?? join(
|
|
1089
|
+
const binDir = process.env["AGENTPROTO_BIN_DIR"] ?? join(homedir3(), ".local", "bin");
|
|
780
1090
|
await mkdir(binDir, { recursive: true });
|
|
781
1091
|
const destBin = join(binDir, opts.extractBin.split("/").pop());
|
|
782
1092
|
const cpCode = await spawnInherit("cp", ["-p", srcBin, destBin]);
|
|
@@ -802,19 +1112,19 @@ async function runDownloadInstaller(opts) {
|
|
|
802
1112
|
});
|
|
803
1113
|
}
|
|
804
1114
|
}
|
|
805
|
-
function
|
|
1115
|
+
function homedir3() {
|
|
806
1116
|
return process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/";
|
|
807
1117
|
}
|
|
808
1118
|
function spawnInherit(cmd, argv) {
|
|
809
|
-
return new Promise((
|
|
1119
|
+
return new Promise((resolve4, reject) => {
|
|
810
1120
|
const child = spawn(cmd, argv, { stdio: "inherit" });
|
|
811
1121
|
child.once("error", reject);
|
|
812
|
-
child.once("exit", (code) =>
|
|
1122
|
+
child.once("exit", (code) => resolve4(code ?? 0));
|
|
813
1123
|
});
|
|
814
1124
|
}
|
|
815
1125
|
async function runVersionCheck(check) {
|
|
816
1126
|
if (!check) return { ok: false, message: "no version_check declared" };
|
|
817
|
-
return new Promise((
|
|
1127
|
+
return new Promise((resolve4) => {
|
|
818
1128
|
const child = spawn("bash", ["-lc", check.cmd], {
|
|
819
1129
|
stdio: ["ignore", "pipe", "ignore"]
|
|
820
1130
|
});
|
|
@@ -822,19 +1132,19 @@ async function runVersionCheck(check) {
|
|
|
822
1132
|
child.stdout.on("data", (c) => {
|
|
823
1133
|
buf += c.toString("utf8");
|
|
824
1134
|
});
|
|
825
|
-
child.once("error", () =>
|
|
1135
|
+
child.once("error", () => resolve4({ ok: false, message: "check failed" }));
|
|
826
1136
|
child.once("exit", (code) => {
|
|
827
1137
|
if (code !== 0) {
|
|
828
|
-
|
|
1138
|
+
resolve4({ ok: false, message: `check exited ${code}` });
|
|
829
1139
|
return;
|
|
830
1140
|
}
|
|
831
1141
|
const re = new RegExp(check.parse);
|
|
832
1142
|
const m = buf.match(re);
|
|
833
1143
|
if (!m || !m[1]) {
|
|
834
|
-
|
|
1144
|
+
resolve4({ ok: false, message: "could not parse version" });
|
|
835
1145
|
return;
|
|
836
1146
|
}
|
|
837
|
-
|
|
1147
|
+
resolve4({ ok: true, message: `version ${m[1]}` });
|
|
838
1148
|
});
|
|
839
1149
|
});
|
|
840
1150
|
}
|
|
@@ -1019,6 +1329,134 @@ function formatRelative(ms) {
|
|
|
1019
1329
|
return `${Math.round(ms / 864e5)}d`;
|
|
1020
1330
|
}
|
|
1021
1331
|
|
|
1332
|
+
// src/util/pty-factory.ts
|
|
1333
|
+
async function loadNodePtyFactory() {
|
|
1334
|
+
try {
|
|
1335
|
+
const nodePtyMod = await import('node-pty');
|
|
1336
|
+
const nodePty = nodePtyMod.default ?? nodePtyMod;
|
|
1337
|
+
if (typeof nodePty.spawn !== "function") return null;
|
|
1338
|
+
return (opts) => {
|
|
1339
|
+
const pty = nodePty.spawn(opts.command, opts.args, {
|
|
1340
|
+
name: "xterm-256color",
|
|
1341
|
+
cols: opts.cols,
|
|
1342
|
+
rows: opts.rows,
|
|
1343
|
+
cwd: opts.cwd,
|
|
1344
|
+
env: {
|
|
1345
|
+
...process.env,
|
|
1346
|
+
...opts.env ?? {}
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
const proc = {
|
|
1350
|
+
get pid() {
|
|
1351
|
+
return pty.pid;
|
|
1352
|
+
},
|
|
1353
|
+
write(data) {
|
|
1354
|
+
pty.write(data);
|
|
1355
|
+
},
|
|
1356
|
+
resize(cols, rows) {
|
|
1357
|
+
pty.resize(cols, rows);
|
|
1358
|
+
},
|
|
1359
|
+
kill(signal) {
|
|
1360
|
+
pty.kill(signal);
|
|
1361
|
+
},
|
|
1362
|
+
onData(handler) {
|
|
1363
|
+
pty.onData(handler);
|
|
1364
|
+
},
|
|
1365
|
+
onExit(handler) {
|
|
1366
|
+
pty.onExit(handler);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
return proc;
|
|
1370
|
+
};
|
|
1371
|
+
} catch {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
var CONFIG_FILE_PATH = () => join(homedir(), ".agentproto", "config.json");
|
|
1376
|
+
async function loadConfig(path) {
|
|
1377
|
+
const target = CONFIG_FILE_PATH();
|
|
1378
|
+
try {
|
|
1379
|
+
const raw = await promises.readFile(target, "utf8");
|
|
1380
|
+
const parsed = JSON.parse(raw);
|
|
1381
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1382
|
+
return parsed;
|
|
1383
|
+
}
|
|
1384
|
+
console.warn(
|
|
1385
|
+
`[runtime/config] ${target}: top-level value is not an object \u2014 ignoring`
|
|
1386
|
+
);
|
|
1387
|
+
return {};
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
const code = err.code;
|
|
1390
|
+
if (code && code !== "ENOENT") {
|
|
1391
|
+
console.warn(
|
|
1392
|
+
`[runtime/config] failed to read ${target}: ${err instanceof Error ? err.message : String(err)}`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return {};
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
var WORKSPACES_CONFIG_VERSION = 1;
|
|
1399
|
+
var DEFAULT_CONFIG_DIR = () => resolve(homedir(), ".agentproto");
|
|
1400
|
+
var DEFAULT_CONFIG_PATH = () => resolve(DEFAULT_CONFIG_DIR(), "workspaces.json");
|
|
1401
|
+
function sanitizeSlug(input2) {
|
|
1402
|
+
const trimmed = input2.trim().toLowerCase();
|
|
1403
|
+
const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1404
|
+
return cleaned || "workspace";
|
|
1405
|
+
}
|
|
1406
|
+
async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
|
|
1407
|
+
let raw;
|
|
1408
|
+
try {
|
|
1409
|
+
raw = await promises.readFile(path, "utf8");
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
if (err.code === "ENOENT") {
|
|
1412
|
+
return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
|
|
1413
|
+
}
|
|
1414
|
+
throw err;
|
|
1415
|
+
}
|
|
1416
|
+
let parsed;
|
|
1417
|
+
try {
|
|
1418
|
+
parsed = JSON.parse(raw);
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
`agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return normalizeConfig(parsed);
|
|
1425
|
+
}
|
|
1426
|
+
function normalizeConfig(parsed) {
|
|
1427
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1428
|
+
return { version: WORKSPACES_CONFIG_VERSION, workspaces: [] };
|
|
1429
|
+
}
|
|
1430
|
+
const obj = parsed;
|
|
1431
|
+
const workspaces = [];
|
|
1432
|
+
if (Array.isArray(obj.workspaces)) {
|
|
1433
|
+
for (const entry of obj.workspaces) {
|
|
1434
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1435
|
+
const e = entry;
|
|
1436
|
+
const slug = typeof e.slug === "string" ? sanitizeSlug(e.slug) : "";
|
|
1437
|
+
const path = typeof e.path === "string" ? e.path : "";
|
|
1438
|
+
if (!slug || !path) continue;
|
|
1439
|
+
const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
1440
|
+
const updatedAt = typeof e.updatedAt === "string" ? e.updatedAt : addedAt;
|
|
1441
|
+
const we = { slug, path, addedAt, updatedAt };
|
|
1442
|
+
if (typeof e.label === "string" && e.label.trim()) {
|
|
1443
|
+
we.label = e.label.trim();
|
|
1444
|
+
}
|
|
1445
|
+
workspaces.push(we);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const w of workspaces) dedup.set(w.slug, w);
|
|
1450
|
+
const finalList = Array.from(dedup.values());
|
|
1451
|
+
const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
|
|
1452
|
+
const out = {
|
|
1453
|
+
version: WORKSPACES_CONFIG_VERSION,
|
|
1454
|
+
workspaces: finalList
|
|
1455
|
+
};
|
|
1456
|
+
if (active !== void 0) out.active = active;
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1022
1460
|
// ../define-doctype/dist/index.mjs
|
|
1023
1461
|
var DEFAULT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{1,79}$/;
|
|
1024
1462
|
var MAX_DESCRIPTION_LEN = 2e3;
|
|
@@ -1153,7 +1591,7 @@ function createVerbs(spec) {
|
|
|
1153
1591
|
}
|
|
1154
1592
|
return { path, handle: newHandle, rendered };
|
|
1155
1593
|
}
|
|
1156
|
-
async function
|
|
1594
|
+
async function resolve4(block, ctx = {}) {
|
|
1157
1595
|
if ("inline" in block) {
|
|
1158
1596
|
return spec.define(block.inline);
|
|
1159
1597
|
}
|
|
@@ -1179,7 +1617,7 @@ function createVerbs(spec) {
|
|
|
1179
1617
|
async function deleteFn(path) {
|
|
1180
1618
|
await rm(path, { force: true });
|
|
1181
1619
|
}
|
|
1182
|
-
return { create, load, list, update, resolve:
|
|
1620
|
+
return { create, load, list, update, resolve: resolve4, delete: deleteFn };
|
|
1183
1621
|
}
|
|
1184
1622
|
function defaultBody(spec, frontmatter) {
|
|
1185
1623
|
const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
|
|
@@ -1503,12 +1941,55 @@ async function writeRuntimeMeta(workspace, meta) {
|
|
|
1503
1941
|
await writeFile(
|
|
1504
1942
|
join(dir, "runtime.json"),
|
|
1505
1943
|
JSON.stringify(meta, null, 2) + "\n",
|
|
1506
|
-
"utf8"
|
|
1944
|
+
{ encoding: "utf8", mode: 384 }
|
|
1507
1945
|
);
|
|
1508
1946
|
} catch (err) {
|
|
1509
1947
|
console.error("[runtime] failed to write .agentproto/runtime.json:", err);
|
|
1510
1948
|
}
|
|
1511
1949
|
}
|
|
1950
|
+
async function unlinkRuntimeMeta(workspace) {
|
|
1951
|
+
try {
|
|
1952
|
+
await unlink(join(workspace, ".agentproto", "runtime.json"));
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
async function readRuntimeMeta(workspace) {
|
|
1957
|
+
const path = join(workspace, ".agentproto", "runtime.json");
|
|
1958
|
+
try {
|
|
1959
|
+
const [raw, st] = await Promise.all([readFile(path, "utf8"), stat(path)]);
|
|
1960
|
+
const meta = JSON.parse(raw);
|
|
1961
|
+
return { meta, mtime: st.mtime };
|
|
1962
|
+
} catch {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function sweepStaleRuntimeMetas(workspaces, currentWorkspace) {
|
|
1967
|
+
const cleaned = [];
|
|
1968
|
+
for (const ws of workspaces) {
|
|
1969
|
+
if (ws === currentWorkspace) continue;
|
|
1970
|
+
const meta = await readRuntimeMeta(ws);
|
|
1971
|
+
if (!meta) continue;
|
|
1972
|
+
const pid = meta.meta.pid;
|
|
1973
|
+
if (typeof pid === "number" && !isPidAlive(pid)) {
|
|
1974
|
+
try {
|
|
1975
|
+
await unlink(join(ws, ".agentproto", "runtime.json"));
|
|
1976
|
+
cleaned.push(join(ws, ".agentproto", "runtime.json"));
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return cleaned;
|
|
1982
|
+
}
|
|
1983
|
+
function isPidAlive(pid) {
|
|
1984
|
+
try {
|
|
1985
|
+
process.kill(pid, 0);
|
|
1986
|
+
return true;
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
const code = err.code;
|
|
1989
|
+
if (code === "EPERM") return true;
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1512
1993
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
1513
1994
|
var MAX_TIMEOUT_MS = 6e5;
|
|
1514
1995
|
var ALLOWLIST_REL = ".agentproto/allowed-commands.json";
|
|
@@ -1937,21 +2418,21 @@ function registerFsTools(server, opts) {
|
|
|
1937
2418
|
}
|
|
1938
2419
|
);
|
|
1939
2420
|
}
|
|
1940
|
-
var
|
|
1941
|
-
var
|
|
1942
|
-
var
|
|
1943
|
-
function
|
|
2421
|
+
var WORKSPACES_CONFIG_VERSION2 = 1;
|
|
2422
|
+
var DEFAULT_CONFIG_DIR2 = () => resolve(homedir(), ".agentproto");
|
|
2423
|
+
var DEFAULT_CONFIG_PATH2 = () => resolve(DEFAULT_CONFIG_DIR2(), "workspaces.json");
|
|
2424
|
+
function sanitizeSlug2(input2) {
|
|
1944
2425
|
const trimmed = input2.trim().toLowerCase();
|
|
1945
2426
|
const cleaned = trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1946
2427
|
return cleaned || "workspace";
|
|
1947
2428
|
}
|
|
1948
|
-
async function
|
|
2429
|
+
async function loadWorkspacesConfig2(path = DEFAULT_CONFIG_PATH2()) {
|
|
1949
2430
|
let raw;
|
|
1950
2431
|
try {
|
|
1951
2432
|
raw = await promises.readFile(path, "utf8");
|
|
1952
2433
|
} catch (err) {
|
|
1953
2434
|
if (err.code === "ENOENT") {
|
|
1954
|
-
return { version:
|
|
2435
|
+
return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
|
|
1955
2436
|
}
|
|
1956
2437
|
throw err;
|
|
1957
2438
|
}
|
|
@@ -1963,18 +2444,18 @@ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
|
|
|
1963
2444
|
`agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete the file or fix it manually.`
|
|
1964
2445
|
);
|
|
1965
2446
|
}
|
|
1966
|
-
return
|
|
2447
|
+
return normalizeConfig2(parsed);
|
|
1967
2448
|
}
|
|
1968
2449
|
function findWorkspace(config, slug) {
|
|
1969
|
-
return config.workspaces.find((w) => w.slug ===
|
|
2450
|
+
return config.workspaces.find((w) => w.slug === sanitizeSlug2(slug));
|
|
1970
2451
|
}
|
|
1971
2452
|
function getActiveWorkspace(config) {
|
|
1972
2453
|
if (!config.active) return config.workspaces[0];
|
|
1973
2454
|
return findWorkspace(config, config.active) ?? config.workspaces[0];
|
|
1974
2455
|
}
|
|
1975
|
-
function
|
|
2456
|
+
function normalizeConfig2(parsed) {
|
|
1976
2457
|
if (!parsed || typeof parsed !== "object") {
|
|
1977
|
-
return { version:
|
|
2458
|
+
return { version: WORKSPACES_CONFIG_VERSION2, workspaces: [] };
|
|
1978
2459
|
}
|
|
1979
2460
|
const obj = parsed;
|
|
1980
2461
|
const workspaces = [];
|
|
@@ -1982,7 +2463,7 @@ function normaliseConfig(parsed) {
|
|
|
1982
2463
|
for (const entry of obj.workspaces) {
|
|
1983
2464
|
if (!entry || typeof entry !== "object") continue;
|
|
1984
2465
|
const e = entry;
|
|
1985
|
-
const slug = typeof e.slug === "string" ?
|
|
2466
|
+
const slug = typeof e.slug === "string" ? sanitizeSlug2(e.slug) : "";
|
|
1986
2467
|
const path = typeof e.path === "string" ? e.path : "";
|
|
1987
2468
|
if (!slug || !path) continue;
|
|
1988
2469
|
const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1999,7 +2480,7 @@ function normaliseConfig(parsed) {
|
|
|
1999
2480
|
const finalList = Array.from(dedup.values());
|
|
2000
2481
|
const active = typeof obj.active === "string" && finalList.some((w) => w.slug === obj.active) ? obj.active : finalList[0]?.slug;
|
|
2001
2482
|
const out = {
|
|
2002
|
-
version:
|
|
2483
|
+
version: WORKSPACES_CONFIG_VERSION2,
|
|
2003
2484
|
workspaces: finalList
|
|
2004
2485
|
};
|
|
2005
2486
|
if (active !== void 0) out.active = active;
|
|
@@ -2125,7 +2606,7 @@ async function scanGoose(home, out, errors) {
|
|
|
2125
2606
|
async function scanRegisteredWorkspaces(out, errors) {
|
|
2126
2607
|
let workspaces = [];
|
|
2127
2608
|
try {
|
|
2128
|
-
const cfg = await
|
|
2609
|
+
const cfg = await loadWorkspacesConfig2();
|
|
2129
2610
|
workspaces = cfg.workspaces;
|
|
2130
2611
|
} catch (err) {
|
|
2131
2612
|
errors.push(`workspaces: load failed \u2014 ${err.message}`);
|
|
@@ -2243,7 +2724,7 @@ async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
|
|
|
2243
2724
|
`agentproto: ${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}). Delete or fix manually.`
|
|
2244
2725
|
);
|
|
2245
2726
|
}
|
|
2246
|
-
return
|
|
2727
|
+
return normalize3(parsed);
|
|
2247
2728
|
}
|
|
2248
2729
|
async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
|
|
2249
2730
|
await promises.mkdir(dirname(path), { recursive: true });
|
|
@@ -2269,7 +2750,7 @@ function addImport(config, input2) {
|
|
|
2269
2750
|
function removeImport(config, id) {
|
|
2270
2751
|
return { ...config, imports: config.imports.filter((e) => e.id !== id) };
|
|
2271
2752
|
}
|
|
2272
|
-
function
|
|
2753
|
+
function normalize3(parsed) {
|
|
2273
2754
|
if (!parsed || typeof parsed !== "object") return EMPTY;
|
|
2274
2755
|
const obj = parsed;
|
|
2275
2756
|
const imports = [];
|
|
@@ -2289,6 +2770,7 @@ function normalise(parsed) {
|
|
|
2289
2770
|
}
|
|
2290
2771
|
function registerSessionTools(server, opts) {
|
|
2291
2772
|
const { registry, resolveAgentAdapter, listAgentAdapters, mcpProxy } = opts;
|
|
2773
|
+
const ptyEnabled = opts.ptyEnabled === true;
|
|
2292
2774
|
server.tool(
|
|
2293
2775
|
"start_agent_session",
|
|
2294
2776
|
"Spawn a long-running agent CLI (claude-code, hermes, \u2026) on the host. The session stays alive across multiple turns \u2014 call `prompt_agent_session` to continue the conversation. Returns the session id + initial descriptor. When `workspaceSlug` is set, resolves the cwd via `~/.agentproto/workspaces.json`; otherwise pass `cwd` explicitly or fall back to the active workspace.",
|
|
@@ -2325,7 +2807,7 @@ function registerSessionTools(server, opts) {
|
|
|
2325
2807
|
let resolvedSlug = input2.workspaceSlug ?? "default";
|
|
2326
2808
|
if (!cwd) {
|
|
2327
2809
|
try {
|
|
2328
|
-
const config = await
|
|
2810
|
+
const config = await loadWorkspacesConfig2();
|
|
2329
2811
|
const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
|
|
2330
2812
|
if (ws) {
|
|
2331
2813
|
cwd = ws.path;
|
|
@@ -2421,22 +2903,49 @@ function registerSessionTools(server, opts) {
|
|
|
2421
2903
|
}
|
|
2422
2904
|
);
|
|
2423
2905
|
server.tool(
|
|
2424
|
-
"
|
|
2425
|
-
"List
|
|
2906
|
+
"list_sessions",
|
|
2907
|
+
"List sessions tracked by the daemon \u2014 agent-CLI sessions (claude-code, hermes, \u2026), terminal/PTY sessions (claude TUI, bash, \u2026), and raw commands. Each entry includes `kind`, `pty` (true for real PTYs), `name` (when set at spawn), `status`, `command`, age + exit code. Use this when you need to know what's already running before spawning anything new, or to discover a session id by name.",
|
|
2426
2908
|
{
|
|
2427
|
-
|
|
2909
|
+
kind: z.enum(["terminal", "agent-cli", "command", "all"]).optional().describe(
|
|
2910
|
+
"Filter by session kind. `all` (default) returns every kind. Use `terminal` to list only PTY sessions, `agent-cli` for structured ACP agents."
|
|
2911
|
+
),
|
|
2912
|
+
onlyAlive: z.boolean().optional().describe("When true, only running/starting sessions. Default false."),
|
|
2913
|
+
status: z.enum(["starting", "running", "exited", "killed", "error"]).optional().describe("Filter by exact status (overrides onlyAlive).")
|
|
2428
2914
|
},
|
|
2429
2915
|
async (input2) => {
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
(s) => s.
|
|
2433
|
-
|
|
2916
|
+
let rows = registry.list();
|
|
2917
|
+
if (input2.kind && input2.kind !== "all") {
|
|
2918
|
+
rows = rows.filter((s) => s.kind === input2.kind);
|
|
2919
|
+
}
|
|
2920
|
+
if (input2.status) {
|
|
2921
|
+
rows = rows.filter((s) => s.status === input2.status);
|
|
2922
|
+
} else if (input2.onlyAlive) {
|
|
2923
|
+
rows = rows.filter(
|
|
2924
|
+
(s) => s.status === "running" || s.status === "starting"
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2434
2927
|
return {
|
|
2435
2928
|
content: [
|
|
2436
|
-
{
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2929
|
+
{ type: "text", text: JSON.stringify({ sessions: rows }, null, 2) }
|
|
2930
|
+
]
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
);
|
|
2934
|
+
server.tool(
|
|
2935
|
+
"list_agent_sessions",
|
|
2936
|
+
"DEPRECATED \u2014 prefer `list_sessions` which returns ALL kinds + filters. Despite the name this tool already returns every kind, not just agent-cli sessions; the new tool's name reflects the actual surface.",
|
|
2937
|
+
{
|
|
2938
|
+
onlyAlive: z.boolean().optional().describe("Filter to status running/starting only. Default false.")
|
|
2939
|
+
},
|
|
2940
|
+
async (input2) => {
|
|
2941
|
+
const all = registry.list();
|
|
2942
|
+
const filtered = input2.onlyAlive ? all.filter((s) => s.status === "running" || s.status === "starting") : all;
|
|
2943
|
+
return {
|
|
2944
|
+
content: [
|
|
2945
|
+
{
|
|
2946
|
+
type: "text",
|
|
2947
|
+
text: JSON.stringify({ sessions: filtered }, null, 2)
|
|
2948
|
+
}
|
|
2440
2949
|
]
|
|
2441
2950
|
};
|
|
2442
2951
|
}
|
|
@@ -2792,6 +3301,206 @@ function registerSessionTools(server, opts) {
|
|
|
2792
3301
|
};
|
|
2793
3302
|
}
|
|
2794
3303
|
);
|
|
3304
|
+
const ptyNotConfigured = (toolName) => ({
|
|
3305
|
+
content: [
|
|
3306
|
+
{
|
|
3307
|
+
type: "text",
|
|
3308
|
+
text: `${toolName}: PTY support not enabled \u2014 the daemon was started without a node-pty factory. Re-run \`agentproto serve\` from a build that ships node-pty (the optional dep ships with @agentproto/cli).`
|
|
3309
|
+
}
|
|
3310
|
+
],
|
|
3311
|
+
isError: true
|
|
3312
|
+
});
|
|
3313
|
+
server.tool(
|
|
3314
|
+
"start_terminal_session",
|
|
3315
|
+
"Spawn a process under a real PTY (node-pty) on the host. Bytes (including ANSI escapes, alt-screen sequences) flow through the daemon's byte ring buffer; subscribers attach via the WS at /sessions/:id/pty. Use for interactive TUIs (claude, vim, htop) or to orchestrate shells from another agent. Returns the session descriptor.",
|
|
3316
|
+
{
|
|
3317
|
+
argv: z.array(z.string()).min(1).describe(
|
|
3318
|
+
"Argv array. First element is the binary, rest are arguments. e.g. ['claude'] or ['bash', '-l']."
|
|
3319
|
+
),
|
|
3320
|
+
workspaceSlug: z.string().optional().describe(
|
|
3321
|
+
"Workspace slug from `agentproto workspace list`. Resolves cwd. Omit to use `cwd` explicitly or the active workspace."
|
|
3322
|
+
),
|
|
3323
|
+
cwd: z.string().optional().describe("Absolute cwd. Wins over workspaceSlug when both set."),
|
|
3324
|
+
cols: z.number().int().min(1).max(500).optional().describe("Initial cols. Default 80."),
|
|
3325
|
+
rows: z.number().int().min(1).max(200).optional().describe("Initial rows. Default 24."),
|
|
3326
|
+
name: z.string().optional().describe(
|
|
3327
|
+
"User-friendly slug. Becomes an alias for the session id in subsequent tool calls (read/write/kill accept either)."
|
|
3328
|
+
),
|
|
3329
|
+
label: z.string().optional().describe(
|
|
3330
|
+
"Free-text label surfaced in list_agent_sessions and the UI."
|
|
3331
|
+
)
|
|
3332
|
+
},
|
|
3333
|
+
async (input2) => {
|
|
3334
|
+
if (!ptyEnabled) return ptyNotConfigured("start_terminal_session");
|
|
3335
|
+
let cwd = input2.cwd;
|
|
3336
|
+
let resolvedSlug = input2.workspaceSlug ?? "default";
|
|
3337
|
+
if (!cwd) {
|
|
3338
|
+
try {
|
|
3339
|
+
const config = await loadWorkspacesConfig2();
|
|
3340
|
+
const ws = input2.workspaceSlug ? findWorkspace(config, input2.workspaceSlug) : getActiveWorkspace(config);
|
|
3341
|
+
if (ws) {
|
|
3342
|
+
cwd = ws.path;
|
|
3343
|
+
resolvedSlug = ws.slug;
|
|
3344
|
+
}
|
|
3345
|
+
} catch {
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (!cwd) {
|
|
3349
|
+
return {
|
|
3350
|
+
content: [
|
|
3351
|
+
{
|
|
3352
|
+
type: "text",
|
|
3353
|
+
text: "start_terminal_session: no cwd resolvable. Pass `cwd` explicitly or `workspaceSlug` matching `agentproto workspace list`."
|
|
3354
|
+
}
|
|
3355
|
+
],
|
|
3356
|
+
isError: true
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
try {
|
|
3360
|
+
const desc = registry.spawnPty({
|
|
3361
|
+
argv: input2.argv,
|
|
3362
|
+
cwd,
|
|
3363
|
+
workspaceSlug: resolvedSlug,
|
|
3364
|
+
cols: input2.cols ?? 80,
|
|
3365
|
+
rows: input2.rows ?? 24,
|
|
3366
|
+
...input2.name ? { name: input2.name } : {},
|
|
3367
|
+
...input2.label ? { label: input2.label } : {}
|
|
3368
|
+
});
|
|
3369
|
+
return {
|
|
3370
|
+
content: [{ type: "text", text: JSON.stringify(desc, null, 2) }]
|
|
3371
|
+
};
|
|
3372
|
+
} catch (err) {
|
|
3373
|
+
return {
|
|
3374
|
+
content: [
|
|
3375
|
+
{
|
|
3376
|
+
type: "text",
|
|
3377
|
+
text: `start_terminal_session: ${err instanceof Error ? err.message : String(err)}`
|
|
3378
|
+
}
|
|
3379
|
+
],
|
|
3380
|
+
isError: true
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
);
|
|
3385
|
+
server.tool(
|
|
3386
|
+
"write_terminal_input",
|
|
3387
|
+
"Send keystrokes to a PTY session's stdin. The text is forwarded verbatim \u2014 include trailing newlines if the target needs them (e.g. shell commands). Use after `start_terminal_session` to drive an interactive CLI.",
|
|
3388
|
+
{
|
|
3389
|
+
sessionId: z.string().describe("Session id OR name from start_terminal_session."),
|
|
3390
|
+
text: z.string().describe("Text to write. Sent as-is to the PTY's stdin.")
|
|
3391
|
+
},
|
|
3392
|
+
async (input2) => {
|
|
3393
|
+
if (!ptyEnabled) return ptyNotConfigured("write_terminal_input");
|
|
3394
|
+
const desc = registry.findByIdOrName(input2.sessionId);
|
|
3395
|
+
if (!desc) {
|
|
3396
|
+
return {
|
|
3397
|
+
content: [
|
|
3398
|
+
{
|
|
3399
|
+
type: "text",
|
|
3400
|
+
text: `write_terminal_input: no session "${input2.sessionId}"`
|
|
3401
|
+
}
|
|
3402
|
+
],
|
|
3403
|
+
isError: true
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
const ok = registry.writeTerminalInput(desc.id, input2.text);
|
|
3407
|
+
return {
|
|
3408
|
+
content: [
|
|
3409
|
+
{
|
|
3410
|
+
type: "text",
|
|
3411
|
+
text: JSON.stringify({ ok, sessionId: desc.id }, null, 2)
|
|
3412
|
+
}
|
|
3413
|
+
],
|
|
3414
|
+
...ok ? {} : { isError: true }
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
);
|
|
3418
|
+
server.tool(
|
|
3419
|
+
"read_terminal_output",
|
|
3420
|
+
"Snapshot the recent byte buffer of a PTY session. Returns base64-encoded bytes (the buffer is RAW including ANSI escapes \u2014 strip with a regex if you want plain text). `lastBytes` caps the read from the tail.",
|
|
3421
|
+
{
|
|
3422
|
+
sessionId: z.string().describe("Session id OR name from start_terminal_session."),
|
|
3423
|
+
lastBytes: z.number().int().min(1).max(64 * 1024).optional().describe("Max bytes from the tail. Default: full ring buffer (~64 KiB).")
|
|
3424
|
+
},
|
|
3425
|
+
async (input2) => {
|
|
3426
|
+
if (!ptyEnabled) return ptyNotConfigured("read_terminal_output");
|
|
3427
|
+
const desc = registry.findByIdOrName(input2.sessionId);
|
|
3428
|
+
if (!desc) {
|
|
3429
|
+
return {
|
|
3430
|
+
content: [
|
|
3431
|
+
{
|
|
3432
|
+
type: "text",
|
|
3433
|
+
text: `read_terminal_output: no session "${input2.sessionId}"`
|
|
3434
|
+
}
|
|
3435
|
+
],
|
|
3436
|
+
isError: true
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
const buf = registry.readTerminalOutput(
|
|
3440
|
+
desc.id,
|
|
3441
|
+
input2.lastBytes
|
|
3442
|
+
);
|
|
3443
|
+
if (!buf) {
|
|
3444
|
+
return {
|
|
3445
|
+
content: [
|
|
3446
|
+
{
|
|
3447
|
+
type: "text",
|
|
3448
|
+
text: `read_terminal_output: session "${desc.id}" is not a PTY`
|
|
3449
|
+
}
|
|
3450
|
+
],
|
|
3451
|
+
isError: true
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
return {
|
|
3455
|
+
content: [
|
|
3456
|
+
{
|
|
3457
|
+
type: "text",
|
|
3458
|
+
text: JSON.stringify(
|
|
3459
|
+
{
|
|
3460
|
+
sessionId: desc.id,
|
|
3461
|
+
status: desc.status,
|
|
3462
|
+
bytes: buf.byteLength,
|
|
3463
|
+
b64: buf.toString("base64")
|
|
3464
|
+
},
|
|
3465
|
+
null,
|
|
3466
|
+
2
|
|
3467
|
+
)
|
|
3468
|
+
}
|
|
3469
|
+
]
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
);
|
|
3473
|
+
server.tool(
|
|
3474
|
+
"kill_terminal_session",
|
|
3475
|
+
"SIGTERM a PTY session and drop it from the alive set. Same effect as `kill_agent_session` for the PTY family \u2014 separate name so it's obvious what's being stopped.",
|
|
3476
|
+
{
|
|
3477
|
+
sessionId: z.string().describe("Session id OR name from start_terminal_session.")
|
|
3478
|
+
},
|
|
3479
|
+
async (input2) => {
|
|
3480
|
+
if (!ptyEnabled) return ptyNotConfigured("kill_terminal_session");
|
|
3481
|
+
const desc = registry.findByIdOrName(input2.sessionId);
|
|
3482
|
+
if (!desc) {
|
|
3483
|
+
return {
|
|
3484
|
+
content: [
|
|
3485
|
+
{
|
|
3486
|
+
type: "text",
|
|
3487
|
+
text: `kill_terminal_session: no session "${input2.sessionId}"`
|
|
3488
|
+
}
|
|
3489
|
+
],
|
|
3490
|
+
isError: true
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
const ok = registry.kill(desc.id);
|
|
3494
|
+
return {
|
|
3495
|
+
content: [
|
|
3496
|
+
{
|
|
3497
|
+
type: "text",
|
|
3498
|
+
text: JSON.stringify({ ok, sessionId: desc.id }, null, 2)
|
|
3499
|
+
}
|
|
3500
|
+
]
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
);
|
|
2795
3504
|
}
|
|
2796
3505
|
function startHeartbeat(opts) {
|
|
2797
3506
|
const filename = opts.filename ?? "HEARTBEAT.md";
|
|
@@ -2964,6 +3673,11 @@ function preview(text3, max = 120) {
|
|
|
2964
3673
|
const flat = text3.replace(/\s+/g, " ").trim();
|
|
2965
3674
|
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
2966
3675
|
}
|
|
3676
|
+
var DEFAULT_ALLOWED_ORIGINS = [
|
|
3677
|
+
"http://localhost:*",
|
|
3678
|
+
"http://127.0.0.1:*",
|
|
3679
|
+
"https://localhost:*"
|
|
3680
|
+
];
|
|
2967
3681
|
async function startHttpServer(opts) {
|
|
2968
3682
|
const startedAt = Date.now();
|
|
2969
3683
|
const authSource = opts.auth ?? { mode: "none" };
|
|
@@ -2988,15 +3702,69 @@ async function startHttpServer(opts) {
|
|
|
2988
3702
|
}
|
|
2989
3703
|
return true;
|
|
2990
3704
|
}
|
|
3705
|
+
function checkSessionsToken(req) {
|
|
3706
|
+
if (!opts.token) return "ok";
|
|
3707
|
+
const header = req.headers.authorization;
|
|
3708
|
+
const expected = `Bearer ${opts.token}`;
|
|
3709
|
+
if (header === expected) return "ok";
|
|
3710
|
+
const urlStr = req.url ?? "";
|
|
3711
|
+
if (urlStr.includes("?")) {
|
|
3712
|
+
const qs = new URLSearchParams(urlStr.slice(urlStr.indexOf("?") + 1));
|
|
3713
|
+
const qsToken = qs.get("token");
|
|
3714
|
+
if (qsToken && qsToken === opts.token) return "ok";
|
|
3715
|
+
}
|
|
3716
|
+
const origin = req.headers.origin;
|
|
3717
|
+
if (typeof origin === "string" && originAllowed(origin)) return "ok";
|
|
3718
|
+
return header ? "bad" : "missing";
|
|
3719
|
+
}
|
|
3720
|
+
function originAllowed(origin) {
|
|
3721
|
+
const list = opts.strictOrigins ? opts.allowedOrigins ?? [] : [...DEFAULT_ALLOWED_ORIGINS, ...opts.allowedOrigins ?? []];
|
|
3722
|
+
for (const pattern of list) {
|
|
3723
|
+
if (pattern === origin) return true;
|
|
3724
|
+
if (pattern.endsWith(":*")) {
|
|
3725
|
+
const prefix = pattern.slice(0, -2);
|
|
3726
|
+
if (origin === prefix || origin.startsWith(prefix + ":")) return true;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
return false;
|
|
3730
|
+
}
|
|
3731
|
+
function rejectUnauthorizedSession(req, res, kind) {
|
|
3732
|
+
const origin = req.headers.origin ?? null;
|
|
3733
|
+
const allowList = opts.strictOrigins ? opts.allowedOrigins ?? [] : [...DEFAULT_ALLOWED_ORIGINS, ...opts.allowedOrigins ?? []];
|
|
3734
|
+
res.writeHead(401, { "content-type": "application/json" });
|
|
3735
|
+
const message = kind === "missing" ? origin ? `Origin "${origin}" not in the daemon's allowlist, and no Bearer token sent. Either add it (\`agentproto config set daemon.allowedOrigins ${origin}\` then restart the daemon), or send Authorization: Bearer <token> read from <workspace>/.agentproto/runtime.json.` : `Authorization: Bearer <token> required on mutating /sessions/* routes. Read the token from <workspace>/.agentproto/runtime.json. (Browser callers can use an Origin in the allowlist instead.)` : `Invalid bearer token. The daemon regenerated its token on its last boot \u2014 re-read <workspace>/.agentproto/runtime.json.`;
|
|
3736
|
+
res.end(
|
|
3737
|
+
JSON.stringify({
|
|
3738
|
+
error: "sessions_unauthorized",
|
|
3739
|
+
message,
|
|
3740
|
+
// Debug data so a UI / network-tab inspection points the user
|
|
3741
|
+
// straight at the fix without needing the daemon's stderr.
|
|
3742
|
+
rejectedOrigin: origin,
|
|
3743
|
+
allowedOrigins: allowList,
|
|
3744
|
+
// We never echo the token; only the absence-or-presence flag.
|
|
3745
|
+
receivedAuthHeader: typeof req.headers.authorization === "string"
|
|
3746
|
+
})
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
function isMutatingSessionsRoute(method, path) {
|
|
3750
|
+
if (!path.startsWith("/sessions")) return false;
|
|
3751
|
+
if (method === "POST" || method === "DELETE" || method === "PUT" || method === "PATCH") {
|
|
3752
|
+
return true;
|
|
3753
|
+
}
|
|
3754
|
+
return false;
|
|
3755
|
+
}
|
|
2991
3756
|
async function handleMcp(req, res) {
|
|
2992
3757
|
if (!authorize(req, res)) return;
|
|
2993
3758
|
const server2 = await opts.mcpServerFactory();
|
|
2994
3759
|
const transport = new StreamableHTTPServerTransport({
|
|
2995
3760
|
sessionIdGenerator: void 0
|
|
2996
3761
|
});
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3762
|
+
const transportInternal = transport;
|
|
3763
|
+
if ("onerror" in transport) {
|
|
3764
|
+
transportInternal["onerror"] = (err) => {
|
|
3765
|
+
console.error("[mcp transport.onerror]", err);
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3000
3768
|
res.on("close", () => {
|
|
3001
3769
|
void transport.close();
|
|
3002
3770
|
void server2.close();
|
|
@@ -3073,6 +3841,77 @@ async function startHttpServer(opts) {
|
|
|
3073
3841
|
res.writeHead(202, { "content-type": "application/json" });
|
|
3074
3842
|
res.end(JSON.stringify({ status: "fired" }));
|
|
3075
3843
|
}
|
|
3844
|
+
async function handleFileUpload(req, res, url) {
|
|
3845
|
+
const reply = (status, body) => {
|
|
3846
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
3847
|
+
res.end(JSON.stringify(body));
|
|
3848
|
+
};
|
|
3849
|
+
const qs = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
|
|
3850
|
+
const cwd = qs.get("cwd");
|
|
3851
|
+
const rawName = qs.get("name");
|
|
3852
|
+
if (!cwd || !rawName) {
|
|
3853
|
+
reply(400, { error: "missing_cwd_or_name" });
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (!isAbsolute(cwd)) {
|
|
3857
|
+
reply(400, { error: "cwd_not_absolute" });
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
const safeName = rawName.replace(/[/\\]/g, "_").replace(/\.\.+/g, ".").replace(/^\.+/, "").slice(0, 200);
|
|
3861
|
+
if (safeName.length === 0) {
|
|
3862
|
+
reply(400, { error: "invalid_name" });
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
try {
|
|
3866
|
+
const st = await stat(cwd);
|
|
3867
|
+
if (!st.isDirectory()) {
|
|
3868
|
+
reply(400, { error: "cwd_not_a_directory" });
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
} catch {
|
|
3872
|
+
reply(400, { error: "cwd_not_found" });
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
const dir = join(cwd, ".agentproto-attachments");
|
|
3876
|
+
const target = resolve(dir, safeName);
|
|
3877
|
+
if (!target.startsWith(resolve(dir) + (dir.endsWith("/") ? "" : "/"))) {
|
|
3878
|
+
reply(400, { error: "path_traversal_blocked" });
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
3882
|
+
const chunks = [];
|
|
3883
|
+
let total = 0;
|
|
3884
|
+
let aborted = false;
|
|
3885
|
+
await new Promise((resolveBody, rejectBody) => {
|
|
3886
|
+
req.on("data", (chunk) => {
|
|
3887
|
+
if (aborted) return;
|
|
3888
|
+
const buf = chunk;
|
|
3889
|
+
total += buf.length;
|
|
3890
|
+
if (total > MAX_BYTES) {
|
|
3891
|
+
aborted = true;
|
|
3892
|
+
reply(413, { error: "file_too_large", maxBytes: MAX_BYTES });
|
|
3893
|
+
rejectBody(new Error("too_large"));
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
chunks.push(buf);
|
|
3897
|
+
});
|
|
3898
|
+
req.on("end", () => {
|
|
3899
|
+
if (!aborted) resolveBody();
|
|
3900
|
+
});
|
|
3901
|
+
req.on("error", (err) => rejectBody(err));
|
|
3902
|
+
}).catch(() => void 0);
|
|
3903
|
+
if (aborted) return;
|
|
3904
|
+
try {
|
|
3905
|
+
await mkdir(dir, { recursive: true });
|
|
3906
|
+
await writeFile(target, Buffer.concat(chunks));
|
|
3907
|
+
reply(200, { path: target, bytes: total });
|
|
3908
|
+
} catch (err) {
|
|
3909
|
+
reply(500, {
|
|
3910
|
+
error: "write_failed",
|
|
3911
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3076
3915
|
function applyCors(req, res) {
|
|
3077
3916
|
const origin = req.headers.origin ?? "*";
|
|
3078
3917
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
@@ -3086,6 +3925,9 @@ async function startHttpServer(opts) {
|
|
|
3086
3925
|
"Access-Control-Allow-Methods",
|
|
3087
3926
|
"GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
|
3088
3927
|
);
|
|
3928
|
+
if (req.headers["access-control-request-private-network"] === "true") {
|
|
3929
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
3930
|
+
}
|
|
3089
3931
|
}
|
|
3090
3932
|
const server = createServer((req, res) => {
|
|
3091
3933
|
void (async () => {
|
|
@@ -3130,19 +3972,36 @@ async function startHttpServer(opts) {
|
|
|
3130
3972
|
await handleHeartbeatTick(req, res);
|
|
3131
3973
|
return;
|
|
3132
3974
|
}
|
|
3975
|
+
if (path === "/files/upload" && req.method === "POST") {
|
|
3976
|
+
const gate = checkSessionsToken(req);
|
|
3977
|
+
if (gate !== "ok") {
|
|
3978
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
await handleFileUpload(req, res, url);
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3133
3984
|
if (opts.sessions && path.startsWith("/sessions")) {
|
|
3985
|
+
if (isMutatingSessionsRoute(req.method ?? "GET", path)) {
|
|
3986
|
+
const gate = checkSessionsToken(req);
|
|
3987
|
+
if (gate !== "ok") {
|
|
3988
|
+
rejectUnauthorizedSession(req, res, gate);
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3134
3992
|
const handled = await handleSessions(
|
|
3135
3993
|
req,
|
|
3136
3994
|
res,
|
|
3137
3995
|
path,
|
|
3138
3996
|
opts.sessions,
|
|
3139
|
-
opts.resolveAgentAdapter
|
|
3997
|
+
opts.resolveAgentAdapter,
|
|
3998
|
+
opts.ptyEnabled === true
|
|
3140
3999
|
);
|
|
3141
4000
|
if (handled) return;
|
|
3142
4001
|
}
|
|
3143
4002
|
if (path === "/workspaces" && req.method === "GET") {
|
|
3144
4003
|
try {
|
|
3145
|
-
const config = await
|
|
4004
|
+
const config = await loadWorkspacesConfig2();
|
|
3146
4005
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3147
4006
|
res.end(JSON.stringify(config));
|
|
3148
4007
|
} catch (err) {
|
|
@@ -3326,19 +4185,179 @@ async function startHttpServer(opts) {
|
|
|
3326
4185
|
}
|
|
3327
4186
|
})();
|
|
3328
4187
|
});
|
|
4188
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
4189
|
+
server.on("upgrade", (req, socket, head) => {
|
|
4190
|
+
const url = req.url ?? "/";
|
|
4191
|
+
const path = url.split("?")[0] ?? "/";
|
|
4192
|
+
const ptyMatch = path.match(/^\/sessions\/([^/]+)\/pty$/);
|
|
4193
|
+
if (!ptyMatch) {
|
|
4194
|
+
socket.destroy();
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (!opts.sessions || opts.ptyEnabled !== true) {
|
|
4198
|
+
rejectUpgrade(socket, 501, "pty_not_configured");
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
const gate = checkSessionsToken(req);
|
|
4202
|
+
if (gate !== "ok") {
|
|
4203
|
+
rejectUpgrade(socket, 401, gate === "missing" ? "missing_token" : "bad_token");
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
const id = ptyMatch[1];
|
|
4207
|
+
if (!id) {
|
|
4208
|
+
rejectUpgrade(socket, 400, "missing_id");
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
const desc = opts.sessions.findByIdOrName(id);
|
|
4212
|
+
if (!desc) {
|
|
4213
|
+
rejectUpgrade(socket, 404, "session_not_found");
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
4216
|
+
if (desc.pty !== true) {
|
|
4217
|
+
rejectUpgrade(socket, 400, "session_not_pty");
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (desc.status === "exited" || desc.status === "killed" || desc.status === "error") {
|
|
4221
|
+
rejectUpgrade(
|
|
4222
|
+
socket,
|
|
4223
|
+
410,
|
|
4224
|
+
`session_${desc.status}`,
|
|
4225
|
+
`Session ${desc.id}${desc.name ? ` (${desc.name})` : ""} has ${desc.status}. Spawn a fresh one with: agentproto sessions restart ${desc.name ?? desc.id}`
|
|
4226
|
+
);
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
4230
|
+
handlePtyWebSocket(ws, req, desc.id, opts.sessions);
|
|
4231
|
+
});
|
|
4232
|
+
});
|
|
3329
4233
|
const bind = opts.bind ?? "127.0.0.1";
|
|
3330
|
-
await new Promise((
|
|
4234
|
+
await new Promise((resolve8, reject) => {
|
|
3331
4235
|
server.once("error", reject);
|
|
3332
|
-
server.listen(opts.port, bind, () =>
|
|
4236
|
+
server.listen(opts.port, bind, () => resolve8());
|
|
3333
4237
|
});
|
|
3334
4238
|
return {
|
|
3335
4239
|
url: `http://${bind}:${opts.port}`,
|
|
3336
4240
|
async stop() {
|
|
3337
|
-
|
|
4241
|
+
wss.close();
|
|
4242
|
+
await new Promise((resolve8) => server.close(() => resolve8()));
|
|
3338
4243
|
}
|
|
3339
4244
|
};
|
|
3340
4245
|
}
|
|
3341
|
-
|
|
4246
|
+
function rejectUpgrade(socket, status, reason, message) {
|
|
4247
|
+
const reasonText = `${status} ${reason}`;
|
|
4248
|
+
const body = { error: reason };
|
|
4249
|
+
if (message) body.message = message;
|
|
4250
|
+
try {
|
|
4251
|
+
socket.write(
|
|
4252
|
+
`HTTP/1.1 ${reasonText}\r
|
|
4253
|
+
Content-Type: application/json\r
|
|
4254
|
+
Connection: close\r
|
|
4255
|
+
\r
|
|
4256
|
+
` + JSON.stringify(body) + `
|
|
4257
|
+
`
|
|
4258
|
+
);
|
|
4259
|
+
} catch {
|
|
4260
|
+
}
|
|
4261
|
+
socket.destroy();
|
|
4262
|
+
}
|
|
4263
|
+
function handlePtyWebSocket(ws, req, sessionId, registry) {
|
|
4264
|
+
const query = new URLSearchParams(
|
|
4265
|
+
(req.url ?? "").includes("?") ? (req.url ?? "").split("?")[1] : ""
|
|
4266
|
+
);
|
|
4267
|
+
const cols = clampPositiveInt(query.get("cols"), 80);
|
|
4268
|
+
const rows = clampPositiveInt(query.get("rows"), 24);
|
|
4269
|
+
const BACKPRESSURE_BYTES = 256 * 1024;
|
|
4270
|
+
const handle = registry.attachPty(
|
|
4271
|
+
sessionId,
|
|
4272
|
+
{ cols, rows },
|
|
4273
|
+
(chunk) => {
|
|
4274
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
4275
|
+
if (ws.bufferedAmount > BACKPRESSURE_BYTES) return;
|
|
4276
|
+
ws.send(
|
|
4277
|
+
JSON.stringify({ kind: "data", b64: chunk.toString("base64") })
|
|
4278
|
+
);
|
|
4279
|
+
},
|
|
4280
|
+
(evt) => {
|
|
4281
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
4282
|
+
ws.send(
|
|
4283
|
+
JSON.stringify({
|
|
4284
|
+
kind: "exit",
|
|
4285
|
+
exitCode: evt.exitCode,
|
|
4286
|
+
...evt.signal != null ? { signal: evt.signal } : {}
|
|
4287
|
+
})
|
|
4288
|
+
);
|
|
4289
|
+
try {
|
|
4290
|
+
ws.close(1e3, "session exited");
|
|
4291
|
+
} catch {
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
);
|
|
4295
|
+
if (!handle) {
|
|
4296
|
+
try {
|
|
4297
|
+
ws.close(1011, "session not attachable");
|
|
4298
|
+
} catch {
|
|
4299
|
+
}
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
ws.on("message", (raw, isBinary) => {
|
|
4303
|
+
if (isBinary) {
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
let frame;
|
|
4307
|
+
try {
|
|
4308
|
+
frame = JSON.parse(raw.toString("utf8"));
|
|
4309
|
+
} catch {
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
if (!frame || typeof frame !== "object") return;
|
|
4313
|
+
const f = frame;
|
|
4314
|
+
switch (f.kind) {
|
|
4315
|
+
case "input": {
|
|
4316
|
+
if (typeof f.text === "string") {
|
|
4317
|
+
handle.write(f.text);
|
|
4318
|
+
} else if (typeof f.b64 === "string") {
|
|
4319
|
+
try {
|
|
4320
|
+
handle.write(Buffer.from(f.b64, "base64").toString("utf8"));
|
|
4321
|
+
} catch {
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
break;
|
|
4325
|
+
}
|
|
4326
|
+
case "resize": {
|
|
4327
|
+
const c = typeof f.cols === "number" && f.cols > 0 ? Math.floor(f.cols) : null;
|
|
4328
|
+
const r = typeof f.rows === "number" && f.rows > 0 ? Math.floor(f.rows) : null;
|
|
4329
|
+
if (c && r) handle.resize(c, r);
|
|
4330
|
+
break;
|
|
4331
|
+
}
|
|
4332
|
+
case "ping":
|
|
4333
|
+
try {
|
|
4334
|
+
ws.send(JSON.stringify({ kind: "pong" }));
|
|
4335
|
+
} catch {
|
|
4336
|
+
}
|
|
4337
|
+
break;
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
ws.on("close", () => {
|
|
4341
|
+
handle.detach();
|
|
4342
|
+
});
|
|
4343
|
+
ws.on("error", () => {
|
|
4344
|
+
handle.detach();
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
function clampPositiveInt(raw, fallback) {
|
|
4348
|
+
if (!raw) return fallback;
|
|
4349
|
+
const n = Number.parseInt(raw, 10);
|
|
4350
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
4351
|
+
}
|
|
4352
|
+
function clampInt(raw, fallback, min, max) {
|
|
4353
|
+
if (!raw) return fallback;
|
|
4354
|
+
const n = Number.parseInt(raw, 10);
|
|
4355
|
+
if (!Number.isFinite(n)) return fallback;
|
|
4356
|
+
if (n < min) return min;
|
|
4357
|
+
if (n > max) return max;
|
|
4358
|
+
return n;
|
|
4359
|
+
}
|
|
4360
|
+
async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false) {
|
|
3342
4361
|
const json = (status, body) => {
|
|
3343
4362
|
res.writeHead(status, { "content-type": "application/json" });
|
|
3344
4363
|
res.end(JSON.stringify(body));
|
|
@@ -3370,7 +4389,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
|
|
|
3370
4389
|
let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
|
|
3371
4390
|
if (!cwd) {
|
|
3372
4391
|
try {
|
|
3373
|
-
const config = await
|
|
4392
|
+
const config = await loadWorkspacesConfig2();
|
|
3374
4393
|
const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
|
|
3375
4394
|
if (ws) {
|
|
3376
4395
|
cwd = ws.path;
|
|
@@ -3392,7 +4411,11 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
|
|
|
3392
4411
|
return true;
|
|
3393
4412
|
}
|
|
3394
4413
|
try {
|
|
3395
|
-
const
|
|
4414
|
+
const resumeSessionId = typeof b.resumeSessionId === "string" && b.resumeSessionId.length > 0 ? b.resumeSessionId : void 0;
|
|
4415
|
+
const agentSession = await resolved.startSession({
|
|
4416
|
+
cwd,
|
|
4417
|
+
...resumeSessionId ? { resumeSessionId } : {}
|
|
4418
|
+
});
|
|
3396
4419
|
const desc = registry.spawnAgent({
|
|
3397
4420
|
workspaceSlug,
|
|
3398
4421
|
cwd,
|
|
@@ -3411,14 +4434,78 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
|
|
|
3411
4434
|
}
|
|
3412
4435
|
return true;
|
|
3413
4436
|
}
|
|
4437
|
+
if (path === "/sessions/terminal" && req.method === "POST") {
|
|
4438
|
+
if (!ptyEnabled) {
|
|
4439
|
+
json(501, {
|
|
4440
|
+
error: "pty_not_configured",
|
|
4441
|
+
message: "POST /sessions/terminal needs the host to inject `spawnPty` into createGateway (node-pty optional dep \u2014 install in @agentproto/cli)."
|
|
4442
|
+
});
|
|
4443
|
+
return true;
|
|
4444
|
+
}
|
|
4445
|
+
const body = await readJsonBody(req);
|
|
4446
|
+
if (!body || typeof body !== "object") {
|
|
4447
|
+
json(400, { error: "invalid_body" });
|
|
4448
|
+
return true;
|
|
4449
|
+
}
|
|
4450
|
+
const b = body;
|
|
4451
|
+
const argv = Array.isArray(b.argv) ? b.argv.filter((v) => typeof v === "string") : [];
|
|
4452
|
+
if (argv.length === 0) {
|
|
4453
|
+
json(400, { error: "missing_argv" });
|
|
4454
|
+
return true;
|
|
4455
|
+
}
|
|
4456
|
+
const cols = typeof b.cols === "number" && b.cols > 0 ? Math.floor(b.cols) : 80;
|
|
4457
|
+
const rows = typeof b.rows === "number" && b.rows > 0 ? Math.floor(b.rows) : 24;
|
|
4458
|
+
let cwd = typeof b.cwd === "string" && b.cwd.length > 0 ? b.cwd : null;
|
|
4459
|
+
let workspaceSlug = typeof b.workspaceSlug === "string" ? b.workspaceSlug : "";
|
|
4460
|
+
if (!cwd) {
|
|
4461
|
+
try {
|
|
4462
|
+
const config = await loadWorkspacesConfig2();
|
|
4463
|
+
const ws = workspaceSlug ? findWorkspace(config, workspaceSlug) : getActiveWorkspace(config);
|
|
4464
|
+
if (ws) {
|
|
4465
|
+
cwd = ws.path;
|
|
4466
|
+
workspaceSlug = ws.slug;
|
|
4467
|
+
}
|
|
4468
|
+
} catch {
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
if (!cwd) {
|
|
4472
|
+
cwd = process.cwd();
|
|
4473
|
+
console.warn(
|
|
4474
|
+
`[/sessions/terminal] no cwd resolvable for argv=${argv.join(" ")} \u2014 falling back to daemon's cwd ${cwd}`
|
|
4475
|
+
);
|
|
4476
|
+
}
|
|
4477
|
+
if (!workspaceSlug) workspaceSlug = "default";
|
|
4478
|
+
try {
|
|
4479
|
+
const desc = registry.spawnPty({
|
|
4480
|
+
argv,
|
|
4481
|
+
cwd,
|
|
4482
|
+
workspaceSlug,
|
|
4483
|
+
cols,
|
|
4484
|
+
rows,
|
|
4485
|
+
...b.env && typeof b.env === "object" ? { env: b.env } : {},
|
|
4486
|
+
...typeof b.name === "string" ? { name: b.name } : {},
|
|
4487
|
+
...typeof b.label === "string" ? { label: b.label } : {}
|
|
4488
|
+
});
|
|
4489
|
+
json(201, desc);
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4492
|
+
const status = msg.includes("already in use") ? 409 : msg.includes("no PTY factory") ? 501 : 500;
|
|
4493
|
+
json(status, { error: "pty_spawn_failed", message: msg });
|
|
4494
|
+
}
|
|
4495
|
+
return true;
|
|
4496
|
+
}
|
|
3414
4497
|
const promptMatch = path.match(/^\/sessions\/([^/]+)\/prompt$/);
|
|
3415
4498
|
if (promptMatch && req.method === "POST") {
|
|
3416
4499
|
const id2 = promptMatch[1];
|
|
3417
4500
|
if (!id2) return false;
|
|
3418
4501
|
const body = await readJsonBody(req);
|
|
3419
4502
|
const prompt = body?.prompt;
|
|
3420
|
-
|
|
3421
|
-
|
|
4503
|
+
const validPrompt = typeof prompt === "string" && prompt.length > 0 || Array.isArray(prompt) && prompt.length > 0 && prompt.every((b) => b !== null && typeof b === "object") || prompt !== null && typeof prompt === "object" && !Array.isArray(prompt);
|
|
4504
|
+
if (!validPrompt) {
|
|
4505
|
+
json(400, {
|
|
4506
|
+
error: "missing_prompt",
|
|
4507
|
+
message: "Body `prompt` must be a non-empty string, a content block object, or an array of content blocks."
|
|
4508
|
+
});
|
|
3422
4509
|
return true;
|
|
3423
4510
|
}
|
|
3424
4511
|
const reqUrl = req.url ?? "";
|
|
@@ -3470,14 +4557,43 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
|
|
|
3470
4557
|
}
|
|
3471
4558
|
return true;
|
|
3472
4559
|
}
|
|
3473
|
-
const idMatch = path.match(
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
if (!
|
|
4560
|
+
const idMatch = path.match(
|
|
4561
|
+
/^\/sessions\/([^/]+)(\/stream|\/kill|\/preview)?$/
|
|
4562
|
+
);
|
|
4563
|
+
if (!idMatch) return false;
|
|
4564
|
+
const [, rawIdOrName, suffix] = idMatch;
|
|
4565
|
+
if (!rawIdOrName) return false;
|
|
4566
|
+
const resolvedDesc = registry.findByIdOrName(rawIdOrName);
|
|
4567
|
+
const id = resolvedDesc?.id ?? rawIdOrName;
|
|
4568
|
+
if (suffix === "/preview" && req.method === "GET") {
|
|
4569
|
+
if (!resolvedDesc) {
|
|
4570
|
+
json(404, { error: "session_not_found", id: rawIdOrName });
|
|
4571
|
+
return true;
|
|
4572
|
+
}
|
|
4573
|
+
const reqUrl = req.url ?? "";
|
|
4574
|
+
const qs = new URLSearchParams(
|
|
4575
|
+
reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : ""
|
|
4576
|
+
);
|
|
4577
|
+
const lineCap = clampInt(qs.get("lines"), 10, 1, 200);
|
|
4578
|
+
const byteCap = clampInt(qs.get("bytes"), 16 * 1024, 256, 64 * 1024);
|
|
4579
|
+
const lines = [];
|
|
4580
|
+
if (resolvedDesc.pty !== true) {
|
|
4581
|
+
const unsub = registry.attach(id, (line) => {
|
|
4582
|
+
lines.push(line);
|
|
4583
|
+
});
|
|
4584
|
+
if (unsub) unsub();
|
|
4585
|
+
}
|
|
4586
|
+
const bufBytes = registry.readTerminalOutput(id, byteCap);
|
|
4587
|
+
json(200, {
|
|
4588
|
+
id: resolvedDesc.id,
|
|
4589
|
+
lines: lines.slice(-lineCap),
|
|
4590
|
+
bytes: bufBytes ? bufBytes.toString("base64") : null
|
|
4591
|
+
});
|
|
4592
|
+
return true;
|
|
4593
|
+
}
|
|
3477
4594
|
if (suffix === "/stream" && req.method === "GET") {
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
json(404, { error: "session_not_found", id });
|
|
4595
|
+
if (!resolvedDesc) {
|
|
4596
|
+
json(404, { error: "session_not_found", id: rawIdOrName });
|
|
3481
4597
|
return true;
|
|
3482
4598
|
}
|
|
3483
4599
|
res.writeHead(200, {
|
|
@@ -3521,12 +4637,11 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter) {
|
|
|
3521
4637
|
return true;
|
|
3522
4638
|
}
|
|
3523
4639
|
if (!suffix && req.method === "GET") {
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
json(404, { error: "session_not_found", id });
|
|
4640
|
+
if (!resolvedDesc) {
|
|
4641
|
+
json(404, { error: "session_not_found", id: rawIdOrName });
|
|
3527
4642
|
return true;
|
|
3528
4643
|
}
|
|
3529
|
-
json(200,
|
|
4644
|
+
json(200, resolvedDesc);
|
|
3530
4645
|
return true;
|
|
3531
4646
|
}
|
|
3532
4647
|
if (!suffix && req.method === "DELETE") {
|
|
@@ -3548,14 +4663,80 @@ async function readJsonBody(req) {
|
|
|
3548
4663
|
return null;
|
|
3549
4664
|
}
|
|
3550
4665
|
}
|
|
4666
|
+
var RESUME_STRATEGIES = {
|
|
4667
|
+
"claude-code": {
|
|
4668
|
+
// Printed by claude on graceful exit when session persistence
|
|
4669
|
+
// is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
|
|
4670
|
+
outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
|
|
4671
|
+
storeAs: "claudeResumeId",
|
|
4672
|
+
fsProbe: probeMtimeLatestJsonl(".claude/projects"),
|
|
4673
|
+
spawnArgs: (id) => ["claude", "--resume", id]
|
|
4674
|
+
}
|
|
4675
|
+
// Stubs for other shipped adapters — fill in as we learn each
|
|
4676
|
+
// provider's resume mechanism. Today they fall back to ACP-level
|
|
4677
|
+
// resume (whatever the agent-cli runtime supports) or fresh spawn.
|
|
4678
|
+
//
|
|
4679
|
+
// hermes: { storeAs: "hermesResumeId", ... }
|
|
4680
|
+
// codex: { storeAs: "codexResumeId", ... }
|
|
4681
|
+
// openclaw: { storeAs: "openClawResumeId", ... }
|
|
4682
|
+
// opencode: { storeAs: "openCodeResumeId", ... }
|
|
4683
|
+
};
|
|
4684
|
+
function probeMtimeLatestJsonl(storeRel) {
|
|
4685
|
+
return async (cwd, prevStartedAt) => {
|
|
4686
|
+
const encoded = cwd.replace(/\//g, "-");
|
|
4687
|
+
const dir = resolve(homedir(), storeRel, encoded);
|
|
4688
|
+
let entries;
|
|
4689
|
+
try {
|
|
4690
|
+
entries = await promises.readdir(dir);
|
|
4691
|
+
} catch {
|
|
4692
|
+
return null;
|
|
4693
|
+
}
|
|
4694
|
+
const jsonl = entries.filter((e) => e.endsWith(".jsonl"));
|
|
4695
|
+
if (jsonl.length === 0) return null;
|
|
4696
|
+
const startedAtMs = Date.parse(prevStartedAt);
|
|
4697
|
+
const candidates = [];
|
|
4698
|
+
for (const f of jsonl) {
|
|
4699
|
+
try {
|
|
4700
|
+
const st = await promises.stat(join(dir, f));
|
|
4701
|
+
if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1e3) {
|
|
4702
|
+
candidates.push({ name: f, mtime: st.mtimeMs });
|
|
4703
|
+
}
|
|
4704
|
+
} catch {
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
if (candidates.length === 0) return null;
|
|
4708
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
4709
|
+
return candidates[0].name.replace(/\.jsonl$/, "");
|
|
4710
|
+
};
|
|
4711
|
+
}
|
|
3551
4712
|
var SESSIONS_FILE_PATH = () => resolve(homedir(), ".agentproto", "sessions.json");
|
|
3552
4713
|
var RECENT_LINES_CAP = 500;
|
|
4714
|
+
var RECENT_BYTES_CAP = 64 * 1024;
|
|
3553
4715
|
var PERSIST_DEBOUNCE_MS = 1500;
|
|
4716
|
+
var HISTORY_CAP = 200;
|
|
4717
|
+
function stripAnsiCodes(s) {
|
|
4718
|
+
return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
4719
|
+
}
|
|
3554
4720
|
function createSessionsRegistry(opts) {
|
|
3555
|
-
const persistPath = SESSIONS_FILE_PATH();
|
|
4721
|
+
const persistPath = opts?.persistPath ?? SESSIONS_FILE_PATH();
|
|
4722
|
+
const persist = opts?.persist ?? true;
|
|
4723
|
+
const ptyFactory = opts?.spawnPty;
|
|
4724
|
+
const resumeAgent = opts?.resumeAgent;
|
|
3556
4725
|
const sessions = /* @__PURE__ */ new Map();
|
|
3557
4726
|
let persistTimer = null;
|
|
4727
|
+
let nextSubId = 1;
|
|
4728
|
+
let shutdownDone = false;
|
|
4729
|
+
if (persist) {
|
|
4730
|
+
loadHistorySnapshot(persistPath, sessions);
|
|
4731
|
+
}
|
|
4732
|
+
const onProcessExit = () => {
|
|
4733
|
+
shutdownImpl();
|
|
4734
|
+
};
|
|
4735
|
+
if (persist) {
|
|
4736
|
+
process.on("exit", onProcessExit);
|
|
4737
|
+
}
|
|
3558
4738
|
const schedulePersist = () => {
|
|
4739
|
+
if (!persist) return;
|
|
3559
4740
|
if (persistTimer) clearTimeout(persistTimer);
|
|
3560
4741
|
persistTimer = setTimeout(() => {
|
|
3561
4742
|
void persistSnapshot();
|
|
@@ -3581,8 +4762,47 @@ function createSessionsRegistry(opts) {
|
|
|
3581
4762
|
rt.recentLines.splice(0, rt.recentLines.length - RECENT_LINES_CAP);
|
|
3582
4763
|
}
|
|
3583
4764
|
rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4765
|
+
sniffResumeHints(rt, line);
|
|
3584
4766
|
rt.emitter.emit("line", { line, stream });
|
|
3585
4767
|
};
|
|
4768
|
+
const sniffResumeHints = (rt, line) => {
|
|
4769
|
+
if (rt.desc.kind !== "agent-cli") return;
|
|
4770
|
+
const strategy = rt.desc.adapterSlug ? RESUME_STRATEGIES[rt.desc.adapterSlug] : void 0;
|
|
4771
|
+
if (!strategy?.outputHint) return;
|
|
4772
|
+
const plain = stripAnsiCodes(line);
|
|
4773
|
+
const m = plain.match(strategy.outputHint);
|
|
4774
|
+
if (m && m[1]) {
|
|
4775
|
+
rt.desc.resumeMetadata = {
|
|
4776
|
+
...rt.desc.resumeMetadata ?? {},
|
|
4777
|
+
[strategy.storeAs]: m[1]
|
|
4778
|
+
};
|
|
4779
|
+
schedulePersist();
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4782
|
+
const appendBytes = (rt, chunk) => {
|
|
4783
|
+
rt.recentBytes.push(chunk);
|
|
4784
|
+
rt.recentBytesSize += chunk.byteLength;
|
|
4785
|
+
while (rt.recentBytesSize > RECENT_BYTES_CAP && rt.recentBytes.length > 1) {
|
|
4786
|
+
const dropped = rt.recentBytes.shift();
|
|
4787
|
+
if (dropped) rt.recentBytesSize -= dropped.byteLength;
|
|
4788
|
+
}
|
|
4789
|
+
rt.desc.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4790
|
+
rt.emitter.emit("data", chunk);
|
|
4791
|
+
};
|
|
4792
|
+
const reconcilePtySize = (rt) => {
|
|
4793
|
+
if (!rt.pty || !rt.ptySubscribers || rt.ptySubscribers.size === 0) return;
|
|
4794
|
+
let cols = Infinity;
|
|
4795
|
+
let rows = Infinity;
|
|
4796
|
+
for (const dim of rt.ptySubscribers.values()) {
|
|
4797
|
+
if (dim.cols < cols) cols = dim.cols;
|
|
4798
|
+
if (dim.rows < rows) rows = dim.rows;
|
|
4799
|
+
}
|
|
4800
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) return;
|
|
4801
|
+
try {
|
|
4802
|
+
rt.pty.resize(cols, rows);
|
|
4803
|
+
} catch {
|
|
4804
|
+
}
|
|
4805
|
+
};
|
|
3586
4806
|
const projectEvent = (rt, evt) => {
|
|
3587
4807
|
switch (evt.kind) {
|
|
3588
4808
|
case "text-delta":
|
|
@@ -3671,6 +4891,59 @@ function createSessionsRegistry(opts) {
|
|
|
3671
4891
|
}
|
|
3672
4892
|
return rt;
|
|
3673
4893
|
};
|
|
4894
|
+
const maybeResumeAgent = async (rt) => {
|
|
4895
|
+
if (rt.agentSession) return;
|
|
4896
|
+
if (!resumeAgent) return;
|
|
4897
|
+
if (rt.desc.kind !== "agent-cli") return;
|
|
4898
|
+
const adapterSlug = rt.desc.adapterSlug ?? rt.adapterSlug;
|
|
4899
|
+
const adapterSessionId = rt.desc.adapterSessionId;
|
|
4900
|
+
const cwd = rt.desc.cwd;
|
|
4901
|
+
if (!adapterSlug || !adapterSessionId || !cwd) return;
|
|
4902
|
+
if (rt.resumePromise) {
|
|
4903
|
+
await rt.resumePromise;
|
|
4904
|
+
return;
|
|
4905
|
+
}
|
|
4906
|
+
rt.resumePromise = (async () => {
|
|
4907
|
+
appendLine(
|
|
4908
|
+
rt,
|
|
4909
|
+
`\u2500\u2500 resuming ${adapterSlug} session ${adapterSessionId} \u2500\u2500`,
|
|
4910
|
+
"stdout"
|
|
4911
|
+
);
|
|
4912
|
+
try {
|
|
4913
|
+
const fresh = await resumeAgent({
|
|
4914
|
+
adapterSlug,
|
|
4915
|
+
cwd,
|
|
4916
|
+
resumeSessionId: adapterSessionId
|
|
4917
|
+
});
|
|
4918
|
+
if (!fresh) {
|
|
4919
|
+
appendLine(
|
|
4920
|
+
rt,
|
|
4921
|
+
`[error] resume failed: adapter '${adapterSlug}' returned null`,
|
|
4922
|
+
"stderr"
|
|
4923
|
+
);
|
|
4924
|
+
return;
|
|
4925
|
+
}
|
|
4926
|
+
rt.agentSession = fresh;
|
|
4927
|
+
rt.adapterSlug = adapterSlug;
|
|
4928
|
+
rt.desc.adapterSessionId = fresh.sessionId;
|
|
4929
|
+
if (rt.desc.status !== "running") {
|
|
4930
|
+
rt.desc.status = "running";
|
|
4931
|
+
delete rt.desc.endedAt;
|
|
4932
|
+
delete rt.desc.exitCode;
|
|
4933
|
+
rt.emitter.emit("status", rt.desc.status);
|
|
4934
|
+
}
|
|
4935
|
+
schedulePersist();
|
|
4936
|
+
} catch (err) {
|
|
4937
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4938
|
+
appendLine(rt, `[error] resume failed: ${msg}`, "stderr");
|
|
4939
|
+
}
|
|
4940
|
+
})();
|
|
4941
|
+
try {
|
|
4942
|
+
await rt.resumePromise;
|
|
4943
|
+
} finally {
|
|
4944
|
+
rt.resumePromise = void 0;
|
|
4945
|
+
}
|
|
4946
|
+
};
|
|
3674
4947
|
const runAgentTurn = async (rt, message) => {
|
|
3675
4948
|
if (!rt.agentSession) {
|
|
3676
4949
|
throw new Error("runAgentTurn: session has no agentSession");
|
|
@@ -3734,12 +5007,16 @@ function createSessionsRegistry(opts) {
|
|
|
3734
5007
|
pid: child.pid ?? null,
|
|
3735
5008
|
status: "starting",
|
|
3736
5009
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5010
|
+
argv: input2.argv,
|
|
5011
|
+
cwd: input2.cwd,
|
|
3737
5012
|
...input2.label ? { label: input2.label } : {}
|
|
3738
5013
|
};
|
|
3739
5014
|
const rt = {
|
|
3740
5015
|
desc,
|
|
3741
5016
|
child,
|
|
3742
5017
|
recentLines: [],
|
|
5018
|
+
recentBytes: [],
|
|
5019
|
+
recentBytesSize: 0,
|
|
3743
5020
|
emitter: new EventEmitter(),
|
|
3744
5021
|
busy: false,
|
|
3745
5022
|
textBuf: "",
|
|
@@ -3792,6 +5069,8 @@ function createSessionsRegistry(opts) {
|
|
|
3792
5069
|
desc,
|
|
3793
5070
|
child: input2.child,
|
|
3794
5071
|
recentLines: [],
|
|
5072
|
+
recentBytes: [],
|
|
5073
|
+
recentBytesSize: 0,
|
|
3795
5074
|
emitter: new EventEmitter(),
|
|
3796
5075
|
busy: false,
|
|
3797
5076
|
textBuf: "",
|
|
@@ -3829,6 +5108,13 @@ function createSessionsRegistry(opts) {
|
|
|
3829
5108
|
status: "running",
|
|
3830
5109
|
// driver already started the session
|
|
3831
5110
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5111
|
+
cwd: input2.cwd,
|
|
5112
|
+
adapterSlug: input2.adapterSlug,
|
|
5113
|
+
// ACP-level session id — sticks across daemon restart so
|
|
5114
|
+
// `agentproto sessions restart <id>` can pass it as
|
|
5115
|
+
// `resumeSessionId` and the adapter reattaches to the prior
|
|
5116
|
+
// conversation rather than starting blank.
|
|
5117
|
+
adapterSessionId: input2.agentSession.sessionId,
|
|
3832
5118
|
...input2.label ? { label: input2.label } : {}
|
|
3833
5119
|
};
|
|
3834
5120
|
const rt = {
|
|
@@ -3836,6 +5122,8 @@ function createSessionsRegistry(opts) {
|
|
|
3836
5122
|
agentSession: input2.agentSession,
|
|
3837
5123
|
adapterSlug: input2.adapterSlug,
|
|
3838
5124
|
recentLines: [],
|
|
5125
|
+
recentBytes: [],
|
|
5126
|
+
recentBytesSize: 0,
|
|
3839
5127
|
emitter: new EventEmitter(),
|
|
3840
5128
|
busy: false,
|
|
3841
5129
|
textBuf: "",
|
|
@@ -3860,14 +5148,117 @@ function createSessionsRegistry(opts) {
|
|
|
3860
5148
|
}
|
|
3861
5149
|
return desc;
|
|
3862
5150
|
},
|
|
5151
|
+
spawnPty(input2) {
|
|
5152
|
+
if (!ptyFactory) {
|
|
5153
|
+
throw new Error(
|
|
5154
|
+
"sessions.spawnPty: no PTY factory configured (node-pty optional dep missing in the daemon)"
|
|
5155
|
+
);
|
|
5156
|
+
}
|
|
5157
|
+
const id = `sess_${randomUUID().slice(0, 8)}`;
|
|
5158
|
+
if (input2.name) {
|
|
5159
|
+
for (const rt2 of sessions.values()) {
|
|
5160
|
+
if (rt2.desc.name !== input2.name) continue;
|
|
5161
|
+
const status = rt2.desc.status;
|
|
5162
|
+
if (status === "running" || status === "starting") {
|
|
5163
|
+
throw new Error(
|
|
5164
|
+
`sessions.spawnPty: name "${input2.name}" already in use by ${rt2.desc.id} (status=${status})`
|
|
5165
|
+
);
|
|
5166
|
+
}
|
|
5167
|
+
rt2.desc.name = void 0;
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
const [bin, ...args] = input2.argv;
|
|
5171
|
+
if (!bin) {
|
|
5172
|
+
throw new Error("sessions.spawnPty: argv must include a binary");
|
|
5173
|
+
}
|
|
5174
|
+
const env = { ...process.env, ...input2.env ?? {} };
|
|
5175
|
+
delete env.NODE_OPTIONS;
|
|
5176
|
+
const pty = ptyFactory({
|
|
5177
|
+
command: bin,
|
|
5178
|
+
args,
|
|
5179
|
+
cwd: input2.cwd,
|
|
5180
|
+
env,
|
|
5181
|
+
cols: input2.cols,
|
|
5182
|
+
rows: input2.rows
|
|
5183
|
+
});
|
|
5184
|
+
const desc = {
|
|
5185
|
+
id,
|
|
5186
|
+
kind: "terminal",
|
|
5187
|
+
workspaceSlug: input2.workspaceSlug,
|
|
5188
|
+
command: [bin, ...args].map(quoteArg).join(" "),
|
|
5189
|
+
pid: pty.pid,
|
|
5190
|
+
status: "running",
|
|
5191
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5192
|
+
pty: true,
|
|
5193
|
+
argv: input2.argv,
|
|
5194
|
+
cwd: input2.cwd,
|
|
5195
|
+
...input2.name ? { name: input2.name } : {},
|
|
5196
|
+
...input2.label ? { label: input2.label } : {}
|
|
5197
|
+
};
|
|
5198
|
+
const rt = {
|
|
5199
|
+
desc,
|
|
5200
|
+
pty,
|
|
5201
|
+
ptySubscribers: /* @__PURE__ */ new Map(),
|
|
5202
|
+
recentLines: [],
|
|
5203
|
+
recentBytes: [],
|
|
5204
|
+
recentBytesSize: 0,
|
|
5205
|
+
emitter: new EventEmitter(),
|
|
5206
|
+
busy: false,
|
|
5207
|
+
textBuf: "",
|
|
5208
|
+
thoughtBuf: ""
|
|
5209
|
+
};
|
|
5210
|
+
rt.emitter.setMaxListeners(50);
|
|
5211
|
+
sessions.set(id, rt);
|
|
5212
|
+
pty.onData((chunk) => {
|
|
5213
|
+
appendBytes(rt, Buffer.from(chunk, "utf8"));
|
|
5214
|
+
});
|
|
5215
|
+
pty.onExit((evt) => {
|
|
5216
|
+
if (rt.desc.status !== "killed") {
|
|
5217
|
+
rt.desc.status = "exited";
|
|
5218
|
+
}
|
|
5219
|
+
rt.desc.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5220
|
+
if (typeof evt.exitCode === "number") rt.desc.exitCode = evt.exitCode;
|
|
5221
|
+
rt.emitter.emit("exit", evt);
|
|
5222
|
+
rt.emitter.emit("status", rt.desc.status);
|
|
5223
|
+
schedulePersist();
|
|
5224
|
+
});
|
|
5225
|
+
schedulePersist();
|
|
5226
|
+
return desc;
|
|
5227
|
+
},
|
|
3863
5228
|
async sendPrompt(id, message) {
|
|
5229
|
+
const rtPre = sessions.get(id);
|
|
5230
|
+
if (rtPre) await maybeResumeAgent(rtPre);
|
|
3864
5231
|
const rt = validateAgentTurn(id, "sendPrompt");
|
|
3865
5232
|
await runAgentTurn(rt, message);
|
|
3866
5233
|
},
|
|
3867
5234
|
enqueuePrompt(id, message) {
|
|
3868
|
-
const
|
|
3869
|
-
|
|
3870
|
-
|
|
5235
|
+
const rtPre = sessions.get(id);
|
|
5236
|
+
if (!rtPre) {
|
|
5237
|
+
throw new Error(`enqueuePrompt: no session "${id}"`);
|
|
5238
|
+
}
|
|
5239
|
+
if (rtPre.desc.kind !== "agent-cli") {
|
|
5240
|
+
throw new Error(
|
|
5241
|
+
`enqueuePrompt: session "${id}" is not an agent session (kind=${rtPre.desc.kind})`
|
|
5242
|
+
);
|
|
5243
|
+
}
|
|
5244
|
+
if (rtPre.busy) {
|
|
5245
|
+
throw new Error(
|
|
5246
|
+
`enqueuePrompt: session "${id}" is mid-turn \u2014 wait for it to finish or cancel`
|
|
5247
|
+
);
|
|
5248
|
+
}
|
|
5249
|
+
void (async () => {
|
|
5250
|
+
try {
|
|
5251
|
+
await maybeResumeAgent(rtPre);
|
|
5252
|
+
const rt = validateAgentTurn(id, "enqueuePrompt");
|
|
5253
|
+
await runAgentTurn(rt, message);
|
|
5254
|
+
} catch (err) {
|
|
5255
|
+
appendLine(
|
|
5256
|
+
rtPre,
|
|
5257
|
+
`[error] ${err instanceof Error ? err.message : String(err)}`,
|
|
5258
|
+
"stderr"
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
})();
|
|
3871
5262
|
},
|
|
3872
5263
|
list() {
|
|
3873
5264
|
return Array.from(sessions.values()).map((s) => s.desc).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
@@ -3883,6 +5274,65 @@ function createSessionsRegistry(opts) {
|
|
|
3883
5274
|
rt.emitter.on("line", handler);
|
|
3884
5275
|
return () => rt.emitter.off("line", handler);
|
|
3885
5276
|
},
|
|
5277
|
+
attachPty(id, initial, onData, onExit) {
|
|
5278
|
+
const rt = sessions.get(id);
|
|
5279
|
+
if (!rt || !rt.pty || !rt.ptySubscribers) return null;
|
|
5280
|
+
for (const chunk of rt.recentBytes) onData(chunk);
|
|
5281
|
+
const subId = nextSubId++;
|
|
5282
|
+
rt.ptySubscribers.set(subId, { cols: initial.cols, rows: initial.rows });
|
|
5283
|
+
reconcilePtySize(rt);
|
|
5284
|
+
const dataHandler = (chunk) => onData(chunk);
|
|
5285
|
+
const exitHandler = (evt) => onExit(evt);
|
|
5286
|
+
rt.emitter.on("data", dataHandler);
|
|
5287
|
+
rt.emitter.once("exit", exitHandler);
|
|
5288
|
+
return {
|
|
5289
|
+
write(data) {
|
|
5290
|
+
if (!rt.pty) return;
|
|
5291
|
+
try {
|
|
5292
|
+
rt.pty.write(data);
|
|
5293
|
+
} catch {
|
|
5294
|
+
}
|
|
5295
|
+
},
|
|
5296
|
+
resize(cols, rows) {
|
|
5297
|
+
if (!rt.ptySubscribers) return;
|
|
5298
|
+
rt.ptySubscribers.set(subId, { cols, rows });
|
|
5299
|
+
reconcilePtySize(rt);
|
|
5300
|
+
},
|
|
5301
|
+
detach() {
|
|
5302
|
+
rt.emitter.off("data", dataHandler);
|
|
5303
|
+
rt.emitter.off("exit", exitHandler);
|
|
5304
|
+
rt.ptySubscribers?.delete(subId);
|
|
5305
|
+
reconcilePtySize(rt);
|
|
5306
|
+
}
|
|
5307
|
+
};
|
|
5308
|
+
},
|
|
5309
|
+
findByIdOrName(query) {
|
|
5310
|
+
const direct = sessions.get(query);
|
|
5311
|
+
if (direct) return direct.desc;
|
|
5312
|
+
for (const rt of sessions.values()) {
|
|
5313
|
+
if (rt.desc.name === query) return rt.desc;
|
|
5314
|
+
}
|
|
5315
|
+
return void 0;
|
|
5316
|
+
},
|
|
5317
|
+
writeTerminalInput(id, data) {
|
|
5318
|
+
const rt = sessions.get(id);
|
|
5319
|
+
if (!rt || !rt.pty) return false;
|
|
5320
|
+
try {
|
|
5321
|
+
rt.pty.write(data);
|
|
5322
|
+
return true;
|
|
5323
|
+
} catch {
|
|
5324
|
+
return false;
|
|
5325
|
+
}
|
|
5326
|
+
},
|
|
5327
|
+
readTerminalOutput(id, lastBytes) {
|
|
5328
|
+
const rt = sessions.get(id);
|
|
5329
|
+
if (!rt || !rt.pty) return null;
|
|
5330
|
+
const joined = Buffer.concat(rt.recentBytes, rt.recentBytesSize);
|
|
5331
|
+
if (typeof lastBytes === "number" && lastBytes > 0 && joined.byteLength > lastBytes) {
|
|
5332
|
+
return joined.subarray(joined.byteLength - lastBytes);
|
|
5333
|
+
}
|
|
5334
|
+
return joined;
|
|
5335
|
+
},
|
|
3886
5336
|
kill(id, signal = "SIGTERM") {
|
|
3887
5337
|
const rt = sessions.get(id);
|
|
3888
5338
|
if (!rt) return false;
|
|
@@ -3894,6 +5344,12 @@ function createSessionsRegistry(opts) {
|
|
|
3894
5344
|
if (rt.agentSession) {
|
|
3895
5345
|
void rt.agentSession.close().catch(() => void 0);
|
|
3896
5346
|
}
|
|
5347
|
+
if (rt.pty) {
|
|
5348
|
+
try {
|
|
5349
|
+
rt.pty.kill(signal);
|
|
5350
|
+
} catch {
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
3897
5353
|
rt.child?.kill(signal);
|
|
3898
5354
|
schedulePersist();
|
|
3899
5355
|
return true;
|
|
@@ -3907,19 +5363,91 @@ function createSessionsRegistry(opts) {
|
|
|
3907
5363
|
return true;
|
|
3908
5364
|
},
|
|
3909
5365
|
shutdown() {
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
5366
|
+
shutdownImpl();
|
|
5367
|
+
}
|
|
5368
|
+
};
|
|
5369
|
+
function shutdownImpl() {
|
|
5370
|
+
if (shutdownDone) return;
|
|
5371
|
+
shutdownDone = true;
|
|
5372
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
5373
|
+
if (persist) {
|
|
5374
|
+
try {
|
|
5375
|
+
process.off("exit", onProcessExit);
|
|
5376
|
+
} catch {
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5380
|
+
for (const rt of sessions.values()) {
|
|
5381
|
+
rt.emitter.removeAllListeners();
|
|
5382
|
+
if (rt.desc.status === "running" || rt.desc.status === "starting") {
|
|
5383
|
+
rt.desc.status = "killed";
|
|
5384
|
+
rt.desc.endedAt = nowIso;
|
|
5385
|
+
if (rt.agentSession) {
|
|
5386
|
+
void rt.agentSession.close().catch(() => void 0);
|
|
5387
|
+
}
|
|
5388
|
+
if (rt.pty) {
|
|
5389
|
+
try {
|
|
5390
|
+
rt.pty.kill("SIGTERM");
|
|
5391
|
+
} catch {
|
|
3916
5392
|
}
|
|
3917
|
-
rt.child?.kill("SIGTERM");
|
|
3918
5393
|
}
|
|
5394
|
+
rt.child?.kill("SIGTERM");
|
|
3919
5395
|
}
|
|
3920
|
-
sessions.clear();
|
|
3921
5396
|
}
|
|
3922
|
-
|
|
5397
|
+
if (persist) {
|
|
5398
|
+
try {
|
|
5399
|
+
const snapshot = {
|
|
5400
|
+
savedAt: nowIso,
|
|
5401
|
+
sessions: Array.from(sessions.values()).map((s) => s.desc)
|
|
5402
|
+
};
|
|
5403
|
+
mkdirSync(dirname(persistPath), { recursive: true });
|
|
5404
|
+
writeFileSync(persistPath, JSON.stringify(snapshot, null, 2) + "\n");
|
|
5405
|
+
} catch {
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
sessions.clear();
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
function loadHistorySnapshot(persistPath, sessions) {
|
|
5412
|
+
let raw;
|
|
5413
|
+
try {
|
|
5414
|
+
raw = readFileSync(persistPath, "utf8");
|
|
5415
|
+
} catch {
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
let parsed;
|
|
5419
|
+
try {
|
|
5420
|
+
parsed = JSON.parse(raw);
|
|
5421
|
+
} catch {
|
|
5422
|
+
console.warn(
|
|
5423
|
+
`[sessions] history file ${persistPath} is malformed \u2014 ignoring`
|
|
5424
|
+
);
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
if (!Array.isArray(parsed.sessions)) return;
|
|
5428
|
+
const sorted = parsed.sessions.filter((s) => !!s && typeof s.id === "string").sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, HISTORY_CAP);
|
|
5429
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5430
|
+
for (const desc of sorted) {
|
|
5431
|
+
if (sessions.has(desc.id)) continue;
|
|
5432
|
+
const wasAlive = desc.status === "running" || desc.status === "starting";
|
|
5433
|
+
const reclassified = wasAlive ? {
|
|
5434
|
+
...desc,
|
|
5435
|
+
status: "killed",
|
|
5436
|
+
endedAt: desc.endedAt ?? now
|
|
5437
|
+
} : desc;
|
|
5438
|
+
const rt = {
|
|
5439
|
+
desc: reclassified,
|
|
5440
|
+
recentLines: [],
|
|
5441
|
+
recentBytes: [],
|
|
5442
|
+
recentBytesSize: 0,
|
|
5443
|
+
emitter: new EventEmitter(),
|
|
5444
|
+
busy: false,
|
|
5445
|
+
textBuf: "",
|
|
5446
|
+
thoughtBuf: ""
|
|
5447
|
+
};
|
|
5448
|
+
rt.emitter.setMaxListeners(50);
|
|
5449
|
+
sessions.set(desc.id, rt);
|
|
5450
|
+
}
|
|
3923
5451
|
}
|
|
3924
5452
|
function quoteArg(arg) {
|
|
3925
5453
|
if (arg === "") return '""';
|
|
@@ -4115,9 +5643,24 @@ async function openClient(entry) {
|
|
|
4115
5643
|
const transport = new StdioClientTransport({
|
|
4116
5644
|
command: snap.command,
|
|
4117
5645
|
args: snap.args ?? [],
|
|
4118
|
-
env: { ...process.env, ...expandedEnv }
|
|
5646
|
+
env: { ...process.env, ...expandedEnv },
|
|
5647
|
+
// Pipe stderr so we can include it in the error message instead of
|
|
5648
|
+
// surfacing the opaque "Connection closed" MCP error code.
|
|
5649
|
+
stderr: "pipe"
|
|
4119
5650
|
});
|
|
4120
|
-
|
|
5651
|
+
const stderrBuf = [];
|
|
5652
|
+
transport.stderr?.on("data", (chunk) => {
|
|
5653
|
+
stderrBuf.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
5654
|
+
if (stderrBuf.length > 40) stderrBuf.shift();
|
|
5655
|
+
});
|
|
5656
|
+
try {
|
|
5657
|
+
await client.connect(transport);
|
|
5658
|
+
} catch (err) {
|
|
5659
|
+
const stderrText = stderrBuf.join("").trim();
|
|
5660
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5661
|
+
throw new Error(stderrText ? `${msg}
|
|
5662
|
+
stderr: ${stderrText.slice(-600)}` : msg);
|
|
5663
|
+
}
|
|
4121
5664
|
return client;
|
|
4122
5665
|
}
|
|
4123
5666
|
if (snap.type === "http") {
|
|
@@ -4175,7 +5718,7 @@ function quickTunnelProvider() {
|
|
|
4175
5718
|
{ stdio: ["ignore", "pipe", "pipe"], shell: false }
|
|
4176
5719
|
);
|
|
4177
5720
|
child = proc;
|
|
4178
|
-
const url = await new Promise((
|
|
5721
|
+
const url = await new Promise((resolve8, reject) => {
|
|
4179
5722
|
let settled = false;
|
|
4180
5723
|
const timer = setTimeout(() => {
|
|
4181
5724
|
if (settled) return;
|
|
@@ -4200,7 +5743,7 @@ function quickTunnelProvider() {
|
|
|
4200
5743
|
if (match) {
|
|
4201
5744
|
settled = true;
|
|
4202
5745
|
clearTimeout(timer);
|
|
4203
|
-
|
|
5746
|
+
resolve8(match[0]);
|
|
4204
5747
|
}
|
|
4205
5748
|
};
|
|
4206
5749
|
proc.stderr?.on("data", onData);
|
|
@@ -4249,15 +5792,15 @@ function quickTunnelProvider() {
|
|
|
4249
5792
|
}
|
|
4250
5793
|
}, 3e3);
|
|
4251
5794
|
timer.unref();
|
|
4252
|
-
await new Promise((
|
|
5795
|
+
await new Promise((resolve8) => {
|
|
4253
5796
|
if (proc.exitCode !== null) {
|
|
4254
5797
|
clearTimeout(timer);
|
|
4255
|
-
|
|
5798
|
+
resolve8();
|
|
4256
5799
|
return;
|
|
4257
5800
|
}
|
|
4258
5801
|
proc.once("exit", () => {
|
|
4259
5802
|
clearTimeout(timer);
|
|
4260
|
-
|
|
5803
|
+
resolve8();
|
|
4261
5804
|
});
|
|
4262
5805
|
});
|
|
4263
5806
|
}
|
|
@@ -4336,7 +5879,7 @@ var RemoteController = class {
|
|
|
4336
5879
|
let bearerTokenHash = "";
|
|
4337
5880
|
if (exposesGateway) {
|
|
4338
5881
|
bearerToken = randomBytes(32).toString("base64url");
|
|
4339
|
-
bearerTokenHash =
|
|
5882
|
+
bearerTokenHash = sha2562(bearerToken);
|
|
4340
5883
|
}
|
|
4341
5884
|
const state = {
|
|
4342
5885
|
provider: providerId,
|
|
@@ -4401,7 +5944,7 @@ function pickProvider(id) {
|
|
|
4401
5944
|
const _exhaustive = id;
|
|
4402
5945
|
throw new Error(`unknown remote provider: ${String(_exhaustive)}`);
|
|
4403
5946
|
}
|
|
4404
|
-
function
|
|
5947
|
+
function sha2562(input2) {
|
|
4405
5948
|
return createHash("sha256").update(input2).digest("hex");
|
|
4406
5949
|
}
|
|
4407
5950
|
function errToMessage(err) {
|
|
@@ -4494,7 +6037,7 @@ var WorkspacePathError = class extends Error {
|
|
|
4494
6037
|
};
|
|
4495
6038
|
function createWorkspaceFs(opts) {
|
|
4496
6039
|
const root = resolve(opts.workspace);
|
|
4497
|
-
function
|
|
6040
|
+
function resolvePath42(path) {
|
|
4498
6041
|
if (typeof path !== "string" || path.length === 0) {
|
|
4499
6042
|
throw new WorkspacePathError("path must be a non-empty string");
|
|
4500
6043
|
}
|
|
@@ -4514,18 +6057,18 @@ function createWorkspaceFs(opts) {
|
|
|
4514
6057
|
}
|
|
4515
6058
|
return {
|
|
4516
6059
|
async readFile(path) {
|
|
4517
|
-
const abs =
|
|
6060
|
+
const abs = resolvePath42(path);
|
|
4518
6061
|
const buf = await readFile(abs);
|
|
4519
6062
|
return buf.toString("utf8");
|
|
4520
6063
|
},
|
|
4521
6064
|
async writeFile(path, content) {
|
|
4522
|
-
const abs =
|
|
6065
|
+
const abs = resolvePath42(path);
|
|
4523
6066
|
await mkdir(dirname(abs), { recursive: true });
|
|
4524
6067
|
await writeFile(abs, content);
|
|
4525
6068
|
},
|
|
4526
6069
|
async exists(path) {
|
|
4527
6070
|
try {
|
|
4528
|
-
const abs =
|
|
6071
|
+
const abs = resolvePath42(path);
|
|
4529
6072
|
return existsSync(abs);
|
|
4530
6073
|
} catch {
|
|
4531
6074
|
return false;
|
|
@@ -4557,7 +6100,31 @@ async function createGateway(opts) {
|
|
|
4557
6100
|
name: opts.name ?? "agentproto-runtime",
|
|
4558
6101
|
version: opts.version ?? "0.1.0-alpha"
|
|
4559
6102
|
});
|
|
4560
|
-
const sessions = createSessionsRegistry(
|
|
6103
|
+
const sessions = createSessionsRegistry({
|
|
6104
|
+
...opts.spawnPty ? { spawnPty: opts.spawnPty } : {},
|
|
6105
|
+
// Resume hook: when a prompt arrives for a dead agent-cli row
|
|
6106
|
+
// (typical after daemon restart), the registry calls back into
|
|
6107
|
+
// the adapter resolver to re-create the AgentSession with
|
|
6108
|
+
// `resumeSessionId = adapterSessionId`. ACP semantics: the
|
|
6109
|
+
// upstream provider reattaches to the prior conversation.
|
|
6110
|
+
// Unwired (no resolveAgentAdapter) → legacy "not an agent
|
|
6111
|
+
// session" error, user must spawn fresh.
|
|
6112
|
+
...opts.resolveAgentAdapter ? {
|
|
6113
|
+
resumeAgent: async ({ adapterSlug, cwd, resumeSessionId }) => {
|
|
6114
|
+
const adapter = await opts.resolveAgentAdapter(adapterSlug);
|
|
6115
|
+
if (!adapter) return null;
|
|
6116
|
+
try {
|
|
6117
|
+
return await adapter.startSession({ cwd, resumeSessionId });
|
|
6118
|
+
} catch (err) {
|
|
6119
|
+
console.warn(
|
|
6120
|
+
`[agentproto] resumeAgent('${adapterSlug}', ${resumeSessionId}) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
6121
|
+
);
|
|
6122
|
+
return null;
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
} : {}
|
|
6126
|
+
});
|
|
6127
|
+
const token = opts.token ?? randomUUID();
|
|
4561
6128
|
const mcpProxy = new McpProxyRegistry();
|
|
4562
6129
|
const mcpServerFactory = async () => {
|
|
4563
6130
|
const { server } = await createMcpServer({
|
|
@@ -4572,6 +6139,7 @@ async function createGateway(opts) {
|
|
|
4572
6139
|
registerSessionTools(server, {
|
|
4573
6140
|
registry: sessions,
|
|
4574
6141
|
mcpProxy,
|
|
6142
|
+
ptyEnabled: opts.spawnPty != null,
|
|
4575
6143
|
...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
|
|
4576
6144
|
...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {}
|
|
4577
6145
|
});
|
|
@@ -4613,6 +6181,10 @@ async function createGateway(opts) {
|
|
|
4613
6181
|
heartbeat,
|
|
4614
6182
|
sessions,
|
|
4615
6183
|
mcpProxy,
|
|
6184
|
+
token,
|
|
6185
|
+
ptyEnabled: opts.spawnPty != null,
|
|
6186
|
+
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
6187
|
+
...opts.strictOrigins ? { strictOrigins: true } : {},
|
|
4616
6188
|
...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
|
|
4617
6189
|
...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
|
|
4618
6190
|
meta: { workspace, registered }
|
|
@@ -4631,7 +6203,8 @@ async function createGateway(opts) {
|
|
|
4631
6203
|
pid: process.pid,
|
|
4632
6204
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4633
6205
|
name: opts.name ?? "agentproto-runtime",
|
|
4634
|
-
registered
|
|
6206
|
+
registered,
|
|
6207
|
+
token
|
|
4635
6208
|
});
|
|
4636
6209
|
return {
|
|
4637
6210
|
url: http.url,
|
|
@@ -4639,6 +6212,7 @@ async function createGateway(opts) {
|
|
|
4639
6212
|
workspaceFs,
|
|
4640
6213
|
registered,
|
|
4641
6214
|
sessions,
|
|
6215
|
+
token,
|
|
4642
6216
|
async stop() {
|
|
4643
6217
|
heartbeat.stop();
|
|
4644
6218
|
sessions.shutdown();
|
|
@@ -4687,13 +6261,20 @@ async function runServe(args) {
|
|
|
4687
6261
|
label: { type: "string", short: "l" },
|
|
4688
6262
|
workspace: { type: "string", short: "w" },
|
|
4689
6263
|
port: { type: "string", short: "p" },
|
|
4690
|
-
bind: { type: "string", short: "b" }
|
|
6264
|
+
bind: { type: "string", short: "b" },
|
|
6265
|
+
"allow-origin": { type: "string", multiple: true },
|
|
6266
|
+
interactive: { type: "boolean", short: "i" }
|
|
4691
6267
|
}
|
|
4692
6268
|
});
|
|
4693
|
-
const
|
|
6269
|
+
const cfg = await loadConfig();
|
|
6270
|
+
const cfgDaemon = cfg.daemon ?? {};
|
|
6271
|
+
const cfgTunnel = cfg.tunnel ?? {};
|
|
6272
|
+
const workspace = resolve(
|
|
6273
|
+
values.workspace ?? cfgDaemon.workspace ?? process.cwd()
|
|
6274
|
+
);
|
|
4694
6275
|
try {
|
|
4695
|
-
const
|
|
4696
|
-
if (!
|
|
6276
|
+
const stat3 = await promises.stat(workspace);
|
|
6277
|
+
if (!stat3.isDirectory()) {
|
|
4697
6278
|
process.stderr.write(
|
|
4698
6279
|
`agentproto serve: --workspace "${workspace}" is not a directory.
|
|
4699
6280
|
`
|
|
@@ -4708,20 +6289,21 @@ async function runServe(args) {
|
|
|
4708
6289
|
);
|
|
4709
6290
|
return 2;
|
|
4710
6291
|
}
|
|
4711
|
-
const port = values.port ? Number.parseInt(values.port, 10) : 18790;
|
|
6292
|
+
const port = values.port ? Number.parseInt(values.port, 10) : typeof cfgDaemon.port === "number" ? cfgDaemon.port : 18790;
|
|
4712
6293
|
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
4713
6294
|
process.stderr.write(`agentproto serve: invalid --port "${values.port}".
|
|
4714
6295
|
`);
|
|
4715
6296
|
return 2;
|
|
4716
6297
|
}
|
|
4717
|
-
const label = values.label ?? `${userInfo().username}@${hostname()}`;
|
|
6298
|
+
const label = values.label ?? cfgDaemon.label ?? `${userInfo().username}@${hostname()}`;
|
|
6299
|
+
const connectFlag = values.connect ?? (cfgTunnel.autoconnect && cfgTunnel.host ? cfgTunnel.host : void 0);
|
|
4718
6300
|
let token = values.token ?? process.env.AGENTPROTO_TOKEN;
|
|
4719
|
-
if (!token &&
|
|
4720
|
-
const cred = await readHost(
|
|
6301
|
+
if (!token && connectFlag) {
|
|
6302
|
+
const cred = await readHost(connectFlag);
|
|
4721
6303
|
if (cred) {
|
|
4722
6304
|
if (isExpired(cred)) {
|
|
4723
6305
|
process.stderr.write(
|
|
4724
|
-
`agentproto serve: \u26A0 credentials for ${
|
|
6306
|
+
`agentproto serve: \u26A0 credentials for ${connectFlag} are expired (${formatExpiry(cred)}). Re-run \`agentproto auth login --host ${connectFlag}\`.
|
|
4725
6307
|
`
|
|
4726
6308
|
);
|
|
4727
6309
|
}
|
|
@@ -4732,21 +6314,31 @@ async function runServe(args) {
|
|
|
4732
6314
|
);
|
|
4733
6315
|
}
|
|
4734
6316
|
}
|
|
6317
|
+
const allowOriginRaw = values["allow-origin"];
|
|
6318
|
+
const cliOrigins = Array.isArray(allowOriginRaw) ? allowOriginRaw : [];
|
|
6319
|
+
const cfgOrigins = Array.isArray(cfgDaemon.allowedOrigins) ? cfgDaemon.allowedOrigins : [];
|
|
6320
|
+
const merged = [.../* @__PURE__ */ new Set([...cfgOrigins, ...cliOrigins])];
|
|
6321
|
+
const allowedOrigins = merged.length > 0 ? merged : void 0;
|
|
4735
6322
|
const opts = {
|
|
4736
6323
|
workspace,
|
|
4737
6324
|
port,
|
|
4738
|
-
bind: values.bind ?? "127.0.0.1",
|
|
4739
|
-
...
|
|
6325
|
+
bind: values.bind ?? cfgDaemon.bind ?? "127.0.0.1",
|
|
6326
|
+
...connectFlag ? { connect: connectFlag } : {},
|
|
4740
6327
|
...token ? { token } : {},
|
|
4741
|
-
label
|
|
6328
|
+
label,
|
|
6329
|
+
...allowedOrigins ? { allowedOrigins } : {},
|
|
6330
|
+
...cfgDaemon.strictOrigins === true ? { strictOrigins: true } : {}
|
|
4742
6331
|
};
|
|
4743
6332
|
const resolveAgentAdapter = async (slug) => {
|
|
4744
6333
|
try {
|
|
4745
6334
|
const adapter = await resolveAdapter(slug);
|
|
4746
6335
|
const runtime = createAgentCliRuntime(adapter.handle);
|
|
4747
6336
|
return {
|
|
4748
|
-
async startSession({ cwd }) {
|
|
4749
|
-
return runtime.start({
|
|
6337
|
+
async startSession({ cwd, resumeSessionId }) {
|
|
6338
|
+
return runtime.start({
|
|
6339
|
+
cwd,
|
|
6340
|
+
...resumeSessionId ? { resumeSessionId } : {}
|
|
6341
|
+
});
|
|
4750
6342
|
},
|
|
4751
6343
|
commandPreview: `${adapter.handle.bin} ${(adapter.handle.bin_args ?? []).join(" ")}`.trim()
|
|
4752
6344
|
};
|
|
@@ -4757,6 +6349,7 @@ async function runServe(args) {
|
|
|
4757
6349
|
return null;
|
|
4758
6350
|
}
|
|
4759
6351
|
};
|
|
6352
|
+
const spawnPty = await loadNodePtyFactory();
|
|
4760
6353
|
let gateway;
|
|
4761
6354
|
try {
|
|
4762
6355
|
gateway = await createGateway({
|
|
@@ -4771,7 +6364,10 @@ async function runServe(args) {
|
|
|
4771
6364
|
// Discovery for UIs / operators — `GET /adapters` + `list_adapters`
|
|
4772
6365
|
// MCP tool. Walks node_modules @agentproto/adapter-* on each call;
|
|
4773
6366
|
// cheap enough that we don't bother caching here.
|
|
4774
|
-
listAgentAdapters: listInstalledAdapters
|
|
6367
|
+
listAgentAdapters: listInstalledAdapters,
|
|
6368
|
+
...spawnPty ? { spawnPty } : {},
|
|
6369
|
+
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
6370
|
+
...opts.strictOrigins ? { strictOrigins: true } : {}
|
|
4775
6371
|
});
|
|
4776
6372
|
} catch (err) {
|
|
4777
6373
|
process.stderr.write(
|
|
@@ -4780,48 +6376,105 @@ async function runServe(args) {
|
|
|
4780
6376
|
);
|
|
4781
6377
|
return 1;
|
|
4782
6378
|
}
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
6379
|
+
printBootBanner({
|
|
6380
|
+
url: gateway.url,
|
|
6381
|
+
workspace: gateway.workspace,
|
|
6382
|
+
ptyEnabled: spawnPty != null,
|
|
6383
|
+
allowedOrigins: opts.allowedOrigins,
|
|
6384
|
+
strictOrigins: opts.strictOrigins === true,
|
|
6385
|
+
connect: opts.connect
|
|
6386
|
+
});
|
|
6387
|
+
try {
|
|
6388
|
+
const wsConfig = await loadWorkspacesConfig();
|
|
6389
|
+
const paths = wsConfig.workspaces.map((w) => w.path);
|
|
6390
|
+
const cleaned = await sweepStaleRuntimeMetas(paths, opts.workspace);
|
|
6391
|
+
if (cleaned.length > 0) {
|
|
6392
|
+
process.stderr.write(
|
|
6393
|
+
`${color.dim}cleaned ${cleaned.length} stale runtime.json file(s) (dead PID)${color.reset}
|
|
4789
6394
|
`
|
|
4790
|
-
|
|
6395
|
+
);
|
|
6396
|
+
}
|
|
6397
|
+
} catch {
|
|
6398
|
+
}
|
|
4791
6399
|
const aborter = new AbortController();
|
|
4792
6400
|
let shuttingDown = false;
|
|
4793
6401
|
const shutdown = async (signal) => {
|
|
4794
6402
|
if (shuttingDown) return;
|
|
4795
6403
|
shuttingDown = true;
|
|
4796
|
-
process.stderr.write(
|
|
4797
|
-
|
|
4798
|
-
|
|
6404
|
+
process.stderr.write(
|
|
6405
|
+
`
|
|
6406
|
+
${color.dim}\u2500\u2500 shutting down (${signal}) \u2500\u2500${color.reset}
|
|
6407
|
+
`
|
|
6408
|
+
);
|
|
4799
6409
|
aborter.abort();
|
|
4800
6410
|
await gateway.stop().catch(() => void 0);
|
|
6411
|
+
await unlinkRuntimeMeta(opts.workspace).catch(() => void 0);
|
|
4801
6412
|
process.exit(0);
|
|
4802
6413
|
};
|
|
4803
6414
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
4804
6415
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6416
|
+
process.once("SIGHUP", () => void shutdown("SIGHUP"));
|
|
6417
|
+
if (values.interactive) {
|
|
6418
|
+
process.stderr.write(
|
|
6419
|
+
`${color.dim}entering interactive monitor \u2014 q in the TUI to quit (daemon + TUI both).${color.reset}
|
|
6420
|
+
`
|
|
6421
|
+
);
|
|
6422
|
+
const childArgv = [process.argv[1] ?? "", "sessions", "--watch"];
|
|
6423
|
+
const child = childProc.spawn(process.execPath, childArgv, {
|
|
6424
|
+
stdio: "inherit",
|
|
6425
|
+
env: {
|
|
6426
|
+
...process.env,
|
|
6427
|
+
AGENTPROTO_DAEMON_URL: gateway.url,
|
|
6428
|
+
// `gateway.token` is the per-boot daemon bearer (different
|
|
6429
|
+
// from `token`, which was the tunnel JWT). Without this, the
|
|
6430
|
+
// child's restart/kill calls would 401 against its own
|
|
6431
|
+
// parent — exactly the bug that closed --interactive after
|
|
6432
|
+
// any failed action.
|
|
6433
|
+
AGENTPROTO_DAEMON_TOKEN: gateway.token
|
|
6434
|
+
}
|
|
6435
|
+
});
|
|
6436
|
+
await new Promise((resolve4) => {
|
|
6437
|
+
child.once("exit", () => resolve4());
|
|
6438
|
+
aborter.signal.addEventListener("abort", () => {
|
|
6439
|
+
try {
|
|
6440
|
+
child.kill("SIGTERM");
|
|
6441
|
+
} catch {
|
|
6442
|
+
}
|
|
6443
|
+
});
|
|
6444
|
+
});
|
|
6445
|
+
if (!shuttingDown) {
|
|
6446
|
+
aborter.abort();
|
|
6447
|
+
await gateway.stop().catch(() => void 0);
|
|
6448
|
+
await unlinkRuntimeMeta(opts.workspace).catch(() => void 0);
|
|
6449
|
+
}
|
|
6450
|
+
return 0;
|
|
6451
|
+
}
|
|
4805
6452
|
if (!opts.connect) {
|
|
4806
6453
|
process.stderr.write(
|
|
4807
|
-
|
|
6454
|
+
`${color.dim}Press Ctrl-C to stop.${color.reset}
|
|
4808
6455
|
`
|
|
4809
6456
|
);
|
|
4810
|
-
await new Promise((
|
|
4811
|
-
aborter.signal.addEventListener("abort", () =>
|
|
6457
|
+
await new Promise((resolve4) => {
|
|
6458
|
+
aborter.signal.addEventListener("abort", () => resolve4());
|
|
4812
6459
|
});
|
|
4813
6460
|
return 0;
|
|
4814
6461
|
}
|
|
4815
6462
|
process.stderr.write(
|
|
4816
|
-
|
|
6463
|
+
`${color.dim}tunnel \xB7 connecting to ${opts.connect} as '${opts.label}'\u2026${color.reset}
|
|
4817
6464
|
`
|
|
4818
6465
|
);
|
|
4819
6466
|
let backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
4820
6467
|
const backoffMax = opts.reconnectMaxMs ?? 3e4;
|
|
6468
|
+
const reconnectState = { immediate: false };
|
|
4821
6469
|
while (!aborter.signal.aborted) {
|
|
4822
6470
|
try {
|
|
4823
|
-
await runOneTunnel(opts, gateway, aborter.signal);
|
|
6471
|
+
await runOneTunnel(opts, gateway, spawnPty, aborter.signal, reconnectState);
|
|
4824
6472
|
backoffMs = opts.reconnectMinMs ?? 1e3;
|
|
6473
|
+
if (reconnectState.immediate) {
|
|
6474
|
+
reconnectState.immediate = false;
|
|
6475
|
+
continue;
|
|
6476
|
+
}
|
|
6477
|
+
await sleep(2e3, aborter.signal);
|
|
4825
6478
|
} catch (err) {
|
|
4826
6479
|
if (aborter.signal.aborted) break;
|
|
4827
6480
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -4836,17 +6489,17 @@ agentproto serve: ${signal} \u2014 shutting down.
|
|
|
4836
6489
|
}
|
|
4837
6490
|
return 0;
|
|
4838
6491
|
}
|
|
4839
|
-
async function runOneTunnel(opts, gateway, signal) {
|
|
6492
|
+
async function runOneTunnel(opts, gateway, spawnPty, signal, reconnectState) {
|
|
4840
6493
|
if (!opts.connect) throw new Error("runOneTunnel: --connect not set");
|
|
4841
6494
|
const headers = {
|
|
4842
6495
|
"user-agent": "agentproto/0.1.0-alpha"
|
|
4843
6496
|
};
|
|
4844
6497
|
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
4845
6498
|
const ws = new WebSocket(opts.connect, { headers });
|
|
4846
|
-
await new Promise((
|
|
6499
|
+
await new Promise((resolve4, reject) => {
|
|
4847
6500
|
const onOpen = () => {
|
|
4848
6501
|
ws.off("error", onError);
|
|
4849
|
-
|
|
6502
|
+
resolve4();
|
|
4850
6503
|
};
|
|
4851
6504
|
const onError = (err) => {
|
|
4852
6505
|
ws.off("open", onOpen);
|
|
@@ -4866,16 +6519,86 @@ async function runOneTunnel(opts, gateway, signal) {
|
|
|
4866
6519
|
process.stderr.write(`agentproto serve: tunnel up.
|
|
4867
6520
|
`);
|
|
4868
6521
|
const sink = wrapWebSocket(ws);
|
|
6522
|
+
const keepaliveInterval = setInterval(() => {
|
|
6523
|
+
try {
|
|
6524
|
+
sink.send({ t: "ping", nonce: randomUUID() });
|
|
6525
|
+
} catch {
|
|
6526
|
+
}
|
|
6527
|
+
}, 3e4);
|
|
4869
6528
|
const server = createTunnelServer({
|
|
4870
6529
|
sink,
|
|
4871
6530
|
label: opts.label,
|
|
4872
|
-
pty:
|
|
6531
|
+
pty: spawnPty !== null,
|
|
6532
|
+
...spawnPty ? { spawnPty } : {},
|
|
4873
6533
|
// Generic HTTP-relay upstream for tunnel `http_request` frames.
|
|
4874
6534
|
// Cloud-side callers (e.g. the API's local-daemon filesystem
|
|
4875
6535
|
// provider) can now route MCP JSON-RPC + any other HTTP through
|
|
4876
6536
|
// the daemon without needing a public URL. We point at the local
|
|
4877
6537
|
// gateway since that's where `/mcp`, `/sessions`, `/events` live.
|
|
4878
6538
|
httpUpstream: gateway.url,
|
|
6539
|
+
// WS forwarding upstream — daemon dials the local gateway's WS
|
|
6540
|
+
// endpoints (/sessions/:id/pty, etc) and pipes frames to the host.
|
|
6541
|
+
// Used by the cloud tunnel pod so browsers on mobile can attach to
|
|
6542
|
+
// interactive PTY sessions even though the daemon is only reachable
|
|
6543
|
+
// through the host (not directly).
|
|
6544
|
+
dialUpstreamWs: async ({ url, protocols, headers: headers2 }) => {
|
|
6545
|
+
const upstreamHeaders = {
|
|
6546
|
+
...headers2
|
|
6547
|
+
};
|
|
6548
|
+
if (!upstreamHeaders["origin"] && !upstreamHeaders["Origin"]) {
|
|
6549
|
+
try {
|
|
6550
|
+
const u = new URL(url);
|
|
6551
|
+
const httpScheme = u.protocol === "wss:" ? "https:" : "http:";
|
|
6552
|
+
upstreamHeaders["Origin"] = `${httpScheme}//${u.host}`;
|
|
6553
|
+
} catch {
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
return await new Promise((resolve4, reject) => {
|
|
6557
|
+
const sock = new WebSocket(url, protocols ? [...protocols] : void 0, {
|
|
6558
|
+
headers: upstreamHeaders
|
|
6559
|
+
});
|
|
6560
|
+
const onceOpen = () => {
|
|
6561
|
+
sock.off("error", onceError);
|
|
6562
|
+
sock.off("unexpected-response", onceUnexpected);
|
|
6563
|
+
resolve4({
|
|
6564
|
+
protocol: sock.protocol ?? "",
|
|
6565
|
+
send: (data, sendOpts) => {
|
|
6566
|
+
sock.send(data, { binary: sendOpts.binary });
|
|
6567
|
+
},
|
|
6568
|
+
close: (code, reason) => {
|
|
6569
|
+
try {
|
|
6570
|
+
sock.close(code, reason);
|
|
6571
|
+
} catch {
|
|
6572
|
+
}
|
|
6573
|
+
},
|
|
6574
|
+
onMessage: (handler) => {
|
|
6575
|
+
sock.on("message", (raw, isBinary) => {
|
|
6576
|
+
handler(raw, isBinary);
|
|
6577
|
+
});
|
|
6578
|
+
},
|
|
6579
|
+
onClose: (handler) => {
|
|
6580
|
+
sock.on("close", (code, reason) => {
|
|
6581
|
+
handler(code, reason.toString("utf8"));
|
|
6582
|
+
});
|
|
6583
|
+
},
|
|
6584
|
+
onError: (handler) => {
|
|
6585
|
+
sock.on("error", (err) => handler(err));
|
|
6586
|
+
}
|
|
6587
|
+
});
|
|
6588
|
+
};
|
|
6589
|
+
const onceError = (err) => {
|
|
6590
|
+
sock.off("open", onceOpen);
|
|
6591
|
+
reject(err);
|
|
6592
|
+
};
|
|
6593
|
+
const onceUnexpected = (_req, res) => {
|
|
6594
|
+
sock.off("open", onceOpen);
|
|
6595
|
+
reject(new Error(`Unexpected server response: ${res.statusCode ?? 0}`));
|
|
6596
|
+
};
|
|
6597
|
+
sock.once("open", onceOpen);
|
|
6598
|
+
sock.once("error", onceError);
|
|
6599
|
+
sock.once("unexpected-response", onceUnexpected);
|
|
6600
|
+
});
|
|
6601
|
+
},
|
|
4879
6602
|
// v0 authorize hook: trust the bearer-authenticated host completely.
|
|
4880
6603
|
// Token possession proves the host was provisioned for this daemon.
|
|
4881
6604
|
// Per-spawn policy filtering will land alongside the policy.toml.
|
|
@@ -4895,33 +6618,93 @@ async function runOneTunnel(opts, gateway, signal) {
|
|
|
4895
6618
|
kind: "agent-cli",
|
|
4896
6619
|
label: `tunnel: ${request.command.split("/").pop() ?? request.command}`
|
|
4897
6620
|
});
|
|
6621
|
+
},
|
|
6622
|
+
// Graceful drain hook — flip the outer loop's "reconnect immediately"
|
|
6623
|
+
// flag and close the WS so the supervisor reconnects without backoff.
|
|
6624
|
+
// Host will follow up with close(1012) ~2s later as a hard backstop.
|
|
6625
|
+
onReconnectSoon: ({ reasonMs }) => {
|
|
6626
|
+
process.stderr.write(
|
|
6627
|
+
`agentproto serve: host signaled drain (reasonMs=${reasonMs ?? "?"}) \u2014 reconnecting immediately
|
|
6628
|
+
`
|
|
6629
|
+
);
|
|
6630
|
+
reconnectState.immediate = true;
|
|
6631
|
+
try {
|
|
6632
|
+
ws.close(1e3, "host_drain");
|
|
6633
|
+
} catch {
|
|
6634
|
+
}
|
|
4898
6635
|
}
|
|
4899
6636
|
});
|
|
4900
|
-
await new Promise((
|
|
6637
|
+
await new Promise((resolve4) => {
|
|
4901
6638
|
const offClose = sink.onClose(() => {
|
|
6639
|
+
clearInterval(keepaliveInterval);
|
|
4902
6640
|
offClose();
|
|
4903
|
-
|
|
6641
|
+
resolve4();
|
|
4904
6642
|
});
|
|
4905
6643
|
if (signal.aborted) {
|
|
4906
|
-
|
|
6644
|
+
clearInterval(keepaliveInterval);
|
|
6645
|
+
server.close().finally(() => resolve4());
|
|
4907
6646
|
}
|
|
4908
6647
|
signal.addEventListener("abort", () => {
|
|
4909
|
-
|
|
6648
|
+
clearInterval(keepaliveInterval);
|
|
6649
|
+
server.close().finally(() => resolve4());
|
|
4910
6650
|
});
|
|
4911
6651
|
});
|
|
4912
6652
|
process.stderr.write(`agentproto serve: tunnel closed.
|
|
4913
6653
|
`);
|
|
4914
6654
|
}
|
|
4915
6655
|
function sleep(ms, signal) {
|
|
4916
|
-
return new Promise((
|
|
4917
|
-
if (signal.aborted) return
|
|
4918
|
-
const timer = setTimeout(
|
|
6656
|
+
return new Promise((resolve4) => {
|
|
6657
|
+
if (signal.aborted) return resolve4();
|
|
6658
|
+
const timer = setTimeout(resolve4, ms);
|
|
4919
6659
|
signal.addEventListener("abort", () => {
|
|
4920
6660
|
clearTimeout(timer);
|
|
4921
|
-
|
|
6661
|
+
resolve4();
|
|
4922
6662
|
});
|
|
4923
6663
|
});
|
|
4924
6664
|
}
|
|
6665
|
+
function printBootBanner(opts) {
|
|
6666
|
+
const c = color;
|
|
6667
|
+
const home = process.env.HOME ?? "";
|
|
6668
|
+
const workspace = home && opts.workspace.startsWith(home) ? "~" + opts.workspace.slice(home.length) : opts.workspace;
|
|
6669
|
+
const ptyState = opts.ptyEnabled ? `${c.green}enabled${c.reset} ${c.dim}(node-pty)${c.reset}` : `${c.amber}disabled${c.reset} ${c.dim}(install node-pty to enable)${c.reset}`;
|
|
6670
|
+
let origins;
|
|
6671
|
+
if (opts.strictOrigins) {
|
|
6672
|
+
origins = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? `${c.amber}STRICT${c.reset} ${opts.allowedOrigins.join(" \xB7 ")} ${c.dim}(localhost defaults disabled)${c.reset}` : `${c.red}STRICT \xB7 empty allowlist${c.reset} ${c.dim}(every Origin will 401 \u2014 only Bearer-token works)${c.reset}`;
|
|
6673
|
+
} else {
|
|
6674
|
+
origins = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? opts.allowedOrigins.join(" \xB7 ") + ` ${c.dim}+ localhost (default)${c.reset}` : `${c.dim}localhost:* only (default)${c.reset}`;
|
|
6675
|
+
}
|
|
6676
|
+
const mode = opts.connect ? `${c.cyan}tunnel${c.reset} ${c.dim}\u2192 ${opts.connect}${c.reset}` : `${c.dim}local-only${c.reset}`;
|
|
6677
|
+
const line = `${c.dim}\u2500${c.reset}`;
|
|
6678
|
+
process.stderr.write(
|
|
6679
|
+
`
|
|
6680
|
+
${line} ${c.bold}agentproto${c.reset} ${c.dim}\xB7${c.reset} gateway up ${c.dim}\xB7${c.reset} ${c.cyan}${opts.url}${c.reset} ${line}
|
|
6681
|
+
${c.dim}workspace${c.reset} ${workspace}
|
|
6682
|
+
${c.dim}pty${c.reset} ${ptyState}
|
|
6683
|
+
${c.dim}origins${c.reset} ${origins}
|
|
6684
|
+
${c.dim}endpoints${c.reset} /mcp \xB7 /sessions \xB7 /events \xB7 /sessions/:id/pty ${c.dim}(WS)${c.reset}
|
|
6685
|
+
${c.dim}mode${c.reset} ${mode}
|
|
6686
|
+
|
|
6687
|
+
`
|
|
6688
|
+
);
|
|
6689
|
+
}
|
|
6690
|
+
var _isTty = !!process.stderr.isTTY;
|
|
6691
|
+
var color = _isTty ? {
|
|
6692
|
+
reset: "\x1B[0m",
|
|
6693
|
+
bold: "\x1B[1m",
|
|
6694
|
+
dim: "\x1B[2m",
|
|
6695
|
+
green: "\x1B[32m",
|
|
6696
|
+
amber: "\x1B[33m",
|
|
6697
|
+
cyan: "\x1B[36m",
|
|
6698
|
+
red: "\x1B[31m"
|
|
6699
|
+
} : {
|
|
6700
|
+
reset: "",
|
|
6701
|
+
bold: "",
|
|
6702
|
+
dim: "",
|
|
6703
|
+
green: "",
|
|
6704
|
+
amber: "",
|
|
6705
|
+
cyan: "",
|
|
6706
|
+
red: ""
|
|
6707
|
+
};
|
|
4925
6708
|
|
|
4926
6709
|
export { listInstalledAdapters, resolveAdapter, runInstall, runRun, runServe };
|
|
4927
6710
|
//# sourceMappingURL=index.mjs.map
|