@openscout/scout 0.2.57 → 0.2.58
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 +2 -2
- package/dist/client/assets/{arc.es-Ccqmb9Pq.js → arc.es-Bkj9Bkvx.js} +1 -1
- package/dist/client/assets/index-BDyu05Hi.js +140 -0
- package/dist/client/assets/index-pbcZpgCR.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/main.mjs +286 -130
- package/dist/pair-supervisor.mjs +151 -42
- package/dist/scout-control-plane-web.mjs +1085 -889
- package/package.json +2 -2
- package/dist/client/assets/index-CN8PCsrs.css +0 -1
- package/dist/client/assets/index-X8QwrbtL.js +0 -140
|
@@ -5703,6 +5703,20 @@ var init_support_paths = () => {};
|
|
|
5703
5703
|
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
5704
5704
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
5705
5705
|
import { dirname as dirname3, join as join5 } from "path";
|
|
5706
|
+
function buildHarnessResumeCommand(entry, sessionId, cwd) {
|
|
5707
|
+
if (!entry.resume)
|
|
5708
|
+
return null;
|
|
5709
|
+
const parts = [entry.resume.command, entry.resume.sessionFlag, sessionId];
|
|
5710
|
+
if (cwd && entry.resume.cwdFlag) {
|
|
5711
|
+
parts.push(entry.resume.cwdFlag, cwd);
|
|
5712
|
+
}
|
|
5713
|
+
return parts.join(" ");
|
|
5714
|
+
}
|
|
5715
|
+
function findHarnessEntry(harness) {
|
|
5716
|
+
if (!harness)
|
|
5717
|
+
return null;
|
|
5718
|
+
return BUILT_IN_HARNESS_CATALOG.find((e) => e.harness === harness || e.name === harness) ?? null;
|
|
5719
|
+
}
|
|
5706
5720
|
async function writeHarnessCatalogOverrides(overrides, overridePath = resolveOpenScoutSupportPaths().harnessCatalogPath) {
|
|
5707
5721
|
await mkdir2(dirname3(overridePath), { recursive: true });
|
|
5708
5722
|
const payload = {
|
|
@@ -5761,6 +5775,11 @@ var init_harness_catalog = __esm(() => {
|
|
|
5761
5775
|
loginCommand: "claude login",
|
|
5762
5776
|
notReadyMessage: "Claude is installed but not authenticated yet."
|
|
5763
5777
|
},
|
|
5778
|
+
resume: {
|
|
5779
|
+
command: "claude",
|
|
5780
|
+
sessionFlag: "--resume",
|
|
5781
|
+
cwdFlag: "--cwd"
|
|
5782
|
+
},
|
|
5764
5783
|
capabilities: ["chat", "invoke", "deliver", "summarize", "review"]
|
|
5765
5784
|
},
|
|
5766
5785
|
{
|
|
@@ -5792,6 +5811,11 @@ var init_harness_catalog = __esm(() => {
|
|
|
5792
5811
|
loginCommand: "codex login",
|
|
5793
5812
|
notReadyMessage: "Codex is installed but not authenticated yet."
|
|
5794
5813
|
},
|
|
5814
|
+
resume: {
|
|
5815
|
+
command: "codex",
|
|
5816
|
+
sessionFlag: "--thread",
|
|
5817
|
+
cwdFlag: "--cwd"
|
|
5818
|
+
},
|
|
5795
5819
|
capabilities: ["chat", "invoke", "deliver", "review", "execute"]
|
|
5796
5820
|
}
|
|
5797
5821
|
];
|
|
@@ -8056,7 +8080,8 @@ function buildManagedAgentShellExports(options) {
|
|
|
8056
8080
|
// packages/runtime/src/claude-stream-json.ts
|
|
8057
8081
|
import { randomUUID } from "crypto";
|
|
8058
8082
|
import { spawn as spawn3 } from "child_process";
|
|
8059
|
-
import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile5,
|
|
8083
|
+
import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
8084
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
8060
8085
|
import { join as join13 } from "path";
|
|
8061
8086
|
function resolveClaudeStreamJsonOutput(result, fallbackParts) {
|
|
8062
8087
|
const trimmedResult = result?.trim();
|
|
@@ -8092,6 +8117,53 @@ function parseJsonObject(value) {
|
|
|
8092
8117
|
return {};
|
|
8093
8118
|
}
|
|
8094
8119
|
}
|
|
8120
|
+
function readSessionCatalogSync(runtimeDirectory) {
|
|
8121
|
+
const catalogPath = join13(runtimeDirectory, SESSION_CATALOG_FILENAME);
|
|
8122
|
+
try {
|
|
8123
|
+
if (!existsSync9(catalogPath)) {
|
|
8124
|
+
const legacyPath = join13(runtimeDirectory, "claude-session-id.txt");
|
|
8125
|
+
if (existsSync9(legacyPath)) {
|
|
8126
|
+
const legacyId = readFileSync5(legacyPath, "utf8").trim();
|
|
8127
|
+
if (legacyId) {
|
|
8128
|
+
return {
|
|
8129
|
+
activeSessionId: legacyId,
|
|
8130
|
+
sessions: [{ id: legacyId, startedAt: Date.now(), cwd: "" }]
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
}
|
|
8134
|
+
return { activeSessionId: null, sessions: [] };
|
|
8135
|
+
}
|
|
8136
|
+
const raw2 = readFileSync5(catalogPath, "utf8").trim();
|
|
8137
|
+
if (!raw2)
|
|
8138
|
+
return { activeSessionId: null, sessions: [] };
|
|
8139
|
+
const parsed = JSON.parse(raw2);
|
|
8140
|
+
return {
|
|
8141
|
+
activeSessionId: parsed.activeSessionId ?? null,
|
|
8142
|
+
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : []
|
|
8143
|
+
};
|
|
8144
|
+
} catch {
|
|
8145
|
+
return { activeSessionId: null, sessions: [] };
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
async function readSessionCatalog(runtimeDirectory) {
|
|
8149
|
+
return readSessionCatalogSync(runtimeDirectory);
|
|
8150
|
+
}
|
|
8151
|
+
async function writeSessionCatalog(runtimeDirectory, catalog) {
|
|
8152
|
+
const catalogPath = join13(runtimeDirectory, SESSION_CATALOG_FILENAME);
|
|
8153
|
+
await writeFile4(catalogPath, JSON.stringify(catalog, null, 2) + `
|
|
8154
|
+
`);
|
|
8155
|
+
}
|
|
8156
|
+
function catalogRecordSession(catalog, sessionId, cwd) {
|
|
8157
|
+
const now = Date.now();
|
|
8158
|
+
const sessions = catalog.sessions.map((s) => s.id === catalog.activeSessionId && !s.endedAt ? { ...s, endedAt: now } : s);
|
|
8159
|
+
if (!sessions.some((s) => s.id === sessionId)) {
|
|
8160
|
+
sessions.push({ id: sessionId, startedAt: now, cwd });
|
|
8161
|
+
}
|
|
8162
|
+
while (sessions.length > SESSION_CATALOG_MAX_ENTRIES) {
|
|
8163
|
+
sessions.shift();
|
|
8164
|
+
}
|
|
8165
|
+
return { activeSessionId: sessionId, sessions };
|
|
8166
|
+
}
|
|
8095
8167
|
function appendActionOutput(blockState, output) {
|
|
8096
8168
|
if (!blockState || blockState.block.type !== "action" || !output) {
|
|
8097
8169
|
return;
|
|
@@ -8495,7 +8567,7 @@ function buildClaudeStreamJsonSessionSnapshot(raw2, options, targetClaudeSession
|
|
|
8495
8567
|
|
|
8496
8568
|
class ClaudeStreamJsonSession {
|
|
8497
8569
|
options;
|
|
8498
|
-
|
|
8570
|
+
catalogDirectory;
|
|
8499
8571
|
stdoutLogPath;
|
|
8500
8572
|
stderrLogPath;
|
|
8501
8573
|
process = null;
|
|
@@ -8506,7 +8578,7 @@ class ClaudeStreamJsonSession {
|
|
|
8506
8578
|
lastConfigSignature;
|
|
8507
8579
|
constructor(options) {
|
|
8508
8580
|
this.options = options;
|
|
8509
|
-
this.
|
|
8581
|
+
this.catalogDirectory = options.runtimeDirectory;
|
|
8510
8582
|
this.stdoutLogPath = join13(options.logsDirectory, "stdout.log");
|
|
8511
8583
|
this.stderrLogPath = join13(options.logsDirectory, "stderr.log");
|
|
8512
8584
|
this.lastConfigSignature = this.configSignature(options);
|
|
@@ -8612,7 +8684,9 @@ class ClaudeStreamJsonSession {
|
|
|
8612
8684
|
}
|
|
8613
8685
|
if (options.resetSession) {
|
|
8614
8686
|
this.claudeSessionId = null;
|
|
8615
|
-
await
|
|
8687
|
+
const catalog = await readSessionCatalog(this.catalogDirectory);
|
|
8688
|
+
catalog.activeSessionId = null;
|
|
8689
|
+
await writeSessionCatalog(this.catalogDirectory, catalog);
|
|
8616
8690
|
}
|
|
8617
8691
|
}
|
|
8618
8692
|
configSignature(options) {
|
|
@@ -8641,7 +8715,8 @@ class ClaudeStreamJsonSession {
|
|
|
8641
8715
|
await mkdir4(this.options.runtimeDirectory, { recursive: true });
|
|
8642
8716
|
await mkdir4(this.options.logsDirectory, { recursive: true });
|
|
8643
8717
|
await writeFile4(join13(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
|
|
8644
|
-
|
|
8718
|
+
const catalog = await readSessionCatalog(this.catalogDirectory);
|
|
8719
|
+
this.claudeSessionId = catalog.activeSessionId;
|
|
8645
8720
|
const args = [
|
|
8646
8721
|
"--verbose",
|
|
8647
8722
|
"--print",
|
|
@@ -8720,8 +8795,11 @@ class ClaudeStreamJsonSession {
|
|
|
8720
8795
|
const nextSessionId = event.session_id ?? event.sessionId ?? null;
|
|
8721
8796
|
if (nextSessionId && nextSessionId !== this.claudeSessionId) {
|
|
8722
8797
|
this.claudeSessionId = nextSessionId;
|
|
8723
|
-
|
|
8724
|
-
|
|
8798
|
+
(async () => {
|
|
8799
|
+
const catalog = await readSessionCatalog(this.catalogDirectory);
|
|
8800
|
+
const updated = catalogRecordSession(catalog, nextSessionId, this.options.cwd);
|
|
8801
|
+
await writeSessionCatalog(this.catalogDirectory, updated);
|
|
8802
|
+
})();
|
|
8725
8803
|
}
|
|
8726
8804
|
return;
|
|
8727
8805
|
}
|
|
@@ -8794,29 +8872,30 @@ async function answerClaudeStreamJsonQuestion(options, input) {
|
|
|
8794
8872
|
}
|
|
8795
8873
|
async function getClaudeStreamJsonAgentSnapshot(options) {
|
|
8796
8874
|
const stdoutLogPath = join13(options.logsDirectory, "stdout.log");
|
|
8797
|
-
const
|
|
8798
|
-
const [rawLog, persistedSessionId] = await Promise.all([
|
|
8875
|
+
const [rawLog, catalog] = await Promise.all([
|
|
8799
8876
|
readOptionalFile2(stdoutLogPath),
|
|
8800
|
-
|
|
8877
|
+
readSessionCatalog(options.runtimeDirectory)
|
|
8801
8878
|
]);
|
|
8802
8879
|
if (!rawLog) {
|
|
8803
8880
|
return null;
|
|
8804
8881
|
}
|
|
8805
|
-
return buildClaudeStreamJsonSessionSnapshot(rawLog, options,
|
|
8882
|
+
return buildClaudeStreamJsonSessionSnapshot(rawLog, options, catalog.activeSessionId);
|
|
8806
8883
|
}
|
|
8807
8884
|
async function shutdownClaudeStreamJsonAgent(options, shutdownOptions = {}) {
|
|
8808
8885
|
const key = sessionKey(options);
|
|
8809
8886
|
const session = sessions.get(key);
|
|
8810
8887
|
if (!session) {
|
|
8811
8888
|
if (shutdownOptions.resetSession) {
|
|
8812
|
-
await
|
|
8889
|
+
const catalog = await readSessionCatalog(options.runtimeDirectory);
|
|
8890
|
+
catalog.activeSessionId = null;
|
|
8891
|
+
await writeSessionCatalog(options.runtimeDirectory, catalog);
|
|
8813
8892
|
}
|
|
8814
8893
|
return;
|
|
8815
8894
|
}
|
|
8816
8895
|
sessions.delete(key);
|
|
8817
8896
|
await session.shutdown(shutdownOptions);
|
|
8818
8897
|
}
|
|
8819
|
-
var sessions;
|
|
8898
|
+
var SESSION_CATALOG_FILENAME = "session-catalog.json", SESSION_CATALOG_MAX_ENTRIES = 64, sessions;
|
|
8820
8899
|
var init_claude_stream_json = __esm(() => {
|
|
8821
8900
|
sessions = new Map;
|
|
8822
8901
|
});
|
|
@@ -10435,7 +10514,7 @@ function buildInvocationCollaborationContextPrompt(invocation) {
|
|
|
10435
10514
|
|
|
10436
10515
|
// packages/runtime/src/broker-service.ts
|
|
10437
10516
|
import { spawnSync } from "child_process";
|
|
10438
|
-
import { existsSync as
|
|
10517
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
10439
10518
|
import { homedir as homedir11 } from "os";
|
|
10440
10519
|
import { basename as basename4, dirname as dirname10, join as join15, resolve as resolve5 } from "path";
|
|
10441
10520
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
@@ -10474,7 +10553,7 @@ function runtimePackageDir() {
|
|
|
10474
10553
|
return resolve5(moduleDir, "..");
|
|
10475
10554
|
}
|
|
10476
10555
|
function isInstalledRuntimePackageDir(candidate) {
|
|
10477
|
-
return
|
|
10556
|
+
return existsSync10(join15(candidate, "package.json")) && existsSync10(join15(candidate, "bin", "openscout-runtime.mjs"));
|
|
10478
10557
|
}
|
|
10479
10558
|
function findGlobalRuntimeDir() {
|
|
10480
10559
|
const candidates = [
|
|
@@ -10505,7 +10584,7 @@ function findWorkspaceRuntimeDir(startDir) {
|
|
|
10505
10584
|
let current = resolve5(startDir);
|
|
10506
10585
|
while (true) {
|
|
10507
10586
|
const candidate = join15(current, "packages", "runtime");
|
|
10508
|
-
if (
|
|
10587
|
+
if (existsSync10(join15(candidate, "package.json")) && existsSync10(join15(candidate, "src"))) {
|
|
10509
10588
|
return candidate;
|
|
10510
10589
|
}
|
|
10511
10590
|
const parent = dirname10(current);
|
|
@@ -10519,18 +10598,18 @@ function resolveBunExecutable2() {
|
|
|
10519
10598
|
if (explicit && explicit.trim().length > 0) {
|
|
10520
10599
|
return explicit;
|
|
10521
10600
|
}
|
|
10522
|
-
if (basename4(process.execPath).startsWith("bun") &&
|
|
10601
|
+
if (basename4(process.execPath).startsWith("bun") && existsSync10(process.execPath)) {
|
|
10523
10602
|
return process.execPath;
|
|
10524
10603
|
}
|
|
10525
10604
|
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
10526
10605
|
for (const entry of pathEntries) {
|
|
10527
10606
|
const candidate = join15(entry, "bun");
|
|
10528
|
-
if (
|
|
10607
|
+
if (existsSync10(candidate)) {
|
|
10529
10608
|
return candidate;
|
|
10530
10609
|
}
|
|
10531
10610
|
}
|
|
10532
10611
|
const homeBun = join15(homedir11(), ".bun", "bin", "bun");
|
|
10533
|
-
if (
|
|
10612
|
+
if (existsSync10(homeBun)) {
|
|
10534
10613
|
return homeBun;
|
|
10535
10614
|
}
|
|
10536
10615
|
return "bun";
|
|
@@ -10720,10 +10799,10 @@ function launchctlPath() {
|
|
|
10720
10799
|
return "/bin/launchctl";
|
|
10721
10800
|
}
|
|
10722
10801
|
function readLogLines(path) {
|
|
10723
|
-
if (!
|
|
10802
|
+
if (!existsSync10(path)) {
|
|
10724
10803
|
return [];
|
|
10725
10804
|
}
|
|
10726
|
-
return
|
|
10805
|
+
return readFileSync6(path, "utf8").split(`
|
|
10727
10806
|
`).map((line) => line.trim()).filter(Boolean);
|
|
10728
10807
|
}
|
|
10729
10808
|
function isPackageScriptBanner(line) {
|
|
@@ -10820,7 +10899,7 @@ async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
|
|
|
10820
10899
|
ensureServiceDirectories(config);
|
|
10821
10900
|
const launchctl = inspectLaunchctl(config);
|
|
10822
10901
|
const health = await fetchHealthSnapshot(config);
|
|
10823
|
-
const installed =
|
|
10902
|
+
const installed = existsSync10(config.launchAgentPath);
|
|
10824
10903
|
const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
|
|
10825
10904
|
return {
|
|
10826
10905
|
label: config.label,
|
|
@@ -10882,7 +10961,7 @@ async function restartBrokerService(config = resolveBrokerServiceConfig()) {
|
|
|
10882
10961
|
}
|
|
10883
10962
|
async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
|
|
10884
10963
|
await stopBrokerService(config);
|
|
10885
|
-
if (
|
|
10964
|
+
if (existsSync10(config.launchAgentPath)) {
|
|
10886
10965
|
rmSync2(config.launchAgentPath, { force: true });
|
|
10887
10966
|
}
|
|
10888
10967
|
return brokerServiceStatus(config);
|
|
@@ -10975,6 +11054,7 @@ __export(exports_local_agents, {
|
|
|
10975
11054
|
resolveLocalAgentIdentity: () => resolveLocalAgentIdentity,
|
|
10976
11055
|
resolveLocalAgentByName: () => resolveLocalAgentByName,
|
|
10977
11056
|
renderLocalAgentSystemPromptTemplate: () => renderLocalAgentSystemPromptTemplate,
|
|
11057
|
+
normalizeLocalAgentSystemPrompt: () => normalizeLocalAgentSystemPrompt,
|
|
10978
11058
|
loadRegisteredLocalAgentBindings: () => loadRegisteredLocalAgentBindings,
|
|
10979
11059
|
listLocalAgents: () => listLocalAgents,
|
|
10980
11060
|
isLocalAgentSessionAlive: () => isLocalAgentSessionAlive,
|
|
@@ -10998,7 +11078,7 @@ __export(exports_local_agents, {
|
|
|
10998
11078
|
});
|
|
10999
11079
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
11000
11080
|
import { execFileSync as execFileSync2, execSync } from "child_process";
|
|
11001
|
-
import { existsSync as
|
|
11081
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
|
|
11002
11082
|
import { mkdir as mkdir6, rm as rm5, stat as stat4, writeFile as writeFile6 } from "fs/promises";
|
|
11003
11083
|
import { basename as basename5, dirname as dirname11, join as join16, resolve as resolve6 } from "path";
|
|
11004
11084
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
@@ -11011,10 +11091,10 @@ function resolveProjectsRoot(projectPath) {
|
|
|
11011
11091
|
}
|
|
11012
11092
|
try {
|
|
11013
11093
|
const supportPaths = resolveOpenScoutSupportPaths();
|
|
11014
|
-
if (!
|
|
11094
|
+
if (!existsSync11(supportPaths.settingsPath)) {
|
|
11015
11095
|
return dirname11(projectPath);
|
|
11016
11096
|
}
|
|
11017
|
-
const raw2 = JSON.parse(
|
|
11097
|
+
const raw2 = JSON.parse(readFileSync7(supportPaths.settingsPath, "utf8"));
|
|
11018
11098
|
const workspaceRoot = raw2.discovery?.workspaceRoots?.find((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
11019
11099
|
return workspaceRoot ? resolve6(workspaceRoot) : dirname11(projectPath);
|
|
11020
11100
|
} catch {
|
|
@@ -11030,8 +11110,14 @@ function nowSeconds2() {
|
|
|
11030
11110
|
function normalizeBrokerTimestamp(value) {
|
|
11031
11111
|
return value > 10000000000 ? Math.floor(value / 1000) : value;
|
|
11032
11112
|
}
|
|
11113
|
+
function scoutCliPath() {
|
|
11114
|
+
return join16(OPENSCOUT_REPO_ROOT, "packages", "cli", "bin", "scout.mjs");
|
|
11115
|
+
}
|
|
11116
|
+
function legacyNodeBrokerRelayCommand() {
|
|
11117
|
+
return `node ${JSON.stringify(scoutCliPath())}`;
|
|
11118
|
+
}
|
|
11033
11119
|
function brokerRelayCommand() {
|
|
11034
|
-
return `
|
|
11120
|
+
return `bun ${JSON.stringify(scoutCliPath())}`;
|
|
11035
11121
|
}
|
|
11036
11122
|
function sleep3(ms) {
|
|
11037
11123
|
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
@@ -11053,7 +11139,7 @@ function resolveScoutSkillPath() {
|
|
|
11053
11139
|
join16(process.env.HOME ?? "", ".agents", "skills", "relay-agent-comms", "SKILL.md")
|
|
11054
11140
|
];
|
|
11055
11141
|
for (const path of candidatePaths) {
|
|
11056
|
-
if (
|
|
11142
|
+
if (existsSync11(path)) {
|
|
11057
11143
|
return path;
|
|
11058
11144
|
}
|
|
11059
11145
|
}
|
|
@@ -11249,7 +11335,7 @@ function buildLegacyBrokerBackedRelayPrompt(agentId, projectName, projectPath, r
|
|
|
11249
11335
|
function legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath) {
|
|
11250
11336
|
const relayHub = resolveRelayHub();
|
|
11251
11337
|
const brokerUrl = resolveBrokerUrl();
|
|
11252
|
-
const relayCommandBases = ["openscout relay", brokerRelayCommand()];
|
|
11338
|
+
const relayCommandBases = ["openscout relay", brokerRelayCommand(), legacyNodeBrokerRelayCommand()];
|
|
11253
11339
|
const projectPathCandidates = projectPath.endsWith("/") ? [projectPath, projectPath.slice(0, -1)] : [projectPath, `${projectPath}/`];
|
|
11254
11340
|
const candidates = new Set;
|
|
11255
11341
|
for (const pathCandidate of projectPathCandidates) {
|
|
@@ -11260,12 +11346,28 @@ function legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPat
|
|
|
11260
11346
|
}
|
|
11261
11347
|
return Array.from(candidates);
|
|
11262
11348
|
}
|
|
11349
|
+
function generatedLocalAgentSystemPromptCandidates(agentId, projectName, projectPath) {
|
|
11350
|
+
const baseContext = buildLocalAgentTemplateContext(agentId, projectName, projectPath);
|
|
11351
|
+
const relayCommands = [brokerRelayCommand(), legacyNodeBrokerRelayCommand()];
|
|
11352
|
+
const transportModes = [undefined, "codex_app_server", "claude_stream_json"];
|
|
11353
|
+
const candidates = new Set;
|
|
11354
|
+
for (const relayCommand of relayCommands) {
|
|
11355
|
+
const context = {
|
|
11356
|
+
...baseContext,
|
|
11357
|
+
relayCommand
|
|
11358
|
+
};
|
|
11359
|
+
for (const transport of transportModes) {
|
|
11360
|
+
candidates.add(renderLocalAgentSystemPromptTemplate(buildLocalAgentSystemPromptTemplate(), context, transport ? { transport } : {}));
|
|
11361
|
+
}
|
|
11362
|
+
}
|
|
11363
|
+
return Array.from(candidates);
|
|
11364
|
+
}
|
|
11263
11365
|
function normalizeLocalAgentSystemPrompt(agentId, projectName, projectPath, systemPrompt) {
|
|
11264
11366
|
const trimmed = systemPrompt?.trim();
|
|
11265
11367
|
if (!trimmed) {
|
|
11266
11368
|
return;
|
|
11267
11369
|
}
|
|
11268
|
-
if (legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed)) {
|
|
11370
|
+
if (legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed) || generatedLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed)) {
|
|
11269
11371
|
return;
|
|
11270
11372
|
}
|
|
11271
11373
|
return trimmed;
|
|
@@ -11617,11 +11719,13 @@ async function getLocalAgentEndpointSessionSnapshot(endpoint) {
|
|
|
11617
11719
|
async function ensureLocalSessionEndpointOnline(endpoint) {
|
|
11618
11720
|
if (endpoint.transport === "codex_app_server") {
|
|
11619
11721
|
await ensureCodexAppServerAgentOnline(buildCodexEndpointSessionOptions(endpoint));
|
|
11620
|
-
return;
|
|
11722
|
+
return {};
|
|
11621
11723
|
}
|
|
11622
11724
|
if (endpoint.transport === "claude_stream_json") {
|
|
11623
|
-
await ensureClaudeStreamJsonAgentOnline(buildClaudeEndpointSessionOptions(endpoint));
|
|
11725
|
+
const result = await ensureClaudeStreamJsonAgentOnline(buildClaudeEndpointSessionOptions(endpoint));
|
|
11726
|
+
return { externalSessionId: result.sessionId };
|
|
11624
11727
|
}
|
|
11728
|
+
return {};
|
|
11625
11729
|
}
|
|
11626
11730
|
async function shutdownLocalSessionEndpoint(endpoint) {
|
|
11627
11731
|
if (endpoint.transport === "codex_app_server") {
|
|
@@ -12059,7 +12163,7 @@ async function ensureLocalAgentOnline(agentName, record) {
|
|
|
12059
12163
|
await new Promise((resolve7) => setTimeout(resolve7, 100));
|
|
12060
12164
|
}
|
|
12061
12165
|
if (!isLocalAgentSessionAlive(normalizedRecord.tmuxSession)) {
|
|
12062
|
-
const stderrTail =
|
|
12166
|
+
const stderrTail = existsSync11(stderrLogFile) ? readFileSync7(stderrLogFile, "utf8").trim().split(/\r?\n/).slice(-10).join(`
|
|
12063
12167
|
`).trim() : "";
|
|
12064
12168
|
throw new Error(stderrTail ? `Relay agent ${agentName} failed to stay online:
|
|
12065
12169
|
${stderrTail}` : `Relay agent ${agentName} failed to stay online.`);
|
|
@@ -12427,6 +12531,29 @@ async function restartAllLocalAgents(options = {}) {
|
|
|
12427
12531
|
}));
|
|
12428
12532
|
return restarted.filter((agent) => Boolean(agent));
|
|
12429
12533
|
}
|
|
12534
|
+
function readPersistedClaudeSessionId(agentName) {
|
|
12535
|
+
try {
|
|
12536
|
+
const runtimeDir = relayAgentRuntimeDirectory(agentName);
|
|
12537
|
+
const catalogPath = join16(runtimeDir, "session-catalog.json");
|
|
12538
|
+
if (existsSync11(catalogPath)) {
|
|
12539
|
+
const raw2 = readFileSync7(catalogPath, "utf8").trim();
|
|
12540
|
+
if (raw2) {
|
|
12541
|
+
const catalog = JSON.parse(raw2);
|
|
12542
|
+
if (typeof catalog.activeSessionId === "string" && catalog.activeSessionId.trim()) {
|
|
12543
|
+
return catalog.activeSessionId.trim();
|
|
12544
|
+
}
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
const legacyPath = join16(runtimeDir, "claude-session-id.txt");
|
|
12548
|
+
if (existsSync11(legacyPath)) {
|
|
12549
|
+
const value = readFileSync7(legacyPath, "utf8").trim();
|
|
12550
|
+
return value || null;
|
|
12551
|
+
}
|
|
12552
|
+
return null;
|
|
12553
|
+
} catch {
|
|
12554
|
+
return null;
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12430
12557
|
function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
12431
12558
|
const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
|
|
12432
12559
|
const definitionId = normalizedRecord.definitionId ?? agentId;
|
|
@@ -12434,6 +12561,7 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
12434
12561
|
const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
|
|
12435
12562
|
const instance = buildRelayAgentInstance(definitionId, projectRoot);
|
|
12436
12563
|
const actorId = instance.id;
|
|
12564
|
+
const externalSessionId = normalizedRecord.transport === "claude_stream_json" ? readPersistedClaudeSessionId(definitionId) : null;
|
|
12437
12565
|
return {
|
|
12438
12566
|
actor: {
|
|
12439
12567
|
id: actorId,
|
|
@@ -12511,7 +12639,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
12511
12639
|
selector: instance.selector,
|
|
12512
12640
|
nodeQualifier: instance.nodeQualifier,
|
|
12513
12641
|
workspaceQualifier: instance.workspaceQualifier,
|
|
12514
|
-
branch: instance.branch
|
|
12642
|
+
branch: instance.branch,
|
|
12643
|
+
...externalSessionId ? { externalSessionId } : {}
|
|
12515
12644
|
}
|
|
12516
12645
|
}
|
|
12517
12646
|
};
|
|
@@ -12711,12 +12840,12 @@ var init_local_agents = __esm(async () => {
|
|
|
12711
12840
|
});
|
|
12712
12841
|
|
|
12713
12842
|
// packages/web/server/index.ts
|
|
12714
|
-
import { existsSync as
|
|
12843
|
+
import { existsSync as existsSync15 } from "fs";
|
|
12715
12844
|
import { dirname as dirname13, join as join21, resolve as resolve10 } from "path";
|
|
12716
12845
|
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
12717
12846
|
|
|
12718
12847
|
// packages/web/server/create-openscout-web-server.ts
|
|
12719
|
-
import { existsSync as
|
|
12848
|
+
import { existsSync as existsSync14, rmSync as rmSync3 } from "fs";
|
|
12720
12849
|
import { dirname as dirname12, resolve as resolve9 } from "path";
|
|
12721
12850
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
12722
12851
|
|
|
@@ -15487,19 +15616,15 @@ function resolveDbPath() {
|
|
|
15487
15616
|
return join12(controlHome, "control-plane.sqlite");
|
|
15488
15617
|
}
|
|
15489
15618
|
var _db = null;
|
|
15490
|
-
var
|
|
15491
|
-
|
|
15619
|
+
var DB_BUSY_TIMEOUT_MS = 250;
|
|
15620
|
+
function configureReadonlyDb(db) {
|
|
15621
|
+
db.exec(`PRAGMA busy_timeout = ${DB_BUSY_TIMEOUT_MS}`);
|
|
15622
|
+
db.exec("PRAGMA query_only = ON");
|
|
15623
|
+
}
|
|
15492
15624
|
function db() {
|
|
15493
|
-
const now = Date.now();
|
|
15494
|
-
if (_db && now - _dbOpenedAt > DB_REOPEN_MS) {
|
|
15495
|
-
_db.close();
|
|
15496
|
-
_db = null;
|
|
15497
|
-
}
|
|
15498
15625
|
if (!_db) {
|
|
15499
15626
|
_db = new Database(resolveDbPath(), { readonly: true });
|
|
15500
|
-
_db
|
|
15501
|
-
_db.exec("PRAGMA journal_mode = WAL");
|
|
15502
|
-
_dbOpenedAt = now;
|
|
15627
|
+
configureReadonlyDb(_db);
|
|
15503
15628
|
}
|
|
15504
15629
|
return _db;
|
|
15505
15630
|
}
|
|
@@ -15536,7 +15661,7 @@ function resolveHarnessSessionId(transport, endpointSessionId, metadata) {
|
|
|
15536
15661
|
return metadataString(metadata, "threadId") ?? endpointSessionId;
|
|
15537
15662
|
}
|
|
15538
15663
|
if (transport === "claude_stream_json") {
|
|
15539
|
-
return
|
|
15664
|
+
return metadataString(metadata, "externalSessionId") ?? endpointSessionId;
|
|
15540
15665
|
}
|
|
15541
15666
|
return null;
|
|
15542
15667
|
}
|
|
@@ -16714,125 +16839,67 @@ function queryFleet(opts) {
|
|
|
16714
16839
|
activity
|
|
16715
16840
|
};
|
|
16716
16841
|
}
|
|
16717
|
-
var
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
const currentBucketStart2 = Math.floor(nowMs / bucketMs2) * bucketMs2;
|
|
16733
|
-
const alignedStart2 = currentBucketStart2 - (numBuckets - 1) * bucketMs2;
|
|
16734
|
-
if (_cachedTier === tier.label) {
|
|
16735
|
-
let sealedHits = 0;
|
|
16736
|
-
let filledSealed = 0;
|
|
16737
|
-
for (let i = 0;i < numBuckets - 1; i++) {
|
|
16738
|
-
const key = String(alignedStart2 + i * bucketMs2);
|
|
16739
|
-
if (_sealedCounts.has(key)) {
|
|
16740
|
-
sealedHits++;
|
|
16741
|
-
if (_sealedCounts.get(key) > 0)
|
|
16742
|
-
filledSealed++;
|
|
16743
|
-
}
|
|
16744
|
-
}
|
|
16745
|
-
if (sealedHits === numBuckets - 1 && filledSealed >= MIN_FILLED_BUCKETS) {
|
|
16746
|
-
chosenTier = tier;
|
|
16747
|
-
break;
|
|
16748
|
-
}
|
|
16749
|
-
}
|
|
16750
|
-
const rows2 = db().prepare(`SELECT ts FROM activity_items WHERE ts >= ? ORDER BY ts ASC`).all(alignedStart2);
|
|
16751
|
-
const counts2 = new Array(numBuckets).fill(0);
|
|
16752
|
-
for (const row of rows2) {
|
|
16753
|
-
const ms = row.ts < 1000000000000 ? row.ts * 1000 : row.ts;
|
|
16754
|
-
if (ms < alignedStart2)
|
|
16842
|
+
var HEARTRATE_WINDOW_MS = 7 * 24 * 60 * 60000;
|
|
16843
|
+
var DEFAULT_HEARTRATE_BUCKETS = 56;
|
|
16844
|
+
var SQLITE_MILLISECONDS_THRESHOLD = 1000000000000;
|
|
16845
|
+
function normalizeActivityTimestampMs(ts) {
|
|
16846
|
+
return ts < SQLITE_MILLISECONDS_THRESHOLD ? ts * 1000 : ts;
|
|
16847
|
+
}
|
|
16848
|
+
function smoothHeartrateCounts(counts) {
|
|
16849
|
+
const energy = counts.map((count) => Math.sqrt(count));
|
|
16850
|
+
const weights = [0.56, 0.28, 0.11, 0.05];
|
|
16851
|
+
return energy.map((_, index) => {
|
|
16852
|
+
let total = 0;
|
|
16853
|
+
let weightTotal = 0;
|
|
16854
|
+
for (let offset = -3;offset <= 3; offset++) {
|
|
16855
|
+
const nextIndex = index + offset;
|
|
16856
|
+
if (nextIndex < 0 || nextIndex >= energy.length)
|
|
16755
16857
|
continue;
|
|
16756
|
-
const
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
}
|
|
16760
|
-
const filled = counts2.filter((c) => c > 0).length;
|
|
16761
|
-
if (filled >= MIN_FILLED_BUCKETS) {
|
|
16762
|
-
if (_cachedTier !== tier.label) {
|
|
16763
|
-
_sealedCounts.clear();
|
|
16764
|
-
_cachedTier = tier.label;
|
|
16765
|
-
}
|
|
16766
|
-
for (let i = 0;i < numBuckets - 1; i++) {
|
|
16767
|
-
_sealedCounts.set(String(alignedStart2 + i * bucketMs2), counts2[i]);
|
|
16768
|
-
}
|
|
16769
|
-
const peak2 = Math.max(1, ...counts2);
|
|
16770
|
-
return {
|
|
16771
|
-
windowLabel: tier.label,
|
|
16772
|
-
buckets: counts2.map((count, i) => ({
|
|
16773
|
-
ts: Math.round(alignedStart2 + i * bucketMs2),
|
|
16774
|
-
count,
|
|
16775
|
-
value: count / peak2
|
|
16776
|
-
}))
|
|
16777
|
-
};
|
|
16858
|
+
const weight = weights[Math.abs(offset)] ?? 0;
|
|
16859
|
+
total += energy[nextIndex] * weight;
|
|
16860
|
+
weightTotal += weight;
|
|
16778
16861
|
}
|
|
16862
|
+
return weightTotal > 0 ? total / weightTotal : 0;
|
|
16863
|
+
});
|
|
16864
|
+
}
|
|
16865
|
+
function formatHeartrateBucketLabel(bucketMs) {
|
|
16866
|
+
const minutes = Math.round(bucketMs / 60000);
|
|
16867
|
+
if (minutes % (24 * 60) === 0) {
|
|
16868
|
+
return `${minutes / (24 * 60)}d buckets`;
|
|
16779
16869
|
}
|
|
16780
|
-
|
|
16870
|
+
if (minutes % 60 === 0) {
|
|
16871
|
+
return `${minutes / 60}h buckets`;
|
|
16872
|
+
}
|
|
16873
|
+
return `${minutes}m buckets`;
|
|
16874
|
+
}
|
|
16875
|
+
function queryHeartrate(numBuckets = DEFAULT_HEARTRATE_BUCKETS, nowMs = Date.now()) {
|
|
16876
|
+
const bucketMs = HEARTRATE_WINDOW_MS / numBuckets;
|
|
16781
16877
|
const currentBucketStart = Math.floor(nowMs / bucketMs) * bucketMs;
|
|
16782
16878
|
const alignedStart = currentBucketStart - (numBuckets - 1) * bucketMs;
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
counts2[i] = _sealedCounts.get(key);
|
|
16790
|
-
} else {
|
|
16791
|
-
allSealed = false;
|
|
16792
|
-
break;
|
|
16793
|
-
}
|
|
16794
|
-
}
|
|
16795
|
-
if (allSealed) {
|
|
16796
|
-
const liveStart = alignedStart + (numBuckets - 1) * bucketMs;
|
|
16797
|
-
const row = db().prepare(`SELECT COUNT(*) AS c FROM activity_items WHERE ts >= ?`).get(liveStart);
|
|
16798
|
-
counts2[numBuckets - 1] = row.c;
|
|
16799
|
-
const peak2 = Math.max(1, ...counts2);
|
|
16800
|
-
return {
|
|
16801
|
-
windowLabel: chosenTier.label,
|
|
16802
|
-
buckets: counts2.map((count, i) => ({
|
|
16803
|
-
ts: Math.round(alignedStart + i * bucketMs),
|
|
16804
|
-
count,
|
|
16805
|
-
value: count / peak2
|
|
16806
|
-
}))
|
|
16807
|
-
};
|
|
16808
|
-
}
|
|
16809
|
-
}
|
|
16810
|
-
_sealedCounts.clear();
|
|
16811
|
-
_cachedTier = chosenTier.label;
|
|
16812
|
-
const rows = db().prepare(`SELECT ts FROM activity_items WHERE ts >= ? ORDER BY ts ASC`).all(alignedStart);
|
|
16879
|
+
const alignedStartSeconds = Math.floor(alignedStart / 1000);
|
|
16880
|
+
const rows = db().prepare(`SELECT ts
|
|
16881
|
+
FROM activity_items
|
|
16882
|
+
WHERE ts >= ?1
|
|
16883
|
+
OR (ts < ?2 AND ts >= ?3)
|
|
16884
|
+
ORDER BY ts ASC`).all(alignedStart, SQLITE_MILLISECONDS_THRESHOLD, alignedStartSeconds);
|
|
16813
16885
|
const counts = new Array(numBuckets).fill(0);
|
|
16814
16886
|
for (const row of rows) {
|
|
16815
|
-
const ms = row.ts
|
|
16816
|
-
if (ms < alignedStart)
|
|
16887
|
+
const ms = normalizeActivityTimestampMs(row.ts);
|
|
16888
|
+
if (ms < alignedStart || ms > nowMs)
|
|
16817
16889
|
continue;
|
|
16818
16890
|
const idx = Math.min(numBuckets - 1, Math.floor((ms - alignedStart) / bucketMs));
|
|
16819
16891
|
if (idx >= 0)
|
|
16820
16892
|
counts[idx]++;
|
|
16821
16893
|
}
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
}
|
|
16825
|
-
for (const key of _sealedCounts.keys()) {
|
|
16826
|
-
if (Number(key) < alignedStart)
|
|
16827
|
-
_sealedCounts.delete(key);
|
|
16828
|
-
}
|
|
16829
|
-
const peak = Math.max(1, ...counts);
|
|
16894
|
+
const smoothed = smoothHeartrateCounts(counts);
|
|
16895
|
+
const peak = Math.max(1, ...smoothed);
|
|
16830
16896
|
return {
|
|
16831
|
-
windowLabel:
|
|
16897
|
+
windowLabel: "trailing 7d",
|
|
16898
|
+
bucketLabel: formatHeartrateBucketLabel(bucketMs),
|
|
16832
16899
|
buckets: counts.map((count, i) => ({
|
|
16833
16900
|
ts: Math.round(alignedStart + i * bucketMs),
|
|
16834
16901
|
count,
|
|
16835
|
-
value:
|
|
16902
|
+
value: smoothed[i] / peak
|
|
16836
16903
|
}))
|
|
16837
16904
|
};
|
|
16838
16905
|
}
|
|
@@ -17761,7 +17828,7 @@ function whoEntryState(endpoints, registrationKind) {
|
|
|
17761
17828
|
|
|
17762
17829
|
// packages/web/server/core/observe/service.ts
|
|
17763
17830
|
init_src();
|
|
17764
|
-
import { existsSync as
|
|
17831
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
17765
17832
|
import { homedir as homedir12 } from "os";
|
|
17766
17833
|
import { join as join18, resolve as resolve8 } from "path";
|
|
17767
17834
|
|
|
@@ -19749,6 +19816,9 @@ function parseSelf(status) {
|
|
|
19749
19816
|
magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
|
|
19750
19817
|
};
|
|
19751
19818
|
}
|
|
19819
|
+
function isBackendRunning(status) {
|
|
19820
|
+
return (status.BackendState ?? "").trim().toLowerCase() === "running";
|
|
19821
|
+
}
|
|
19752
19822
|
async function readStatusJsonFromFile(filePath) {
|
|
19753
19823
|
const raw2 = await readFile7(filePath, "utf8");
|
|
19754
19824
|
return parseStatusJson(raw2);
|
|
@@ -19766,19 +19836,25 @@ async function readStatusJson() {
|
|
|
19766
19836
|
return null;
|
|
19767
19837
|
}
|
|
19768
19838
|
}
|
|
19769
|
-
async function
|
|
19770
|
-
const
|
|
19771
|
-
if (!
|
|
19772
|
-
return
|
|
19839
|
+
async function readTailscaleSelf() {
|
|
19840
|
+
const summary = await readTailscaleStatusSummary();
|
|
19841
|
+
if (!summary) {
|
|
19842
|
+
return null;
|
|
19773
19843
|
}
|
|
19774
|
-
return
|
|
19844
|
+
return summary.self;
|
|
19775
19845
|
}
|
|
19776
|
-
async function
|
|
19846
|
+
async function readTailscaleStatusSummary() {
|
|
19777
19847
|
const status = await readStatusJson();
|
|
19778
19848
|
if (!status) {
|
|
19779
19849
|
return null;
|
|
19780
19850
|
}
|
|
19781
|
-
return
|
|
19851
|
+
return {
|
|
19852
|
+
backendState: status.BackendState ?? null,
|
|
19853
|
+
running: isBackendRunning(status),
|
|
19854
|
+
health: status.Health ?? [],
|
|
19855
|
+
peers: parsePeers(status),
|
|
19856
|
+
self: parseSelf(status)
|
|
19857
|
+
};
|
|
19782
19858
|
}
|
|
19783
19859
|
|
|
19784
19860
|
// packages/runtime/src/index.ts
|
|
@@ -20075,11 +20151,37 @@ function encodeClaudeProjectsSlug2(absolutePath) {
|
|
|
20075
20151
|
}
|
|
20076
20152
|
function resolveClaudeHistoryPath(cwd, sessionId) {
|
|
20077
20153
|
const normalizedCwd = expandHome(cwd)?.trim();
|
|
20154
|
+
if (!normalizedCwd) {
|
|
20155
|
+
return null;
|
|
20156
|
+
}
|
|
20157
|
+
const projectDir = join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd));
|
|
20078
20158
|
const normalizedSessionId = sessionId?.trim().replace(/\.jsonl$/u, "") || "";
|
|
20079
|
-
if (
|
|
20159
|
+
if (normalizedSessionId) {
|
|
20160
|
+
const exactPath = join18(projectDir, `${normalizedSessionId}.jsonl`);
|
|
20161
|
+
if (existsSync12(exactPath)) {
|
|
20162
|
+
return exactPath;
|
|
20163
|
+
}
|
|
20164
|
+
}
|
|
20165
|
+
return findMostRecentJsonl(projectDir);
|
|
20166
|
+
}
|
|
20167
|
+
function findMostRecentJsonl(dir) {
|
|
20168
|
+
if (!existsSync12(dir))
|
|
20169
|
+
return null;
|
|
20170
|
+
try {
|
|
20171
|
+
let best = null;
|
|
20172
|
+
for (const entry of readdirSync2(dir)) {
|
|
20173
|
+
if (!entry.endsWith(".jsonl"))
|
|
20174
|
+
continue;
|
|
20175
|
+
const full = join18(dir, entry);
|
|
20176
|
+
const st = statSync4(full);
|
|
20177
|
+
if (!best || st.mtimeMs > best.mtime) {
|
|
20178
|
+
best = { path: full, mtime: st.mtimeMs };
|
|
20179
|
+
}
|
|
20180
|
+
}
|
|
20181
|
+
return best?.path ?? null;
|
|
20182
|
+
} catch {
|
|
20080
20183
|
return null;
|
|
20081
20184
|
}
|
|
20082
|
-
return join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd), `${normalizedSessionId}.jsonl`);
|
|
20083
20185
|
}
|
|
20084
20186
|
function historyAdapterAlias(value) {
|
|
20085
20187
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -20143,7 +20245,7 @@ function readHistorySnapshot(candidate) {
|
|
|
20143
20245
|
if (!supportsHistorySessionSnapshotForPath(candidate.path, candidate.adapterType)) {
|
|
20144
20246
|
return null;
|
|
20145
20247
|
}
|
|
20146
|
-
if (!
|
|
20248
|
+
if (!existsSync12(candidate.path)) {
|
|
20147
20249
|
return null;
|
|
20148
20250
|
}
|
|
20149
20251
|
const stat5 = statSync4(candidate.path);
|
|
@@ -20157,7 +20259,7 @@ function readHistorySnapshot(candidate) {
|
|
|
20157
20259
|
}
|
|
20158
20260
|
const replay = createHistorySessionSnapshot({
|
|
20159
20261
|
path: candidate.path,
|
|
20160
|
-
content:
|
|
20262
|
+
content: readFileSync8(candidate.path, "utf8"),
|
|
20161
20263
|
adapterType: candidate.adapterType,
|
|
20162
20264
|
baseTimestampMs: stat5.mtimeMs
|
|
20163
20265
|
});
|
|
@@ -20659,6 +20761,9 @@ async function loadAgentObservePayload(agentId) {
|
|
|
20659
20761
|
|
|
20660
20762
|
// packages/web/server/core/mesh/service.ts
|
|
20661
20763
|
await init_broker_service();
|
|
20764
|
+
import { execFile as execFile2 } from "child_process";
|
|
20765
|
+
import { promisify as promisify2 } from "util";
|
|
20766
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
20662
20767
|
function normalizeHost(host) {
|
|
20663
20768
|
return host.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
20664
20769
|
}
|
|
@@ -20689,9 +20794,13 @@ function stripTrailingDot(value) {
|
|
|
20689
20794
|
return trimmed.replace(/\.$/, "");
|
|
20690
20795
|
}
|
|
20691
20796
|
async function readTailscaleStatus() {
|
|
20692
|
-
const
|
|
20797
|
+
const summary = await readTailscaleStatusSummary();
|
|
20798
|
+
const peers = summary?.peers ?? [];
|
|
20693
20799
|
return {
|
|
20694
|
-
available:
|
|
20800
|
+
available: summary !== null,
|
|
20801
|
+
running: summary?.running ?? false,
|
|
20802
|
+
backendState: summary?.backendState ?? null,
|
|
20803
|
+
health: summary?.health ?? [],
|
|
20695
20804
|
peers,
|
|
20696
20805
|
onlineCount: peers.filter((p) => p.online).length
|
|
20697
20806
|
};
|
|
@@ -20712,6 +20821,16 @@ function computeIssues(health, localNode, nodes, tailscale2) {
|
|
|
20712
20821
|
});
|
|
20713
20822
|
return issues;
|
|
20714
20823
|
}
|
|
20824
|
+
if (tailscale2.available && !tailscale2.running) {
|
|
20825
|
+
issues.push({
|
|
20826
|
+
code: "tailscale_stopped",
|
|
20827
|
+
severity: "warning",
|
|
20828
|
+
title: "Tailscale is stopped",
|
|
20829
|
+
summary: "This machine can still show cached Tailnet peers, but the local Tailscale backend is not running, so the broker cannot reach them.",
|
|
20830
|
+
action: "Start Tailscale on this machine, then refresh mesh discovery.",
|
|
20831
|
+
actionCommand: null
|
|
20832
|
+
});
|
|
20833
|
+
}
|
|
20715
20834
|
if (localNode?.advertiseScope === "local") {
|
|
20716
20835
|
issues.push({
|
|
20717
20836
|
code: "local_only",
|
|
@@ -20879,6 +20998,24 @@ async function announceMeshVisibility() {
|
|
|
20879
20998
|
} catch {}
|
|
20880
20999
|
return loadMeshStatus();
|
|
20881
21000
|
}
|
|
21001
|
+
async function openTailscaleApp() {
|
|
21002
|
+
if (process.platform !== "darwin") {
|
|
21003
|
+
throw new Error("Opening Tailscale from Scout is only supported on macOS right now.");
|
|
21004
|
+
}
|
|
21005
|
+
try {
|
|
21006
|
+
await execFileAsync2("open", ["-a", "Tailscale"]);
|
|
21007
|
+
} catch (error) {
|
|
21008
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
21009
|
+
throw new Error(`Scout could not open Tailscale.app on this machine. ${detail}`);
|
|
21010
|
+
}
|
|
21011
|
+
}
|
|
21012
|
+
async function controlTailscale(action) {
|
|
21013
|
+
if (action === "open_app") {
|
|
21014
|
+
await openTailscaleApp();
|
|
21015
|
+
return loadMeshStatus();
|
|
21016
|
+
}
|
|
21017
|
+
throw new Error(`Unsupported Tailscale action: ${action}`);
|
|
21018
|
+
}
|
|
20882
21019
|
|
|
20883
21020
|
// packages/web/server/runtime-summary.ts
|
|
20884
21021
|
async function loadOpenScoutWebShellState() {
|
|
@@ -20915,780 +21052,839 @@ async function loadOpenScoutWebShellState() {
|
|
|
20915
21052
|
|
|
20916
21053
|
// packages/web/server/create-openscout-web-server.ts
|
|
20917
21054
|
init_setup();
|
|
20918
|
-
|
|
20919
|
-
|
|
20920
|
-
|
|
20921
|
-
|
|
20922
|
-
|
|
20923
|
-
|
|
21055
|
+
init_support_paths();
|
|
21056
|
+
init_claude_stream_json();
|
|
21057
|
+
init_harness_catalog();
|
|
21058
|
+
|
|
21059
|
+
// ../hudson/packages/hudson-relay/src/relay/session.ts
|
|
21060
|
+
import { createRequire } from "module";
|
|
21061
|
+
import { execSync as execSync2 } from "child_process";
|
|
21062
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
21063
|
+
import { join as join19, dirname as pathDirname } from "path";
|
|
21064
|
+
var require2 = createRequire(import.meta.url);
|
|
21065
|
+
var pty = require2("node-pty");
|
|
21066
|
+
var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
|
|
21067
|
+
var MAX_BUFFER_SIZE = 512 * 1024;
|
|
21068
|
+
var sessions3 = new Map;
|
|
21069
|
+
function generateId() {
|
|
21070
|
+
return Math.random().toString(36).slice(2, 10);
|
|
20924
21071
|
}
|
|
20925
|
-
function
|
|
20926
|
-
if (
|
|
20927
|
-
|
|
20928
|
-
return session.agentId;
|
|
20929
|
-
}
|
|
20930
|
-
const participants = session.participantIds.filter((participantId) => participantId.trim().length > 0);
|
|
20931
|
-
if (participants.length === 2) {
|
|
20932
|
-
const operatorCandidates = new Set([senderId.trim(), "operator"]);
|
|
20933
|
-
const nonOperatorParticipants = participants.filter((participantId) => !operatorCandidates.has(participantId));
|
|
20934
|
-
if (nonOperatorParticipants.length === 1) {
|
|
20935
|
-
return nonOperatorParticipants[0] ?? null;
|
|
20936
|
-
}
|
|
20937
|
-
const localSessionParticipant = nonOperatorParticipants.find((participantId) => participantId.startsWith("local-session-agent-")) ?? participants.find((participantId) => participantId.startsWith("local-session-agent-"));
|
|
20938
|
-
if (localSessionParticipant) {
|
|
20939
|
-
return localSessionParticipant;
|
|
20940
|
-
}
|
|
20941
|
-
return participants[0] ?? null;
|
|
20942
|
-
}
|
|
20943
|
-
}
|
|
20944
|
-
if (conversationId?.startsWith("dm.operator.")) {
|
|
20945
|
-
const legacyAgentId = conversationId.slice("dm.operator.".length);
|
|
20946
|
-
return legacyAgentId || null;
|
|
21072
|
+
function send(ws, data) {
|
|
21073
|
+
if (ws.readyState === 1) {
|
|
21074
|
+
ws.send(JSON.stringify(data));
|
|
20947
21075
|
}
|
|
20948
|
-
return null;
|
|
20949
21076
|
}
|
|
20950
|
-
function
|
|
20951
|
-
|
|
21077
|
+
function resolveCwd(raw2) {
|
|
21078
|
+
const home = process.env.HOME || "/tmp";
|
|
21079
|
+
const expanded = (raw2 || home).replace(/^~/, home);
|
|
21080
|
+
if (existsSync13(expanded))
|
|
21081
|
+
return expanded;
|
|
21082
|
+
try {
|
|
21083
|
+
mkdirSync6(expanded, { recursive: true });
|
|
21084
|
+
console.log(`[relay] Created missing cwd: ${expanded}`);
|
|
21085
|
+
return expanded;
|
|
21086
|
+
} catch {
|
|
21087
|
+
console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
|
|
21088
|
+
return home;
|
|
21089
|
+
}
|
|
20952
21090
|
}
|
|
20953
|
-
function
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
|
|
20958
|
-
|
|
21091
|
+
function findBin(name, envOverride) {
|
|
21092
|
+
if (envOverride && process.env[envOverride])
|
|
21093
|
+
return process.env[envOverride];
|
|
21094
|
+
try {
|
|
21095
|
+
return execSync2(`which ${name}`, { encoding: "utf8" }).trim() || null;
|
|
21096
|
+
} catch {
|
|
21097
|
+
return null;
|
|
21098
|
+
}
|
|
20959
21099
|
}
|
|
20960
|
-
function
|
|
20961
|
-
return
|
|
21100
|
+
function findClaudeBin() {
|
|
21101
|
+
return findBin("claude", "CLAUDE_BIN");
|
|
20962
21102
|
}
|
|
20963
|
-
function
|
|
20964
|
-
return
|
|
21103
|
+
function findPiBin() {
|
|
21104
|
+
return findBin("pi", "PI_BIN");
|
|
20965
21105
|
}
|
|
20966
|
-
function
|
|
20967
|
-
|
|
20968
|
-
|
|
20969
|
-
return
|
|
21106
|
+
function tmuxSessionExists2(name) {
|
|
21107
|
+
try {
|
|
21108
|
+
execSync2(`tmux has-session -t ${name} 2>/dev/null`, { encoding: "utf8" });
|
|
21109
|
+
return true;
|
|
21110
|
+
} catch {
|
|
21111
|
+
return false;
|
|
20970
21112
|
}
|
|
20971
|
-
|
|
20972
|
-
|
|
20973
|
-
|
|
21113
|
+
}
|
|
21114
|
+
function bootstrapFiles(cwd, files, sessionId) {
|
|
21115
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
21116
|
+
const absPath = join19(cwd, relPath);
|
|
21117
|
+
if (!existsSync13(absPath)) {
|
|
21118
|
+
try {
|
|
21119
|
+
const dir = pathDirname(absPath);
|
|
21120
|
+
if (!existsSync13(dir))
|
|
21121
|
+
mkdirSync6(dir, { recursive: true });
|
|
21122
|
+
writeFileSync6(absPath, content, "utf-8");
|
|
21123
|
+
console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
|
|
21124
|
+
} catch (err) {
|
|
21125
|
+
console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
|
|
21126
|
+
}
|
|
21127
|
+
}
|
|
20974
21128
|
}
|
|
20975
|
-
return resolveSourceStaticClientRoot(import.meta.url);
|
|
20976
21129
|
}
|
|
20977
|
-
|
|
20978
|
-
|
|
21130
|
+
function spawnTmuxSession(tmuxName, cols, rows, cwd, claudeBin, claudeArgs, env) {
|
|
21131
|
+
const exists = tmuxSessionExists2(tmuxName);
|
|
21132
|
+
if (!exists) {
|
|
21133
|
+
const shellCmd = [claudeBin, ...claudeArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
|
|
21134
|
+
execSync2(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
|
|
21135
|
+
console.log(`[relay] Created tmux session: ${tmuxName}`);
|
|
21136
|
+
} else {
|
|
21137
|
+
try {
|
|
21138
|
+
execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
|
|
21139
|
+
} catch {}
|
|
21140
|
+
console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
|
|
21141
|
+
}
|
|
21142
|
+
return pty.spawn("tmux", ["attach", "-t", tmuxName], {
|
|
21143
|
+
name: "xterm-256color",
|
|
21144
|
+
cols,
|
|
21145
|
+
rows,
|
|
21146
|
+
cwd,
|
|
21147
|
+
env
|
|
21148
|
+
});
|
|
20979
21149
|
}
|
|
20980
|
-
|
|
20981
|
-
const
|
|
20982
|
-
const
|
|
20983
|
-
const
|
|
20984
|
-
const
|
|
20985
|
-
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
|
|
20989
|
-
|
|
20990
|
-
|
|
20991
|
-
|
|
20992
|
-
|
|
20993
|
-
|
|
20994
|
-
|
|
20995
|
-
const result = await controlScoutWebPairingService(action, currentDirectory);
|
|
20996
|
-
shellStateCache.invalidate();
|
|
20997
|
-
return c.json(result);
|
|
20998
|
-
});
|
|
20999
|
-
app.delete("/api/pairing/peers/:fingerprint", async (c) => {
|
|
21000
|
-
const fingerprint = c.req.param("fingerprint");
|
|
21001
|
-
const removed = removeScoutPairingTrustedPeer(fingerprint);
|
|
21002
|
-
if (!removed) {
|
|
21003
|
-
return c.json({ error: "Peer not found" }, 404);
|
|
21150
|
+
function createSession(ws, msg) {
|
|
21151
|
+
const id = generateId();
|
|
21152
|
+
const cols = Math.max(msg.cols || 80, 20);
|
|
21153
|
+
const rows = Math.max(msg.rows || 24, 4);
|
|
21154
|
+
const backend = msg.backend || "pty";
|
|
21155
|
+
const tmuxName = msg.tmuxSession || `hudson-${id}`;
|
|
21156
|
+
const agent = msg.agent || "claude";
|
|
21157
|
+
let agentBin;
|
|
21158
|
+
if (agent === "pi") {
|
|
21159
|
+
agentBin = findPiBin();
|
|
21160
|
+
if (!agentBin) {
|
|
21161
|
+
const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
|
|
21162
|
+
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21163
|
+
send(ws, { type: "session:error", error: reason });
|
|
21164
|
+
return null;
|
|
21004
21165
|
}
|
|
21005
|
-
|
|
21006
|
-
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
return c.json(await loadAgentObserveSummaries(ids));
|
|
21013
|
-
});
|
|
21014
|
-
app.get("/api/agents/:id/observe", async (c) => {
|
|
21015
|
-
const payload = await loadAgentObservePayload(c.req.param("id"));
|
|
21016
|
-
return payload ? c.json(payload) : c.json({ error: "not found" }, 404);
|
|
21017
|
-
});
|
|
21018
|
-
app.get("/api/activity", (c) => c.json(queryActivity()));
|
|
21019
|
-
app.get("/api/heartrate", (c) => c.json(queryHeartrate()));
|
|
21020
|
-
app.get("/api/fleet", (c) => c.json(queryFleet({
|
|
21021
|
-
limit: parseOptionalPositiveInt(c.req.query("limit")),
|
|
21022
|
-
activityLimit: parseOptionalPositiveInt(c.req.query("activityLimit"))
|
|
21023
|
-
})));
|
|
21024
|
-
app.get("/api/messages", (c) => c.json(queryRecentMessages(parseOptionalPositiveInt(c.req.query("limit"), 80) ?? 80, { conversationId: c.req.query("conversationId") || undefined })));
|
|
21025
|
-
const handleListWork = (c) => {
|
|
21026
|
-
const agentId = c.req.query("agentId");
|
|
21027
|
-
const activeOnly = c.req.query("active") !== "false";
|
|
21028
|
-
return c.json(queryWorkItems({
|
|
21029
|
-
agentId: agentId || undefined,
|
|
21030
|
-
activeOnly
|
|
21031
|
-
}));
|
|
21032
|
-
};
|
|
21033
|
-
const handleWorkDetail = (c) => {
|
|
21034
|
-
const workId = c.req.param("id");
|
|
21035
|
-
if (!workId) {
|
|
21036
|
-
return c.json({ error: "id is required" }, 400);
|
|
21166
|
+
} else {
|
|
21167
|
+
agentBin = findClaudeBin();
|
|
21168
|
+
if (!agentBin) {
|
|
21169
|
+
const reason = "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code";
|
|
21170
|
+
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21171
|
+
send(ws, { type: "session:error", error: reason });
|
|
21172
|
+
return null;
|
|
21037
21173
|
}
|
|
21038
|
-
|
|
21039
|
-
|
|
21174
|
+
}
|
|
21175
|
+
if (!existsSync13(agentBin)) {
|
|
21176
|
+
const reason = `${agent} binary not found at ${agentBin}`;
|
|
21177
|
+
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21178
|
+
send(ws, { type: "session:error", error: reason });
|
|
21179
|
+
return null;
|
|
21180
|
+
}
|
|
21181
|
+
if (backend === "tmux" && !findBin("tmux")) {
|
|
21182
|
+
const reason = "tmux not found. Install it with: brew install tmux";
|
|
21183
|
+
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21184
|
+
send(ws, { type: "session:error", error: reason });
|
|
21185
|
+
return null;
|
|
21186
|
+
}
|
|
21187
|
+
const cwd = resolveCwd(msg.cwd);
|
|
21188
|
+
if (msg.workspaceFiles) {
|
|
21189
|
+
bootstrapFiles(cwd, msg.workspaceFiles, id);
|
|
21190
|
+
}
|
|
21191
|
+
let agentArgs;
|
|
21192
|
+
if (agent === "pi") {
|
|
21193
|
+
agentArgs = ["--verbose"];
|
|
21194
|
+
if (msg.provider)
|
|
21195
|
+
agentArgs.push("--provider", msg.provider);
|
|
21196
|
+
if (msg.model)
|
|
21197
|
+
agentArgs.push("--model", msg.model);
|
|
21198
|
+
if (msg.systemPrompt)
|
|
21199
|
+
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21200
|
+
} else {
|
|
21201
|
+
agentArgs = ["--verbose"];
|
|
21202
|
+
if (msg.systemPrompt)
|
|
21203
|
+
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21204
|
+
}
|
|
21205
|
+
const env = { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" };
|
|
21206
|
+
delete env.CLAUDECODE;
|
|
21207
|
+
let ptyProcess;
|
|
21208
|
+
try {
|
|
21209
|
+
if (backend === "tmux") {
|
|
21210
|
+
console.log(`[relay] Session ${id}: tmux backend (session: ${tmuxName}) in ${cwd} [agent: ${agent}]`);
|
|
21211
|
+
ptyProcess = spawnTmuxSession(tmuxName, cols, rows, cwd, agentBin, agentArgs, env);
|
|
21212
|
+
} else {
|
|
21213
|
+
console.log(`[relay] Session ${id}: pty backend, spawning ${agentBin} in ${cwd} [agent: ${agent}]`);
|
|
21214
|
+
ptyProcess = pty.spawn(agentBin, agentArgs, {
|
|
21215
|
+
name: "xterm-256color",
|
|
21216
|
+
cols,
|
|
21217
|
+
rows,
|
|
21218
|
+
cwd,
|
|
21219
|
+
env
|
|
21220
|
+
});
|
|
21221
|
+
}
|
|
21222
|
+
} catch (err) {
|
|
21223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21224
|
+
console.error(`[relay] Session ${id}: failed to spawn PTY \u2014 ${message}`);
|
|
21225
|
+
send(ws, { type: "session:error", error: `Failed to spawn terminal: ${message}` });
|
|
21226
|
+
return null;
|
|
21227
|
+
}
|
|
21228
|
+
const orphanTTL = msg.orphanTTL && msg.orphanTTL > 0 ? msg.orphanTTL : DEFAULT_ORPHAN_TTL_MS;
|
|
21229
|
+
const session = {
|
|
21230
|
+
id,
|
|
21231
|
+
pty: ptyProcess,
|
|
21232
|
+
ws,
|
|
21233
|
+
outputBuffer: "",
|
|
21234
|
+
cols,
|
|
21235
|
+
rows,
|
|
21236
|
+
reapTimer: null,
|
|
21237
|
+
orphanTTL,
|
|
21238
|
+
backend,
|
|
21239
|
+
...backend === "tmux" ? { tmuxSession: tmuxName } : {},
|
|
21240
|
+
exited: false,
|
|
21241
|
+
exitCode: null
|
|
21040
21242
|
};
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
21045
|
-
|
|
21046
|
-
const agentId = c.req.query("agentId");
|
|
21047
|
-
const conversationId = c.req.query("conversationId");
|
|
21048
|
-
const collaborationRecordId = c.req.query("collaborationRecordId");
|
|
21049
|
-
const activeOnly = c.req.query("active") !== "false";
|
|
21050
|
-
return c.json(queryFlights({
|
|
21051
|
-
agentId: agentId || undefined,
|
|
21052
|
-
conversationId: conversationId || undefined,
|
|
21053
|
-
collaborationRecordId: collaborationRecordId || undefined,
|
|
21054
|
-
activeOnly
|
|
21055
|
-
}));
|
|
21056
|
-
});
|
|
21057
|
-
app.get("/api/sessions", (c) => c.json(querySessions()));
|
|
21058
|
-
app.get("/api/session/:id", (c) => {
|
|
21059
|
-
const session = querySessionById(c.req.param("id"));
|
|
21060
|
-
return session ? c.json(session) : c.json({ error: "not found" }, 404);
|
|
21061
|
-
});
|
|
21062
|
-
app.get("/api/mesh", async (c) => {
|
|
21063
|
-
try {
|
|
21064
|
-
return c.json(await loadMeshStatus());
|
|
21065
|
-
} catch (err) {
|
|
21066
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21067
|
-
return c.json({ error: message }, 500);
|
|
21243
|
+
const startTime = Date.now();
|
|
21244
|
+
ptyProcess.onData((data) => {
|
|
21245
|
+
session.outputBuffer += data;
|
|
21246
|
+
if (session.outputBuffer.length > MAX_BUFFER_SIZE) {
|
|
21247
|
+
session.outputBuffer = session.outputBuffer.slice(-MAX_BUFFER_SIZE);
|
|
21068
21248
|
}
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
try {
|
|
21072
|
-
return c.json(await announceMeshVisibility());
|
|
21073
|
-
} catch (err) {
|
|
21074
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21075
|
-
return c.json({ error: message }, 500);
|
|
21249
|
+
if (session.ws && session.ws.readyState === 1) {
|
|
21250
|
+
send(session.ws, { type: "terminal:data", data });
|
|
21076
21251
|
}
|
|
21077
21252
|
});
|
|
21078
|
-
|
|
21079
|
-
|
|
21080
|
-
|
|
21081
|
-
|
|
21082
|
-
|
|
21083
|
-
|
|
21084
|
-
|
|
21085
|
-
|
|
21086
|
-
|
|
21087
|
-
|
|
21088
|
-
|
|
21089
|
-
|
|
21090
|
-
|
|
21091
|
-
verbosity: config.verbosity ?? "terse",
|
|
21092
|
-
tone: config.tone ?? "direct",
|
|
21093
|
-
quietHours: config.quietHours ?? "22:00 \u2013 07:00"
|
|
21094
|
-
});
|
|
21095
|
-
});
|
|
21096
|
-
app.get("/api/onboarding/state", async (c) => {
|
|
21097
|
-
const hasLocalConfig = localConfigExists();
|
|
21098
|
-
const settings = await readOpenScoutSettings({ currentDirectory }).catch(() => null);
|
|
21099
|
-
const configuredContextRoot = settings?.discovery.contextRoot ?? null;
|
|
21100
|
-
const discoveryStart = configuredContextRoot ?? currentDirectory;
|
|
21101
|
-
const projectRoot = await findNearestProjectRoot(discoveryStart).catch(() => null);
|
|
21102
|
-
const hasProjectConfig = projectRoot !== null;
|
|
21103
|
-
const userName = loadUserConfig().name?.trim() ?? "";
|
|
21104
|
-
return c.json({
|
|
21105
|
-
hasLocalConfig,
|
|
21106
|
-
hasProjectConfig,
|
|
21107
|
-
hasOperatorName: userName.length > 0,
|
|
21108
|
-
localConfigPath: localConfigPath(),
|
|
21109
|
-
localConfig: hasLocalConfig ? loadLocalConfig() : null,
|
|
21110
|
-
projectRoot,
|
|
21111
|
-
currentDirectory,
|
|
21112
|
-
contextRoot: configuredContextRoot,
|
|
21113
|
-
operatorName: userName || null,
|
|
21114
|
-
operatorNameSuggestion: resolveOperatorName()
|
|
21115
|
-
});
|
|
21116
|
-
});
|
|
21117
|
-
app.delete("/api/onboarding/state", (c) => {
|
|
21118
|
-
try {
|
|
21119
|
-
rmSync3(localConfigPath(), { force: true });
|
|
21120
|
-
} catch {}
|
|
21121
|
-
return c.json({ ok: true, localConfigPath: localConfigPath() });
|
|
21122
|
-
});
|
|
21123
|
-
app.post("/api/onboarding/project", async (c) => {
|
|
21124
|
-
const body = await c.req.json().catch(() => ({}));
|
|
21125
|
-
const contextRoot = body.contextRoot?.trim();
|
|
21126
|
-
if (!contextRoot) {
|
|
21127
|
-
return c.json({ error: "contextRoot is required" }, 400);
|
|
21253
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
21254
|
+
session.exited = true;
|
|
21255
|
+
session.exitCode = exitCode;
|
|
21256
|
+
const uptime = Date.now() - startTime;
|
|
21257
|
+
const crashed = exitCode !== 0 && uptime < 5000;
|
|
21258
|
+
let reason;
|
|
21259
|
+
if (crashed) {
|
|
21260
|
+
const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
|
|
21261
|
+
const lines = clean.split(`
|
|
21262
|
+
`).filter((l) => l.trim()).slice(-5);
|
|
21263
|
+
reason = lines.join(`
|
|
21264
|
+
`) || `Process exited with code ${exitCode}`;
|
|
21265
|
+
console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
|
|
21128
21266
|
}
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
try {
|
|
21132
|
-
await writeOpenScoutSettings({
|
|
21133
|
-
discovery: {
|
|
21134
|
-
contextRoot,
|
|
21135
|
-
workspaceRoots: sourceRoots
|
|
21136
|
-
},
|
|
21137
|
-
agents: { defaultHarness: harness }
|
|
21138
|
-
});
|
|
21139
|
-
const result = await initializeOpenScoutSetup({
|
|
21140
|
-
currentDirectory: contextRoot
|
|
21141
|
-
});
|
|
21142
|
-
return c.json({
|
|
21143
|
-
ok: true,
|
|
21144
|
-
projectConfigPath: result.currentProjectConfigPath
|
|
21145
|
-
});
|
|
21146
|
-
} catch (err) {
|
|
21147
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21148
|
-
console.error("[onboarding/project]", message);
|
|
21149
|
-
return c.json({ error: message }, 500);
|
|
21150
|
-
}
|
|
21151
|
-
});
|
|
21152
|
-
app.post("/api/onboarding/init", async (c) => {
|
|
21153
|
-
const body = await c.req.json().catch(() => ({}));
|
|
21154
|
-
writeLocalConfig({
|
|
21155
|
-
version: 1,
|
|
21156
|
-
host: body.host ?? DEFAULT_LOCAL_CONFIG.host,
|
|
21157
|
-
ports: {
|
|
21158
|
-
broker: body.ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker,
|
|
21159
|
-
web: body.ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web,
|
|
21160
|
-
pairing: body.ports?.pairing ?? DEFAULT_LOCAL_CONFIG.ports.pairing
|
|
21161
|
-
}
|
|
21162
|
-
});
|
|
21163
|
-
return c.json({
|
|
21164
|
-
ok: true,
|
|
21165
|
-
localConfig: loadLocalConfig(),
|
|
21166
|
-
localConfigPath: localConfigPath()
|
|
21167
|
-
});
|
|
21168
|
-
});
|
|
21169
|
-
app.post("/api/user", async (c) => {
|
|
21170
|
-
const body = await c.req.json();
|
|
21171
|
-
const config = loadUserConfig();
|
|
21172
|
-
const stringFields = [
|
|
21173
|
-
"name",
|
|
21174
|
-
"handle",
|
|
21175
|
-
"pronouns",
|
|
21176
|
-
"bio",
|
|
21177
|
-
"timezone",
|
|
21178
|
-
"workingHours",
|
|
21179
|
-
"interruptThreshold",
|
|
21180
|
-
"channel",
|
|
21181
|
-
"verbosity",
|
|
21182
|
-
"tone",
|
|
21183
|
-
"quietHours"
|
|
21184
|
-
];
|
|
21185
|
-
for (const key of stringFields) {
|
|
21186
|
-
if (key in body) {
|
|
21187
|
-
const val = body[key];
|
|
21188
|
-
if (typeof val === "string" && val.trim()) {
|
|
21189
|
-
config[key] = val.trim();
|
|
21190
|
-
} else {
|
|
21191
|
-
delete config[key];
|
|
21192
|
-
}
|
|
21193
|
-
}
|
|
21194
|
-
}
|
|
21195
|
-
if ("hue" in body && typeof body.hue === "number") {
|
|
21196
|
-
config.hue = body.hue;
|
|
21197
|
-
}
|
|
21198
|
-
if ("batchWindow" in body && typeof body.batchWindow === "number") {
|
|
21199
|
-
config.batchWindow = body.batchWindow;
|
|
21200
|
-
}
|
|
21201
|
-
saveUserConfig(config);
|
|
21202
|
-
return c.json({
|
|
21203
|
-
name: resolveOperatorName(),
|
|
21204
|
-
handle: config.handle ?? "",
|
|
21205
|
-
pronouns: config.pronouns ?? "",
|
|
21206
|
-
hue: config.hue ?? 195,
|
|
21207
|
-
bio: config.bio ?? "",
|
|
21208
|
-
timezone: config.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
21209
|
-
workingHours: config.workingHours ?? "08:00 \u2013 18:00",
|
|
21210
|
-
interruptThreshold: config.interruptThreshold ?? "blocking-only",
|
|
21211
|
-
batchWindow: config.batchWindow ?? 15,
|
|
21212
|
-
channel: config.channel ?? "here+mobile",
|
|
21213
|
-
verbosity: config.verbosity ?? "terse",
|
|
21214
|
-
tone: config.tone ?? "direct",
|
|
21215
|
-
quietHours: config.quietHours ?? "22:00 \u2013 07:00"
|
|
21216
|
-
});
|
|
21217
|
-
});
|
|
21218
|
-
app.post("/api/agents/:agentId/interrupt", async (c) => {
|
|
21219
|
-
const agentId = c.req.param("agentId");
|
|
21220
|
-
const { interruptLocalAgent: interruptLocalAgent2 } = await init_local_agents().then(() => exports_local_agents);
|
|
21221
|
-
const result = await interruptLocalAgent2(agentId);
|
|
21222
|
-
if (!result.ok)
|
|
21223
|
-
return c.json({ error: "Agent not found or not interruptible" }, 404);
|
|
21224
|
-
return c.json({ ok: true });
|
|
21225
|
-
});
|
|
21226
|
-
app.post("/api/send", async (c) => {
|
|
21227
|
-
const { body, conversationId } = await c.req.json();
|
|
21228
|
-
if (!body?.trim()) {
|
|
21229
|
-
return c.json({ error: "body is required" }, 400);
|
|
21230
|
-
}
|
|
21231
|
-
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21232
|
-
if (directAgentId) {
|
|
21233
|
-
const result2 = await sendScoutDirectMessage({
|
|
21234
|
-
agentId: directAgentId,
|
|
21235
|
-
body: body.trim(),
|
|
21236
|
-
currentDirectory,
|
|
21237
|
-
source: "scout-web"
|
|
21238
|
-
});
|
|
21239
|
-
return c.json(result2);
|
|
21240
|
-
}
|
|
21241
|
-
const result = await sendScoutMessage({
|
|
21242
|
-
senderId,
|
|
21243
|
-
body: body.trim(),
|
|
21244
|
-
currentDirectory
|
|
21245
|
-
});
|
|
21246
|
-
if (!result.usedBroker) {
|
|
21247
|
-
return c.json({ error: "broker unreachable" }, 502);
|
|
21248
|
-
}
|
|
21249
|
-
return c.json(result);
|
|
21250
|
-
});
|
|
21251
|
-
app.post("/api/ask", async (c) => {
|
|
21252
|
-
const { body, conversationId } = await c.req.json();
|
|
21253
|
-
if (!body?.trim()) {
|
|
21254
|
-
return c.json({ error: "body is required" }, 400);
|
|
21255
|
-
}
|
|
21256
|
-
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21257
|
-
if (!directAgentId) {
|
|
21258
|
-
return c.json({
|
|
21259
|
-
error: "ask is only available in a direct conversation with one agent"
|
|
21260
|
-
}, 400);
|
|
21261
|
-
}
|
|
21262
|
-
const result = await askScoutQuestion({
|
|
21263
|
-
senderId,
|
|
21264
|
-
targetLabel: directAgentId,
|
|
21265
|
-
body: body.trim(),
|
|
21266
|
-
currentDirectory
|
|
21267
|
-
});
|
|
21268
|
-
if (!result.usedBroker) {
|
|
21269
|
-
return c.json({ error: "broker unreachable" }, 502);
|
|
21270
|
-
}
|
|
21271
|
-
if (result.unresolvedTarget) {
|
|
21272
|
-
return c.json({
|
|
21273
|
-
error: `could not route ask to ${result.unresolvedTarget}`,
|
|
21274
|
-
targetDiagnostic: result.targetDiagnostic ?? null
|
|
21275
|
-
}, 409);
|
|
21276
|
-
}
|
|
21277
|
-
return c.json(result);
|
|
21278
|
-
});
|
|
21279
|
-
app.get("/api/events", async (c) => {
|
|
21280
|
-
const brokerHost = process.env.OPENSCOUT_BROKER_HOST ?? "127.0.0.1";
|
|
21281
|
-
const brokerPort = process.env.OPENSCOUT_BROKER_PORT ?? "65535";
|
|
21282
|
-
const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? `http://${brokerHost}:${brokerPort}`;
|
|
21283
|
-
try {
|
|
21284
|
-
return await relayEventStream(`${brokerUrl}/v1/events/stream`, {
|
|
21285
|
-
signal: c.req.raw.signal
|
|
21286
|
-
});
|
|
21287
|
-
} catch {
|
|
21288
|
-
return c.text("Broker unreachable", 502);
|
|
21289
|
-
}
|
|
21290
|
-
});
|
|
21291
|
-
await registerScoutWebAssets(app, {
|
|
21292
|
-
assetMode: options.assetMode,
|
|
21293
|
-
staticRoot: resolveStaticRoot(options.staticRoot),
|
|
21294
|
-
viteDevUrl: options.viteDevUrl,
|
|
21295
|
-
defaultViteUrl: "http://127.0.0.1:5180"
|
|
21296
|
-
});
|
|
21297
|
-
const warmupCaches = () => Promise.allSettled([
|
|
21298
|
-
shellStateCache.refresh(),
|
|
21299
|
-
loadPairingState(currentDirectory, true)
|
|
21300
|
-
]).then((results) => {
|
|
21301
|
-
for (const result of results) {
|
|
21302
|
-
if (result.status === "rejected") {
|
|
21303
|
-
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
21304
|
-
console.error("[openscout-web api] initial cache warmup failed:", message);
|
|
21305
|
-
}
|
|
21267
|
+
if (session.ws) {
|
|
21268
|
+
send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
|
|
21306
21269
|
}
|
|
21270
|
+
scheduleReap(session, 1e4);
|
|
21307
21271
|
});
|
|
21308
|
-
|
|
21309
|
-
}
|
|
21310
|
-
|
|
21311
|
-
// ../hudson/packages/hudson-relay/src/relay/session.ts
|
|
21312
|
-
import { createRequire } from "module";
|
|
21313
|
-
import { execSync as execSync2 } from "child_process";
|
|
21314
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
21315
|
-
import { join as join19, dirname as pathDirname } from "path";
|
|
21316
|
-
var require2 = createRequire(import.meta.url);
|
|
21317
|
-
var pty = require2("node-pty");
|
|
21318
|
-
var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
|
|
21319
|
-
var MAX_BUFFER_SIZE = 512 * 1024;
|
|
21320
|
-
var sessions3 = new Map;
|
|
21321
|
-
function generateId() {
|
|
21322
|
-
return Math.random().toString(36).slice(2, 10);
|
|
21272
|
+
sessions3.set(id, session);
|
|
21273
|
+
console.log(`[relay] Session ${id} created (${cols}x${rows})`);
|
|
21274
|
+
return session;
|
|
21323
21275
|
}
|
|
21324
|
-
function
|
|
21325
|
-
if (
|
|
21326
|
-
|
|
21276
|
+
function attachSession(session, ws, cols, rows) {
|
|
21277
|
+
if (session.reapTimer) {
|
|
21278
|
+
clearTimeout(session.reapTimer);
|
|
21279
|
+
session.reapTimer = null;
|
|
21327
21280
|
}
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
return expanded;
|
|
21338
|
-
} catch {
|
|
21339
|
-
console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
|
|
21340
|
-
return home;
|
|
21281
|
+
session.ws = ws;
|
|
21282
|
+
if (cols && rows) {
|
|
21283
|
+
const c = Math.max(cols, 20);
|
|
21284
|
+
const r = Math.max(rows, 4);
|
|
21285
|
+
if (c !== session.cols || r !== session.rows) {
|
|
21286
|
+
session.pty.resize(c, r);
|
|
21287
|
+
session.cols = c;
|
|
21288
|
+
session.rows = r;
|
|
21289
|
+
}
|
|
21341
21290
|
}
|
|
21342
|
-
|
|
21343
|
-
|
|
21344
|
-
if (
|
|
21345
|
-
|
|
21346
|
-
try {
|
|
21347
|
-
return execSync2(`which ${name}`, { encoding: "utf8" }).trim() || null;
|
|
21348
|
-
} catch {
|
|
21349
|
-
return null;
|
|
21291
|
+
if (session.exited) {
|
|
21292
|
+
send(ws, { type: "session:exit", exitCode: session.exitCode });
|
|
21293
|
+
} else if (session.outputBuffer.length > 0) {
|
|
21294
|
+
send(ws, { type: "terminal:data", data: session.outputBuffer });
|
|
21350
21295
|
}
|
|
21296
|
+
console.log(`[relay] Session ${session.id} reconnected`);
|
|
21351
21297
|
}
|
|
21352
|
-
function
|
|
21353
|
-
|
|
21298
|
+
function detachSession(session) {
|
|
21299
|
+
session.ws = null;
|
|
21300
|
+
if (session.exited) {
|
|
21301
|
+
scheduleReap(session, 5000);
|
|
21302
|
+
} else {
|
|
21303
|
+
scheduleReap(session, session.orphanTTL);
|
|
21304
|
+
console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
|
|
21305
|
+
}
|
|
21354
21306
|
}
|
|
21355
|
-
function
|
|
21356
|
-
|
|
21307
|
+
function scheduleReap(session, delay) {
|
|
21308
|
+
if (session.reapTimer)
|
|
21309
|
+
clearTimeout(session.reapTimer);
|
|
21310
|
+
session.reapTimer = setTimeout(() => {
|
|
21311
|
+
if (!session.ws) {
|
|
21312
|
+
destroy(session.id);
|
|
21313
|
+
}
|
|
21314
|
+
}, delay);
|
|
21357
21315
|
}
|
|
21358
|
-
function
|
|
21316
|
+
function destroy(sessionId) {
|
|
21317
|
+
const session = sessions3.get(sessionId);
|
|
21318
|
+
if (!session)
|
|
21319
|
+
return;
|
|
21320
|
+
if (session.reapTimer)
|
|
21321
|
+
clearTimeout(session.reapTimer);
|
|
21359
21322
|
try {
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
}
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
|
|
21375
|
-
|
|
21376
|
-
|
|
21377
|
-
|
|
21323
|
+
session.pty.kill();
|
|
21324
|
+
} catch {}
|
|
21325
|
+
sessions3.delete(sessionId);
|
|
21326
|
+
if (session.backend === "tmux") {
|
|
21327
|
+
console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
|
|
21328
|
+
} else {
|
|
21329
|
+
console.log(`[relay] Session ${sessionId} destroyed`);
|
|
21330
|
+
}
|
|
21331
|
+
}
|
|
21332
|
+
|
|
21333
|
+
// packages/web/server/relay.ts
|
|
21334
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
21335
|
+
import { join as join20 } from "path";
|
|
21336
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
21337
|
+
var UPLOAD_DIR = "/tmp/scout-uploads";
|
|
21338
|
+
async function handleRelayUpload(req) {
|
|
21339
|
+
const { name, data } = await req.json();
|
|
21340
|
+
if (!name || !data) {
|
|
21341
|
+
return Response.json({ error: "Missing name or data" }, { status: 400 });
|
|
21342
|
+
}
|
|
21343
|
+
await mkdir7(UPLOAD_DIR, { recursive: true });
|
|
21344
|
+
const filename = `${randomUUID3()}-${name}`;
|
|
21345
|
+
const filepath = join20(UPLOAD_DIR, filename);
|
|
21346
|
+
await Bun.write(filepath, Buffer.from(data, "base64"));
|
|
21347
|
+
return Response.json({ path: filepath });
|
|
21348
|
+
}
|
|
21349
|
+
var _pendingCommand = null;
|
|
21350
|
+
function queueTerminalCommand(cmd) {
|
|
21351
|
+
for (const [, session] of sessions3) {
|
|
21352
|
+
if (!session.exited) {
|
|
21353
|
+
session.pty.write(cmd + `
|
|
21354
|
+
`);
|
|
21355
|
+
return;
|
|
21356
|
+
}
|
|
21357
|
+
}
|
|
21358
|
+
_pendingCommand = cmd;
|
|
21359
|
+
}
|
|
21360
|
+
function drainPendingCommand() {
|
|
21361
|
+
const cmd = _pendingCommand;
|
|
21362
|
+
_pendingCommand = null;
|
|
21363
|
+
return cmd;
|
|
21364
|
+
}
|
|
21365
|
+
function asRelay(ws) {
|
|
21366
|
+
return ws;
|
|
21367
|
+
}
|
|
21368
|
+
function parseMessage(raw2) {
|
|
21369
|
+
try {
|
|
21370
|
+
const msg = JSON.parse(raw2);
|
|
21371
|
+
if (typeof msg.type === "string")
|
|
21372
|
+
return msg;
|
|
21373
|
+
} catch {}
|
|
21374
|
+
return null;
|
|
21375
|
+
}
|
|
21376
|
+
var relayWebSocket = {
|
|
21377
|
+
open(_ws) {},
|
|
21378
|
+
message(ws, raw2) {
|
|
21379
|
+
const msg = parseMessage(typeof raw2 === "string" ? raw2 : raw2.toString());
|
|
21380
|
+
if (!msg)
|
|
21381
|
+
return;
|
|
21382
|
+
const rws = asRelay(ws);
|
|
21383
|
+
switch (msg.type) {
|
|
21384
|
+
case "session:init": {
|
|
21385
|
+
if (ws.data.sessionId) {
|
|
21386
|
+
const prev = sessions3.get(ws.data.sessionId);
|
|
21387
|
+
if (prev)
|
|
21388
|
+
detachSession(prev);
|
|
21389
|
+
}
|
|
21390
|
+
const session = createSession(rws, msg);
|
|
21391
|
+
if (!session)
|
|
21392
|
+
break;
|
|
21393
|
+
ws.data.sessionId = session.id;
|
|
21394
|
+
send(rws, { type: "session:ready", sessionId: session.id });
|
|
21395
|
+
const pending = drainPendingCommand();
|
|
21396
|
+
if (pending)
|
|
21397
|
+
setTimeout(() => session.pty.write(pending + `
|
|
21398
|
+
`), 400);
|
|
21399
|
+
break;
|
|
21400
|
+
}
|
|
21401
|
+
case "session:reconnect": {
|
|
21402
|
+
const existing = sessions3.get(msg.sessionId);
|
|
21403
|
+
if (existing && !existing.exited) {
|
|
21404
|
+
if (ws.data.sessionId && ws.data.sessionId !== msg.sessionId) {
|
|
21405
|
+
const prev = sessions3.get(ws.data.sessionId);
|
|
21406
|
+
if (prev)
|
|
21407
|
+
detachSession(prev);
|
|
21408
|
+
}
|
|
21409
|
+
if (existing.ws && existing.ws !== rws) {
|
|
21410
|
+
send(existing.ws, { type: "session:detached" });
|
|
21411
|
+
}
|
|
21412
|
+
ws.data.sessionId = existing.id;
|
|
21413
|
+
attachSession(existing, rws, msg.cols, msg.rows);
|
|
21414
|
+
send(rws, {
|
|
21415
|
+
type: "session:ready",
|
|
21416
|
+
sessionId: existing.id,
|
|
21417
|
+
reconnected: true
|
|
21418
|
+
});
|
|
21419
|
+
const pending = drainPendingCommand();
|
|
21420
|
+
if (pending)
|
|
21421
|
+
setTimeout(() => existing.pty.write(pending + `
|
|
21422
|
+
`), 400);
|
|
21423
|
+
} else {
|
|
21424
|
+
send(rws, { type: "session:expired", sessionId: msg.sessionId });
|
|
21425
|
+
}
|
|
21426
|
+
break;
|
|
21427
|
+
}
|
|
21428
|
+
case "terminal:input": {
|
|
21429
|
+
if (!ws.data.sessionId)
|
|
21430
|
+
return;
|
|
21431
|
+
const session = sessions3.get(ws.data.sessionId);
|
|
21432
|
+
if (session && !session.exited) {
|
|
21433
|
+
session.pty.write(msg.data);
|
|
21434
|
+
}
|
|
21435
|
+
break;
|
|
21436
|
+
}
|
|
21437
|
+
case "terminal:resize": {
|
|
21438
|
+
if (!ws.data.sessionId)
|
|
21439
|
+
return;
|
|
21440
|
+
const session = sessions3.get(ws.data.sessionId);
|
|
21441
|
+
if (session && !session.exited) {
|
|
21442
|
+
const cols = Math.max(msg.cols || 80, 20);
|
|
21443
|
+
const rows = Math.max(msg.rows || 24, 4);
|
|
21444
|
+
session.pty.resize(cols, rows);
|
|
21445
|
+
session.cols = cols;
|
|
21446
|
+
session.rows = rows;
|
|
21447
|
+
}
|
|
21448
|
+
break;
|
|
21378
21449
|
}
|
|
21379
21450
|
}
|
|
21451
|
+
},
|
|
21452
|
+
close(ws) {
|
|
21453
|
+
if (ws.data.sessionId) {
|
|
21454
|
+
const session = sessions3.get(ws.data.sessionId);
|
|
21455
|
+
if (session)
|
|
21456
|
+
detachSession(session);
|
|
21457
|
+
ws.data.sessionId = null;
|
|
21458
|
+
}
|
|
21380
21459
|
}
|
|
21460
|
+
};
|
|
21461
|
+
function destroyAllRelaySessions() {
|
|
21462
|
+
for (const [id] of sessions3)
|
|
21463
|
+
destroy(id);
|
|
21381
21464
|
}
|
|
21382
|
-
|
|
21383
|
-
|
|
21384
|
-
|
|
21385
|
-
|
|
21386
|
-
|
|
21387
|
-
console.log(`[relay] Created tmux session: ${tmuxName}`);
|
|
21388
|
-
} else {
|
|
21389
|
-
try {
|
|
21390
|
-
execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
|
|
21391
|
-
} catch {}
|
|
21392
|
-
console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
|
|
21465
|
+
|
|
21466
|
+
// packages/web/server/create-openscout-web-server.ts
|
|
21467
|
+
function parseOptionalPositiveInt(value, fallback) {
|
|
21468
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
21469
|
+
return fallback;
|
|
21393
21470
|
}
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
cols,
|
|
21397
|
-
rows,
|
|
21398
|
-
cwd,
|
|
21399
|
-
env
|
|
21400
|
-
});
|
|
21471
|
+
const parsed = Number.parseInt(value, 10);
|
|
21472
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
21401
21473
|
}
|
|
21402
|
-
function
|
|
21403
|
-
|
|
21404
|
-
|
|
21405
|
-
|
|
21406
|
-
const backend = msg.backend || "pty";
|
|
21407
|
-
const tmuxName = msg.tmuxSession || `hudson-${id}`;
|
|
21408
|
-
const agent = msg.agent || "claude";
|
|
21409
|
-
let agentBin;
|
|
21410
|
-
if (agent === "pi") {
|
|
21411
|
-
agentBin = findPiBin();
|
|
21412
|
-
if (!agentBin) {
|
|
21413
|
-
const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
|
|
21414
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21415
|
-
send(ws, { type: "session:error", error: reason });
|
|
21416
|
-
return null;
|
|
21474
|
+
function inferDirectTargetAgentId(conversationId, session, senderId) {
|
|
21475
|
+
if (session?.kind === "direct") {
|
|
21476
|
+
if (session.agentId) {
|
|
21477
|
+
return session.agentId;
|
|
21417
21478
|
}
|
|
21418
|
-
|
|
21419
|
-
|
|
21420
|
-
|
|
21421
|
-
const
|
|
21422
|
-
|
|
21423
|
-
|
|
21424
|
-
|
|
21479
|
+
const participants = session.participantIds.filter((participantId) => participantId.trim().length > 0);
|
|
21480
|
+
if (participants.length === 2) {
|
|
21481
|
+
const operatorCandidates = new Set([senderId.trim(), "operator"]);
|
|
21482
|
+
const nonOperatorParticipants = participants.filter((participantId) => !operatorCandidates.has(participantId));
|
|
21483
|
+
if (nonOperatorParticipants.length === 1) {
|
|
21484
|
+
return nonOperatorParticipants[0] ?? null;
|
|
21485
|
+
}
|
|
21486
|
+
const localSessionParticipant = nonOperatorParticipants.find((participantId) => participantId.startsWith("local-session-agent-")) ?? participants.find((participantId) => participantId.startsWith("local-session-agent-"));
|
|
21487
|
+
if (localSessionParticipant) {
|
|
21488
|
+
return localSessionParticipant;
|
|
21489
|
+
}
|
|
21490
|
+
return participants[0] ?? null;
|
|
21425
21491
|
}
|
|
21426
21492
|
}
|
|
21427
|
-
if (
|
|
21428
|
-
const
|
|
21429
|
-
|
|
21430
|
-
send(ws, { type: "session:error", error: reason });
|
|
21431
|
-
return null;
|
|
21432
|
-
}
|
|
21433
|
-
if (backend === "tmux" && !findBin("tmux")) {
|
|
21434
|
-
const reason = "tmux not found. Install it with: brew install tmux";
|
|
21435
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21436
|
-
send(ws, { type: "session:error", error: reason });
|
|
21437
|
-
return null;
|
|
21493
|
+
if (conversationId?.startsWith("dm.operator.")) {
|
|
21494
|
+
const legacyAgentId = conversationId.slice("dm.operator.".length);
|
|
21495
|
+
return legacyAgentId || null;
|
|
21438
21496
|
}
|
|
21439
|
-
|
|
21440
|
-
|
|
21441
|
-
|
|
21497
|
+
return null;
|
|
21498
|
+
}
|
|
21499
|
+
function inferDirectSenderId(_session, _fallbackSenderId, _directTargetAgentId) {
|
|
21500
|
+
return "operator";
|
|
21501
|
+
}
|
|
21502
|
+
function resolveConversationRouting(conversationId) {
|
|
21503
|
+
const fallbackSenderId = "operator";
|
|
21504
|
+
const session = conversationId ? querySessionById(conversationId) : null;
|
|
21505
|
+
const directAgentId = inferDirectTargetAgentId(conversationId, session, fallbackSenderId);
|
|
21506
|
+
const senderId = inferDirectSenderId(session, fallbackSenderId, directAgentId);
|
|
21507
|
+
return { directAgentId, senderId };
|
|
21508
|
+
}
|
|
21509
|
+
function resolveBundledStaticClientRoot(moduleUrl = import.meta.url) {
|
|
21510
|
+
return resolve9(dirname12(fileURLToPath7(moduleUrl.toString())), "client");
|
|
21511
|
+
}
|
|
21512
|
+
function resolveSourceStaticClientRoot(moduleUrl = import.meta.url) {
|
|
21513
|
+
return resolve9(dirname12(fileURLToPath7(moduleUrl.toString())), "../dist/client");
|
|
21514
|
+
}
|
|
21515
|
+
function resolveStaticRoot(staticRoot) {
|
|
21516
|
+
const configured = staticRoot?.trim();
|
|
21517
|
+
if (configured) {
|
|
21518
|
+
return configured;
|
|
21442
21519
|
}
|
|
21443
|
-
|
|
21444
|
-
if (
|
|
21445
|
-
|
|
21446
|
-
if (msg.provider)
|
|
21447
|
-
agentArgs.push("--provider", msg.provider);
|
|
21448
|
-
if (msg.model)
|
|
21449
|
-
agentArgs.push("--model", msg.model);
|
|
21450
|
-
if (msg.systemPrompt)
|
|
21451
|
-
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21452
|
-
} else {
|
|
21453
|
-
agentArgs = ["--verbose"];
|
|
21454
|
-
if (msg.systemPrompt)
|
|
21455
|
-
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21520
|
+
const bundled = resolveBundledStaticClientRoot(import.meta.url);
|
|
21521
|
+
if (existsSync14(resolve9(bundled, "index.html"))) {
|
|
21522
|
+
return bundled;
|
|
21456
21523
|
}
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
21460
|
-
|
|
21461
|
-
|
|
21462
|
-
|
|
21463
|
-
|
|
21464
|
-
|
|
21465
|
-
|
|
21466
|
-
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21524
|
+
return resolveSourceStaticClientRoot(import.meta.url);
|
|
21525
|
+
}
|
|
21526
|
+
async function loadPairingState(currentDirectory, refresh) {
|
|
21527
|
+
return refresh ? refreshScoutWebPairingState(currentDirectory) : getScoutWebPairingState(currentDirectory);
|
|
21528
|
+
}
|
|
21529
|
+
async function createOpenScoutWebServer(options) {
|
|
21530
|
+
const shellTtl = options.shellStateCacheTtlMs ?? 15000;
|
|
21531
|
+
const currentDirectory = options.currentDirectory;
|
|
21532
|
+
const app = new Hono2;
|
|
21533
|
+
const shellStateCache = createCachedSnapshot(loadOpenScoutWebShellState, shellTtl);
|
|
21534
|
+
installScoutApiMiddleware(app, "openscout-web api");
|
|
21535
|
+
app.get("/api/health", (c) => c.json({
|
|
21536
|
+
ok: true,
|
|
21537
|
+
surface: "openscout-web",
|
|
21538
|
+
currentDirectory
|
|
21539
|
+
}));
|
|
21540
|
+
app.get("/api/pairing-state", async (c) => c.json(await loadPairingState(currentDirectory, false)));
|
|
21541
|
+
app.get("/api/pairing-state/refresh", async (c) => c.json(await loadPairingState(currentDirectory, true)));
|
|
21542
|
+
app.post("/api/pairing/control", async (c) => {
|
|
21543
|
+
const { action } = await c.req.json();
|
|
21544
|
+
const result = await controlScoutWebPairingService(action, currentDirectory);
|
|
21545
|
+
shellStateCache.invalidate();
|
|
21546
|
+
return c.json(result);
|
|
21547
|
+
});
|
|
21548
|
+
app.delete("/api/pairing/peers/:fingerprint", async (c) => {
|
|
21549
|
+
const fingerprint = c.req.param("fingerprint");
|
|
21550
|
+
const removed = removeScoutPairingTrustedPeer(fingerprint);
|
|
21551
|
+
if (!removed) {
|
|
21552
|
+
return c.json({ error: "Peer not found" }, 404);
|
|
21473
21553
|
}
|
|
21474
|
-
|
|
21475
|
-
|
|
21476
|
-
|
|
21477
|
-
|
|
21478
|
-
|
|
21479
|
-
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
|
|
21490
|
-
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
|
|
21554
|
+
return c.json({ ok: true });
|
|
21555
|
+
});
|
|
21556
|
+
app.get("/api/shell-state", async (c) => c.json(await shellStateCache.get()));
|
|
21557
|
+
app.get("/api/shell-state/refresh", async (c) => c.json(await shellStateCache.refresh()));
|
|
21558
|
+
app.get("/api/agents", (c) => c.json(queryAgents()));
|
|
21559
|
+
app.get("/api/observe/agents", async (c) => {
|
|
21560
|
+
const ids = c.req.query("ids")?.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
21561
|
+
return c.json(await loadAgentObserveSummaries(ids));
|
|
21562
|
+
});
|
|
21563
|
+
app.get("/api/agents/:id/observe", async (c) => {
|
|
21564
|
+
const payload = await loadAgentObservePayload(c.req.param("id"));
|
|
21565
|
+
return payload ? c.json(payload) : c.json({ error: "not found" }, 404);
|
|
21566
|
+
});
|
|
21567
|
+
app.get("/api/agents/:id/session-catalog", (c) => {
|
|
21568
|
+
const agentId = c.req.param("id");
|
|
21569
|
+
const agents = queryAgents();
|
|
21570
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
21571
|
+
if (!agent)
|
|
21572
|
+
return c.json({ error: "agent not found" }, 404);
|
|
21573
|
+
const runtimeDir = relayAgentRuntimeDirectory(agent.id);
|
|
21574
|
+
const catalog = readSessionCatalogSync(runtimeDir);
|
|
21575
|
+
const cwd = agent.cwd ?? agent.projectRoot ?? ".";
|
|
21576
|
+
const sessionId = catalog.activeSessionId;
|
|
21577
|
+
const harnessEntry = findHarnessEntry(agent.harness);
|
|
21578
|
+
const resumeCommand = sessionId && harnessEntry ? buildHarnessResumeCommand(harnessEntry, sessionId, cwd) : null;
|
|
21579
|
+
return c.json({ ...catalog, agentId, harness: agent.harness, resumeCommand });
|
|
21580
|
+
});
|
|
21581
|
+
app.get("/api/activity", (c) => c.json(queryActivity()));
|
|
21582
|
+
app.get("/api/heartrate", (c) => c.json(queryHeartrate()));
|
|
21583
|
+
app.get("/api/fleet", (c) => c.json(queryFleet({
|
|
21584
|
+
limit: parseOptionalPositiveInt(c.req.query("limit")),
|
|
21585
|
+
activityLimit: parseOptionalPositiveInt(c.req.query("activityLimit"))
|
|
21586
|
+
})));
|
|
21587
|
+
app.get("/api/messages", (c) => c.json(queryRecentMessages(parseOptionalPositiveInt(c.req.query("limit"), 80) ?? 80, { conversationId: c.req.query("conversationId") || undefined })));
|
|
21588
|
+
const handleListWork = (c) => {
|
|
21589
|
+
const agentId = c.req.query("agentId");
|
|
21590
|
+
const activeOnly = c.req.query("active") !== "false";
|
|
21591
|
+
return c.json(queryWorkItems({
|
|
21592
|
+
agentId: agentId || undefined,
|
|
21593
|
+
activeOnly
|
|
21594
|
+
}));
|
|
21494
21595
|
};
|
|
21495
|
-
const
|
|
21496
|
-
|
|
21497
|
-
|
|
21498
|
-
|
|
21499
|
-
|
|
21596
|
+
const handleWorkDetail = (c) => {
|
|
21597
|
+
const workId = c.req.param("id");
|
|
21598
|
+
if (!workId) {
|
|
21599
|
+
return c.json({ error: "id is required" }, 400);
|
|
21600
|
+
}
|
|
21601
|
+
const detail = queryWorkItemById(workId);
|
|
21602
|
+
return detail ? c.json(detail) : c.json({ error: "not found" }, 404);
|
|
21603
|
+
};
|
|
21604
|
+
app.get("/api/work", handleListWork);
|
|
21605
|
+
app.get("/api/tasks", handleListWork);
|
|
21606
|
+
app.get("/api/work/:id", handleWorkDetail);
|
|
21607
|
+
app.get("/api/tasks/:id", handleWorkDetail);
|
|
21608
|
+
app.get("/api/flights", (c) => {
|
|
21609
|
+
const agentId = c.req.query("agentId");
|
|
21610
|
+
const conversationId = c.req.query("conversationId");
|
|
21611
|
+
const collaborationRecordId = c.req.query("collaborationRecordId");
|
|
21612
|
+
const activeOnly = c.req.query("active") !== "false";
|
|
21613
|
+
return c.json(queryFlights({
|
|
21614
|
+
agentId: agentId || undefined,
|
|
21615
|
+
conversationId: conversationId || undefined,
|
|
21616
|
+
collaborationRecordId: collaborationRecordId || undefined,
|
|
21617
|
+
activeOnly
|
|
21618
|
+
}));
|
|
21619
|
+
});
|
|
21620
|
+
app.get("/api/sessions", (c) => c.json(querySessions()));
|
|
21621
|
+
app.get("/api/session/:id", (c) => {
|
|
21622
|
+
const session = querySessionById(c.req.param("id"));
|
|
21623
|
+
return session ? c.json(session) : c.json({ error: "not found" }, 404);
|
|
21624
|
+
});
|
|
21625
|
+
app.get("/api/mesh", async (c) => {
|
|
21626
|
+
try {
|
|
21627
|
+
return c.json(await loadMeshStatus());
|
|
21628
|
+
} catch (err) {
|
|
21629
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21630
|
+
return c.json({ error: message }, 500);
|
|
21631
|
+
}
|
|
21632
|
+
});
|
|
21633
|
+
app.post("/api/mesh/announce", async (c) => {
|
|
21634
|
+
try {
|
|
21635
|
+
return c.json(await announceMeshVisibility());
|
|
21636
|
+
} catch (err) {
|
|
21637
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21638
|
+
return c.json({ error: message }, 500);
|
|
21639
|
+
}
|
|
21640
|
+
});
|
|
21641
|
+
app.post("/api/mesh/tailscale", async (c) => {
|
|
21642
|
+
try {
|
|
21643
|
+
const { action } = await c.req.json();
|
|
21644
|
+
return c.json(await controlTailscale(action));
|
|
21645
|
+
} catch (err) {
|
|
21646
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21647
|
+
return c.json({ error: message }, 500);
|
|
21648
|
+
}
|
|
21649
|
+
});
|
|
21650
|
+
app.get("/api/user", (c) => {
|
|
21651
|
+
const config = loadUserConfig();
|
|
21652
|
+
return c.json({
|
|
21653
|
+
name: resolveOperatorName(),
|
|
21654
|
+
handle: config.handle ?? "",
|
|
21655
|
+
pronouns: config.pronouns ?? "",
|
|
21656
|
+
hue: config.hue ?? 195,
|
|
21657
|
+
bio: config.bio ?? "",
|
|
21658
|
+
timezone: config.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
21659
|
+
workingHours: config.workingHours ?? "08:00 \u2013 18:00",
|
|
21660
|
+
interruptThreshold: config.interruptThreshold ?? "blocking-only",
|
|
21661
|
+
batchWindow: config.batchWindow ?? 15,
|
|
21662
|
+
channel: config.channel ?? "here+mobile",
|
|
21663
|
+
verbosity: config.verbosity ?? "terse",
|
|
21664
|
+
tone: config.tone ?? "direct",
|
|
21665
|
+
quietHours: config.quietHours ?? "22:00 \u2013 07:00"
|
|
21666
|
+
});
|
|
21667
|
+
});
|
|
21668
|
+
app.get("/api/onboarding/state", async (c) => {
|
|
21669
|
+
const hasLocalConfig = localConfigExists();
|
|
21670
|
+
const settings = await readOpenScoutSettings({ currentDirectory }).catch(() => null);
|
|
21671
|
+
const configuredContextRoot = settings?.discovery.contextRoot ?? null;
|
|
21672
|
+
const discoveryStart = configuredContextRoot ?? currentDirectory;
|
|
21673
|
+
const projectRoot = await findNearestProjectRoot(discoveryStart).catch(() => null);
|
|
21674
|
+
const hasProjectConfig = projectRoot !== null;
|
|
21675
|
+
const userName = loadUserConfig().name?.trim() ?? "";
|
|
21676
|
+
return c.json({
|
|
21677
|
+
hasLocalConfig,
|
|
21678
|
+
hasProjectConfig,
|
|
21679
|
+
hasOperatorName: userName.length > 0,
|
|
21680
|
+
localConfigPath: localConfigPath(),
|
|
21681
|
+
localConfig: hasLocalConfig ? loadLocalConfig() : null,
|
|
21682
|
+
projectRoot,
|
|
21683
|
+
currentDirectory,
|
|
21684
|
+
contextRoot: configuredContextRoot,
|
|
21685
|
+
operatorName: userName || null,
|
|
21686
|
+
operatorNameSuggestion: resolveOperatorName()
|
|
21687
|
+
});
|
|
21688
|
+
});
|
|
21689
|
+
app.delete("/api/onboarding/state", (c) => {
|
|
21690
|
+
try {
|
|
21691
|
+
rmSync3(localConfigPath(), { force: true });
|
|
21692
|
+
} catch {}
|
|
21693
|
+
return c.json({ ok: true, localConfigPath: localConfigPath() });
|
|
21694
|
+
});
|
|
21695
|
+
app.post("/api/onboarding/project", async (c) => {
|
|
21696
|
+
const body = await c.req.json().catch(() => ({}));
|
|
21697
|
+
const contextRoot = body.contextRoot?.trim();
|
|
21698
|
+
if (!contextRoot) {
|
|
21699
|
+
return c.json({ error: "contextRoot is required" }, 400);
|
|
21700
|
+
}
|
|
21701
|
+
const sourceRoots = (body.sourceRoots ?? []).map((entry) => entry?.trim()).filter((entry) => Boolean(entry && entry.length > 0));
|
|
21702
|
+
const harness = body.defaultHarness === "codex" ? "codex" : "claude";
|
|
21703
|
+
try {
|
|
21704
|
+
await writeOpenScoutSettings({
|
|
21705
|
+
discovery: {
|
|
21706
|
+
contextRoot,
|
|
21707
|
+
workspaceRoots: sourceRoots
|
|
21708
|
+
},
|
|
21709
|
+
agents: { defaultHarness: harness }
|
|
21710
|
+
});
|
|
21711
|
+
const result = await initializeOpenScoutSetup({
|
|
21712
|
+
currentDirectory: contextRoot
|
|
21713
|
+
});
|
|
21714
|
+
return c.json({
|
|
21715
|
+
ok: true,
|
|
21716
|
+
projectConfigPath: result.currentProjectConfigPath
|
|
21717
|
+
});
|
|
21718
|
+
} catch (err) {
|
|
21719
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21720
|
+
console.error("[onboarding/project]", message);
|
|
21721
|
+
return c.json({ error: message }, 500);
|
|
21722
|
+
}
|
|
21723
|
+
});
|
|
21724
|
+
app.post("/api/onboarding/init", async (c) => {
|
|
21725
|
+
const body = await c.req.json().catch(() => ({}));
|
|
21726
|
+
writeLocalConfig({
|
|
21727
|
+
version: 1,
|
|
21728
|
+
host: body.host ?? DEFAULT_LOCAL_CONFIG.host,
|
|
21729
|
+
ports: {
|
|
21730
|
+
broker: body.ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker,
|
|
21731
|
+
web: body.ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web,
|
|
21732
|
+
pairing: body.ports?.pairing ?? DEFAULT_LOCAL_CONFIG.ports.pairing
|
|
21733
|
+
}
|
|
21734
|
+
});
|
|
21735
|
+
return c.json({
|
|
21736
|
+
ok: true,
|
|
21737
|
+
localConfig: loadLocalConfig(),
|
|
21738
|
+
localConfigPath: localConfigPath()
|
|
21739
|
+
});
|
|
21740
|
+
});
|
|
21741
|
+
app.post("/api/user", async (c) => {
|
|
21742
|
+
const body = await c.req.json();
|
|
21743
|
+
const config = loadUserConfig();
|
|
21744
|
+
const stringFields = [
|
|
21745
|
+
"name",
|
|
21746
|
+
"handle",
|
|
21747
|
+
"pronouns",
|
|
21748
|
+
"bio",
|
|
21749
|
+
"timezone",
|
|
21750
|
+
"workingHours",
|
|
21751
|
+
"interruptThreshold",
|
|
21752
|
+
"channel",
|
|
21753
|
+
"verbosity",
|
|
21754
|
+
"tone",
|
|
21755
|
+
"quietHours"
|
|
21756
|
+
];
|
|
21757
|
+
for (const key of stringFields) {
|
|
21758
|
+
if (key in body) {
|
|
21759
|
+
const val = body[key];
|
|
21760
|
+
if (typeof val === "string" && val.trim()) {
|
|
21761
|
+
config[key] = val.trim();
|
|
21762
|
+
} else {
|
|
21763
|
+
delete config[key];
|
|
21764
|
+
}
|
|
21765
|
+
}
|
|
21766
|
+
}
|
|
21767
|
+
if ("hue" in body && typeof body.hue === "number") {
|
|
21768
|
+
config.hue = body.hue;
|
|
21769
|
+
}
|
|
21770
|
+
if ("batchWindow" in body && typeof body.batchWindow === "number") {
|
|
21771
|
+
config.batchWindow = body.batchWindow;
|
|
21772
|
+
}
|
|
21773
|
+
saveUserConfig(config);
|
|
21774
|
+
return c.json({
|
|
21775
|
+
name: resolveOperatorName(),
|
|
21776
|
+
handle: config.handle ?? "",
|
|
21777
|
+
pronouns: config.pronouns ?? "",
|
|
21778
|
+
hue: config.hue ?? 195,
|
|
21779
|
+
bio: config.bio ?? "",
|
|
21780
|
+
timezone: config.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
21781
|
+
workingHours: config.workingHours ?? "08:00 \u2013 18:00",
|
|
21782
|
+
interruptThreshold: config.interruptThreshold ?? "blocking-only",
|
|
21783
|
+
batchWindow: config.batchWindow ?? 15,
|
|
21784
|
+
channel: config.channel ?? "here+mobile",
|
|
21785
|
+
verbosity: config.verbosity ?? "terse",
|
|
21786
|
+
tone: config.tone ?? "direct",
|
|
21787
|
+
quietHours: config.quietHours ?? "22:00 \u2013 07:00"
|
|
21788
|
+
});
|
|
21789
|
+
});
|
|
21790
|
+
app.post("/api/terminal/run", async (c) => {
|
|
21791
|
+
const body = await c.req.json();
|
|
21792
|
+
if (!body.command)
|
|
21793
|
+
return c.json({ error: "missing command" }, 400);
|
|
21794
|
+
queueTerminalCommand(body.command);
|
|
21795
|
+
return c.json({ ok: true });
|
|
21796
|
+
});
|
|
21797
|
+
app.post("/api/agents/:agentId/interrupt", async (c) => {
|
|
21798
|
+
const agentId = c.req.param("agentId");
|
|
21799
|
+
const { interruptLocalAgent: interruptLocalAgent2 } = await init_local_agents().then(() => exports_local_agents);
|
|
21800
|
+
const result = await interruptLocalAgent2(agentId);
|
|
21801
|
+
if (!result.ok)
|
|
21802
|
+
return c.json({ error: "Agent not found or not interruptible" }, 404);
|
|
21803
|
+
return c.json({ ok: true });
|
|
21804
|
+
});
|
|
21805
|
+
app.post("/api/send", async (c) => {
|
|
21806
|
+
const { body, conversationId } = await c.req.json();
|
|
21807
|
+
if (!body?.trim()) {
|
|
21808
|
+
return c.json({ error: "body is required" }, 400);
|
|
21500
21809
|
}
|
|
21501
|
-
|
|
21502
|
-
|
|
21810
|
+
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21811
|
+
if (directAgentId) {
|
|
21812
|
+
const result2 = await sendScoutDirectMessage({
|
|
21813
|
+
agentId: directAgentId,
|
|
21814
|
+
body: body.trim(),
|
|
21815
|
+
currentDirectory,
|
|
21816
|
+
source: "scout-web"
|
|
21817
|
+
});
|
|
21818
|
+
return c.json(result2);
|
|
21819
|
+
}
|
|
21820
|
+
const result = await sendScoutMessage({
|
|
21821
|
+
senderId,
|
|
21822
|
+
body: body.trim(),
|
|
21823
|
+
currentDirectory
|
|
21824
|
+
});
|
|
21825
|
+
if (!result.usedBroker) {
|
|
21826
|
+
return c.json({ error: "broker unreachable" }, 502);
|
|
21503
21827
|
}
|
|
21828
|
+
return c.json(result);
|
|
21504
21829
|
});
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
const crashed = exitCode !== 0 && uptime < 5000;
|
|
21510
|
-
let reason;
|
|
21511
|
-
if (crashed) {
|
|
21512
|
-
const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
|
|
21513
|
-
const lines = clean.split(`
|
|
21514
|
-
`).filter((l) => l.trim()).slice(-5);
|
|
21515
|
-
reason = lines.join(`
|
|
21516
|
-
`) || `Process exited with code ${exitCode}`;
|
|
21517
|
-
console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
|
|
21830
|
+
app.post("/api/ask", async (c) => {
|
|
21831
|
+
const { body, conversationId } = await c.req.json();
|
|
21832
|
+
if (!body?.trim()) {
|
|
21833
|
+
return c.json({ error: "body is required" }, 400);
|
|
21518
21834
|
}
|
|
21519
|
-
|
|
21520
|
-
|
|
21835
|
+
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21836
|
+
if (!directAgentId) {
|
|
21837
|
+
return c.json({
|
|
21838
|
+
error: "ask is only available in a direct conversation with one agent"
|
|
21839
|
+
}, 400);
|
|
21521
21840
|
}
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
|
|
21527
|
-
}
|
|
21528
|
-
|
|
21529
|
-
|
|
21530
|
-
clearTimeout(session.reapTimer);
|
|
21531
|
-
session.reapTimer = null;
|
|
21532
|
-
}
|
|
21533
|
-
session.ws = ws;
|
|
21534
|
-
if (cols && rows) {
|
|
21535
|
-
const c = Math.max(cols, 20);
|
|
21536
|
-
const r = Math.max(rows, 4);
|
|
21537
|
-
if (c !== session.cols || r !== session.rows) {
|
|
21538
|
-
session.pty.resize(c, r);
|
|
21539
|
-
session.cols = c;
|
|
21540
|
-
session.rows = r;
|
|
21841
|
+
const result = await askScoutQuestion({
|
|
21842
|
+
senderId,
|
|
21843
|
+
targetLabel: directAgentId,
|
|
21844
|
+
body: body.trim(),
|
|
21845
|
+
currentDirectory
|
|
21846
|
+
});
|
|
21847
|
+
if (!result.usedBroker) {
|
|
21848
|
+
return c.json({ error: "broker unreachable" }, 502);
|
|
21541
21849
|
}
|
|
21542
|
-
|
|
21543
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
}
|
|
21548
|
-
console.log(`[relay] Session ${session.id} reconnected`);
|
|
21549
|
-
}
|
|
21550
|
-
function detachSession(session) {
|
|
21551
|
-
session.ws = null;
|
|
21552
|
-
if (session.exited) {
|
|
21553
|
-
scheduleReap(session, 5000);
|
|
21554
|
-
} else {
|
|
21555
|
-
scheduleReap(session, session.orphanTTL);
|
|
21556
|
-
console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
|
|
21557
|
-
}
|
|
21558
|
-
}
|
|
21559
|
-
function scheduleReap(session, delay) {
|
|
21560
|
-
if (session.reapTimer)
|
|
21561
|
-
clearTimeout(session.reapTimer);
|
|
21562
|
-
session.reapTimer = setTimeout(() => {
|
|
21563
|
-
if (!session.ws) {
|
|
21564
|
-
destroy(session.id);
|
|
21850
|
+
if (result.unresolvedTarget) {
|
|
21851
|
+
return c.json({
|
|
21852
|
+
error: `could not route ask to ${result.unresolvedTarget}`,
|
|
21853
|
+
targetDiagnostic: result.targetDiagnostic ?? null
|
|
21854
|
+
}, 409);
|
|
21565
21855
|
}
|
|
21566
|
-
|
|
21567
|
-
}
|
|
21568
|
-
|
|
21569
|
-
|
|
21570
|
-
|
|
21571
|
-
|
|
21572
|
-
|
|
21573
|
-
|
|
21574
|
-
|
|
21575
|
-
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
if (session.backend === "tmux") {
|
|
21579
|
-
console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
|
|
21580
|
-
} else {
|
|
21581
|
-
console.log(`[relay] Session ${sessionId} destroyed`);
|
|
21582
|
-
}
|
|
21583
|
-
}
|
|
21584
|
-
|
|
21585
|
-
// packages/web/server/relay.ts
|
|
21586
|
-
import { mkdir as mkdir7 } from "fs/promises";
|
|
21587
|
-
import { join as join20 } from "path";
|
|
21588
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
21589
|
-
var UPLOAD_DIR = "/tmp/scout-uploads";
|
|
21590
|
-
async function handleRelayUpload(req) {
|
|
21591
|
-
const { name, data } = await req.json();
|
|
21592
|
-
if (!name || !data) {
|
|
21593
|
-
return Response.json({ error: "Missing name or data" }, { status: 400 });
|
|
21594
|
-
}
|
|
21595
|
-
await mkdir7(UPLOAD_DIR, { recursive: true });
|
|
21596
|
-
const filename = `${randomUUID3()}-${name}`;
|
|
21597
|
-
const filepath = join20(UPLOAD_DIR, filename);
|
|
21598
|
-
await Bun.write(filepath, Buffer.from(data, "base64"));
|
|
21599
|
-
return Response.json({ path: filepath });
|
|
21600
|
-
}
|
|
21601
|
-
function asRelay(ws) {
|
|
21602
|
-
return ws;
|
|
21603
|
-
}
|
|
21604
|
-
function parseMessage(raw2) {
|
|
21605
|
-
try {
|
|
21606
|
-
const msg = JSON.parse(raw2);
|
|
21607
|
-
if (typeof msg.type === "string")
|
|
21608
|
-
return msg;
|
|
21609
|
-
} catch {}
|
|
21610
|
-
return null;
|
|
21611
|
-
}
|
|
21612
|
-
var relayWebSocket = {
|
|
21613
|
-
open(_ws) {},
|
|
21614
|
-
message(ws, raw2) {
|
|
21615
|
-
const msg = parseMessage(typeof raw2 === "string" ? raw2 : raw2.toString());
|
|
21616
|
-
if (!msg)
|
|
21617
|
-
return;
|
|
21618
|
-
const rws = asRelay(ws);
|
|
21619
|
-
switch (msg.type) {
|
|
21620
|
-
case "session:init": {
|
|
21621
|
-
if (ws.data.sessionId) {
|
|
21622
|
-
const prev = sessions3.get(ws.data.sessionId);
|
|
21623
|
-
if (prev)
|
|
21624
|
-
detachSession(prev);
|
|
21625
|
-
}
|
|
21626
|
-
const session = createSession(rws, msg);
|
|
21627
|
-
if (!session)
|
|
21628
|
-
break;
|
|
21629
|
-
ws.data.sessionId = session.id;
|
|
21630
|
-
send(rws, { type: "session:ready", sessionId: session.id });
|
|
21631
|
-
break;
|
|
21632
|
-
}
|
|
21633
|
-
case "session:reconnect": {
|
|
21634
|
-
const existing = sessions3.get(msg.sessionId);
|
|
21635
|
-
if (existing && !existing.exited) {
|
|
21636
|
-
if (ws.data.sessionId && ws.data.sessionId !== msg.sessionId) {
|
|
21637
|
-
const prev = sessions3.get(ws.data.sessionId);
|
|
21638
|
-
if (prev)
|
|
21639
|
-
detachSession(prev);
|
|
21640
|
-
}
|
|
21641
|
-
if (existing.ws && existing.ws !== rws) {
|
|
21642
|
-
send(existing.ws, { type: "session:detached" });
|
|
21643
|
-
}
|
|
21644
|
-
ws.data.sessionId = existing.id;
|
|
21645
|
-
attachSession(existing, rws, msg.cols, msg.rows);
|
|
21646
|
-
send(rws, {
|
|
21647
|
-
type: "session:ready",
|
|
21648
|
-
sessionId: existing.id,
|
|
21649
|
-
reconnected: true
|
|
21650
|
-
});
|
|
21651
|
-
} else {
|
|
21652
|
-
send(rws, { type: "session:expired", sessionId: msg.sessionId });
|
|
21653
|
-
}
|
|
21654
|
-
break;
|
|
21655
|
-
}
|
|
21656
|
-
case "terminal:input": {
|
|
21657
|
-
if (!ws.data.sessionId)
|
|
21658
|
-
return;
|
|
21659
|
-
const session = sessions3.get(ws.data.sessionId);
|
|
21660
|
-
if (session && !session.exited) {
|
|
21661
|
-
session.pty.write(msg.data);
|
|
21662
|
-
}
|
|
21663
|
-
break;
|
|
21664
|
-
}
|
|
21665
|
-
case "terminal:resize": {
|
|
21666
|
-
if (!ws.data.sessionId)
|
|
21667
|
-
return;
|
|
21668
|
-
const session = sessions3.get(ws.data.sessionId);
|
|
21669
|
-
if (session && !session.exited) {
|
|
21670
|
-
const cols = Math.max(msg.cols || 80, 20);
|
|
21671
|
-
const rows = Math.max(msg.rows || 24, 4);
|
|
21672
|
-
session.pty.resize(cols, rows);
|
|
21673
|
-
session.cols = cols;
|
|
21674
|
-
session.rows = rows;
|
|
21675
|
-
}
|
|
21676
|
-
break;
|
|
21677
|
-
}
|
|
21856
|
+
return c.json(result);
|
|
21857
|
+
});
|
|
21858
|
+
app.get("/api/events", async (c) => {
|
|
21859
|
+
const brokerHost = process.env.OPENSCOUT_BROKER_HOST ?? "127.0.0.1";
|
|
21860
|
+
const brokerPort = process.env.OPENSCOUT_BROKER_PORT ?? "65535";
|
|
21861
|
+
const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? `http://${brokerHost}:${brokerPort}`;
|
|
21862
|
+
try {
|
|
21863
|
+
return await relayEventStream(`${brokerUrl}/v1/events/stream`, {
|
|
21864
|
+
signal: c.req.raw.signal
|
|
21865
|
+
});
|
|
21866
|
+
} catch {
|
|
21867
|
+
return c.text("Broker unreachable", 502);
|
|
21678
21868
|
}
|
|
21679
|
-
}
|
|
21680
|
-
|
|
21681
|
-
|
|
21682
|
-
|
|
21683
|
-
|
|
21684
|
-
|
|
21685
|
-
|
|
21869
|
+
});
|
|
21870
|
+
await registerScoutWebAssets(app, {
|
|
21871
|
+
assetMode: options.assetMode,
|
|
21872
|
+
staticRoot: resolveStaticRoot(options.staticRoot),
|
|
21873
|
+
viteDevUrl: options.viteDevUrl,
|
|
21874
|
+
defaultViteUrl: "http://127.0.0.1:5180"
|
|
21875
|
+
});
|
|
21876
|
+
const warmupCaches = () => Promise.allSettled([
|
|
21877
|
+
shellStateCache.refresh(),
|
|
21878
|
+
loadPairingState(currentDirectory, true)
|
|
21879
|
+
]).then((results) => {
|
|
21880
|
+
for (const result of results) {
|
|
21881
|
+
if (result.status === "rejected") {
|
|
21882
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
21883
|
+
console.error("[openscout-web api] initial cache warmup failed:", message);
|
|
21884
|
+
}
|
|
21686
21885
|
}
|
|
21687
|
-
}
|
|
21688
|
-
};
|
|
21689
|
-
function destroyAllRelaySessions() {
|
|
21690
|
-
for (const [id] of sessions3)
|
|
21691
|
-
destroy(id);
|
|
21886
|
+
});
|
|
21887
|
+
return { app, warmupCaches };
|
|
21692
21888
|
}
|
|
21693
21889
|
|
|
21694
21890
|
// packages/web/server/index.ts
|
|
@@ -21702,11 +21898,11 @@ function resolveStaticRoot2() {
|
|
|
21702
21898
|
}
|
|
21703
21899
|
const selfDir = dirname13(fileURLToPath8(import.meta.url));
|
|
21704
21900
|
const siblingClientRoot = join21(selfDir, "client");
|
|
21705
|
-
if (
|
|
21901
|
+
if (existsSync15(join21(siblingClientRoot, "index.html"))) {
|
|
21706
21902
|
return siblingClientRoot;
|
|
21707
21903
|
}
|
|
21708
21904
|
const sourceDistClientRoot = resolve10(selfDir, "../dist/client");
|
|
21709
|
-
if (
|
|
21905
|
+
if (existsSync15(join21(sourceDistClientRoot, "index.html"))) {
|
|
21710
21906
|
return sourceDistClientRoot;
|
|
21711
21907
|
}
|
|
21712
21908
|
return;
|