@openscout/scout 0.2.56 → 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-Cs--6ecm.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 +3 -3
- package/dist/main.mjs +295 -131
- package/dist/pair-supervisor.mjs +159 -42
- package/dist/scout-control-plane-web.mjs +1123 -855
- package/package.json +2 -2
- package/dist/client/assets/index-DvJjuWtM.js +0 -146
- package/dist/client/assets/index-MaClzcbW.css +0 -1
|
@@ -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
|
|
|
@@ -14987,94 +15116,6 @@ async function controlScoutWebPairingService(action, currentDirectory) {
|
|
|
14987
15116
|
});
|
|
14988
15117
|
}
|
|
14989
15118
|
|
|
14990
|
-
// node_modules/.bun/hono@4.12.10/node_modules/hono/dist/middleware/cors/index.js
|
|
14991
|
-
var cors = (options) => {
|
|
14992
|
-
const defaults = {
|
|
14993
|
-
origin: "*",
|
|
14994
|
-
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
14995
|
-
allowHeaders: [],
|
|
14996
|
-
exposeHeaders: []
|
|
14997
|
-
};
|
|
14998
|
-
const opts = {
|
|
14999
|
-
...defaults,
|
|
15000
|
-
...options
|
|
15001
|
-
};
|
|
15002
|
-
const findAllowOrigin = ((optsOrigin) => {
|
|
15003
|
-
if (typeof optsOrigin === "string") {
|
|
15004
|
-
if (optsOrigin === "*") {
|
|
15005
|
-
if (opts.credentials) {
|
|
15006
|
-
return (origin) => origin || null;
|
|
15007
|
-
}
|
|
15008
|
-
return () => optsOrigin;
|
|
15009
|
-
} else {
|
|
15010
|
-
return (origin) => optsOrigin === origin ? origin : null;
|
|
15011
|
-
}
|
|
15012
|
-
} else if (typeof optsOrigin === "function") {
|
|
15013
|
-
return optsOrigin;
|
|
15014
|
-
} else {
|
|
15015
|
-
return (origin) => optsOrigin.includes(origin) ? origin : null;
|
|
15016
|
-
}
|
|
15017
|
-
})(opts.origin);
|
|
15018
|
-
const findAllowMethods = ((optsAllowMethods) => {
|
|
15019
|
-
if (typeof optsAllowMethods === "function") {
|
|
15020
|
-
return optsAllowMethods;
|
|
15021
|
-
} else if (Array.isArray(optsAllowMethods)) {
|
|
15022
|
-
return () => optsAllowMethods;
|
|
15023
|
-
} else {
|
|
15024
|
-
return () => [];
|
|
15025
|
-
}
|
|
15026
|
-
})(opts.allowMethods);
|
|
15027
|
-
return async function cors2(c, next) {
|
|
15028
|
-
function set(key, value) {
|
|
15029
|
-
c.res.headers.set(key, value);
|
|
15030
|
-
}
|
|
15031
|
-
const allowOrigin = await findAllowOrigin(c.req.header("origin") || "", c);
|
|
15032
|
-
if (allowOrigin) {
|
|
15033
|
-
set("Access-Control-Allow-Origin", allowOrigin);
|
|
15034
|
-
}
|
|
15035
|
-
if (opts.credentials) {
|
|
15036
|
-
set("Access-Control-Allow-Credentials", "true");
|
|
15037
|
-
}
|
|
15038
|
-
if (opts.exposeHeaders?.length) {
|
|
15039
|
-
set("Access-Control-Expose-Headers", opts.exposeHeaders.join(","));
|
|
15040
|
-
}
|
|
15041
|
-
if (c.req.method === "OPTIONS") {
|
|
15042
|
-
if (opts.origin !== "*" || opts.credentials) {
|
|
15043
|
-
set("Vary", "Origin");
|
|
15044
|
-
}
|
|
15045
|
-
if (opts.maxAge != null) {
|
|
15046
|
-
set("Access-Control-Max-Age", opts.maxAge.toString());
|
|
15047
|
-
}
|
|
15048
|
-
const allowMethods = await findAllowMethods(c.req.header("origin") || "", c);
|
|
15049
|
-
if (allowMethods.length) {
|
|
15050
|
-
set("Access-Control-Allow-Methods", allowMethods.join(","));
|
|
15051
|
-
}
|
|
15052
|
-
let headers = opts.allowHeaders;
|
|
15053
|
-
if (!headers?.length) {
|
|
15054
|
-
const requestHeaders = c.req.header("Access-Control-Request-Headers");
|
|
15055
|
-
if (requestHeaders) {
|
|
15056
|
-
headers = requestHeaders.split(/\s*,\s*/);
|
|
15057
|
-
}
|
|
15058
|
-
}
|
|
15059
|
-
if (headers?.length) {
|
|
15060
|
-
set("Access-Control-Allow-Headers", headers.join(","));
|
|
15061
|
-
c.res.headers.append("Vary", "Access-Control-Request-Headers");
|
|
15062
|
-
}
|
|
15063
|
-
c.res.headers.delete("Content-Length");
|
|
15064
|
-
c.res.headers.delete("Content-Type");
|
|
15065
|
-
return new Response(null, {
|
|
15066
|
-
headers: c.res.headers,
|
|
15067
|
-
status: 204,
|
|
15068
|
-
statusText: "No Content"
|
|
15069
|
-
});
|
|
15070
|
-
}
|
|
15071
|
-
await next();
|
|
15072
|
-
if (opts.origin !== "*" || opts.credentials) {
|
|
15073
|
-
c.header("Vary", "Origin", { append: true });
|
|
15074
|
-
}
|
|
15075
|
-
};
|
|
15076
|
-
};
|
|
15077
|
-
|
|
15078
15119
|
// node_modules/.bun/hono@4.12.10/node_modules/hono/dist/adapter/bun/serve-static.js
|
|
15079
15120
|
import { stat as stat3 } from "fs/promises";
|
|
15080
15121
|
import { join as join10 } from "path";
|
|
@@ -15347,6 +15388,40 @@ var upgradeWebSocket = defineWebSocketHelper((c, events2) => {
|
|
|
15347
15388
|
});
|
|
15348
15389
|
|
|
15349
15390
|
// packages/web/server/server-core.ts
|
|
15391
|
+
var LOOPBACK_IPV4_HOST_PATTERN = /^127(?:\.\d{1,3}){3}$/;
|
|
15392
|
+
function normalizeHostname(hostname2) {
|
|
15393
|
+
const trimmed = hostname2.trim().toLowerCase();
|
|
15394
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
15395
|
+
return trimmed.slice(1, -1);
|
|
15396
|
+
}
|
|
15397
|
+
return trimmed;
|
|
15398
|
+
}
|
|
15399
|
+
function isTrustedLoopbackHostname(hostname2) {
|
|
15400
|
+
const normalized = normalizeHostname(hostname2);
|
|
15401
|
+
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || LOOPBACK_IPV4_HOST_PATTERN.test(normalized);
|
|
15402
|
+
}
|
|
15403
|
+
function isTrustedApiRequest(c) {
|
|
15404
|
+
const requestUrl = new URL(c.req.url);
|
|
15405
|
+
if (!isTrustedLoopbackHostname(requestUrl.hostname)) {
|
|
15406
|
+
return false;
|
|
15407
|
+
}
|
|
15408
|
+
const origin = c.req.header("origin");
|
|
15409
|
+
if (origin) {
|
|
15410
|
+
try {
|
|
15411
|
+
const originUrl = new URL(origin);
|
|
15412
|
+
if (!isTrustedLoopbackHostname(originUrl.hostname) || originUrl.origin !== requestUrl.origin) {
|
|
15413
|
+
return false;
|
|
15414
|
+
}
|
|
15415
|
+
} catch {
|
|
15416
|
+
return false;
|
|
15417
|
+
}
|
|
15418
|
+
}
|
|
15419
|
+
const fetchSite = c.req.header("sec-fetch-site")?.trim().toLowerCase();
|
|
15420
|
+
if (fetchSite && fetchSite !== "same-origin" && fetchSite !== "same-site" && fetchSite !== "none") {
|
|
15421
|
+
return false;
|
|
15422
|
+
}
|
|
15423
|
+
return true;
|
|
15424
|
+
}
|
|
15350
15425
|
function createCachedSnapshot(load, ttlMs) {
|
|
15351
15426
|
let inflight = null;
|
|
15352
15427
|
let cached = null;
|
|
@@ -15388,8 +15463,12 @@ function createCachedSnapshot(load, ttlMs) {
|
|
|
15388
15463
|
};
|
|
15389
15464
|
}
|
|
15390
15465
|
function installScoutApiMiddleware(app, label = "api") {
|
|
15391
|
-
app.use("/*", cors());
|
|
15392
15466
|
app.use("/api/*", async (c, next) => {
|
|
15467
|
+
if (!isTrustedApiRequest(c)) {
|
|
15468
|
+
return c.json({ error: "forbidden" }, 403);
|
|
15469
|
+
}
|
|
15470
|
+
c.header("Cross-Origin-Resource-Policy", "same-origin");
|
|
15471
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
15393
15472
|
try {
|
|
15394
15473
|
await next();
|
|
15395
15474
|
} catch (error) {
|
|
@@ -15537,19 +15616,15 @@ function resolveDbPath() {
|
|
|
15537
15616
|
return join12(controlHome, "control-plane.sqlite");
|
|
15538
15617
|
}
|
|
15539
15618
|
var _db = null;
|
|
15540
|
-
var
|
|
15541
|
-
|
|
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
|
+
}
|
|
15542
15624
|
function db() {
|
|
15543
|
-
const now = Date.now();
|
|
15544
|
-
if (_db && now - _dbOpenedAt > DB_REOPEN_MS) {
|
|
15545
|
-
_db.close();
|
|
15546
|
-
_db = null;
|
|
15547
|
-
}
|
|
15548
15625
|
if (!_db) {
|
|
15549
15626
|
_db = new Database(resolveDbPath(), { readonly: true });
|
|
15550
|
-
_db
|
|
15551
|
-
_db.exec("PRAGMA journal_mode = WAL");
|
|
15552
|
-
_dbOpenedAt = now;
|
|
15627
|
+
configureReadonlyDb(_db);
|
|
15553
15628
|
}
|
|
15554
15629
|
return _db;
|
|
15555
15630
|
}
|
|
@@ -15586,7 +15661,7 @@ function resolveHarnessSessionId(transport, endpointSessionId, metadata) {
|
|
|
15586
15661
|
return metadataString(metadata, "threadId") ?? endpointSessionId;
|
|
15587
15662
|
}
|
|
15588
15663
|
if (transport === "claude_stream_json") {
|
|
15589
|
-
return
|
|
15664
|
+
return metadataString(metadata, "externalSessionId") ?? endpointSessionId;
|
|
15590
15665
|
}
|
|
15591
15666
|
return null;
|
|
15592
15667
|
}
|
|
@@ -15612,6 +15687,22 @@ function coerceNumber(value) {
|
|
|
15612
15687
|
}
|
|
15613
15688
|
return null;
|
|
15614
15689
|
}
|
|
15690
|
+
function sqlJoinClauses(clauses, operator = "AND") {
|
|
15691
|
+
return clauses.filter((clause) => Boolean(clause)).join(` ${operator} `);
|
|
15692
|
+
}
|
|
15693
|
+
function sqlWhereClause(clauses, operator = "AND") {
|
|
15694
|
+
const joined = sqlJoinClauses(clauses, operator);
|
|
15695
|
+
return joined ? `WHERE ${joined}` : "";
|
|
15696
|
+
}
|
|
15697
|
+
function sqlPlaceholders(count) {
|
|
15698
|
+
return Array.from({ length: count }, () => "?").join(", ");
|
|
15699
|
+
}
|
|
15700
|
+
function sqlQuoteLiteral(value) {
|
|
15701
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
15702
|
+
}
|
|
15703
|
+
function sqlStringList(values) {
|
|
15704
|
+
return `(${values.map(sqlQuoteLiteral).join(",")})`;
|
|
15705
|
+
}
|
|
15615
15706
|
var LATEST_AGENT_ENDPOINT_JOIN = `LEFT JOIN agent_endpoints ep ON ep.id = (
|
|
15616
15707
|
SELECT ep2.id
|
|
15617
15708
|
FROM agent_endpoints ep2
|
|
@@ -15639,6 +15730,8 @@ function normalizeTimestampMs(value) {
|
|
|
15639
15730
|
}
|
|
15640
15731
|
return numeric < 1000000000000 ? numeric * 1000 : numeric;
|
|
15641
15732
|
}
|
|
15733
|
+
var ACTIVE_FLIGHT_STATES_SQL = sqlStringList(["running", "waking", "waiting", "queued"]);
|
|
15734
|
+
var ACTIVE_WORK_STATES_SQL = sqlStringList(["open", "working", "waiting", "review"]);
|
|
15642
15735
|
function isDuplicateActivityFeedItem(previous, next) {
|
|
15643
15736
|
if (!previous) {
|
|
15644
15737
|
return false;
|
|
@@ -15835,10 +15928,10 @@ function queryActivity(limit = 60) {
|
|
|
15835
15928
|
}
|
|
15836
15929
|
function queryRecentMessages(limit = 80, opts) {
|
|
15837
15930
|
const conversationIds = opts?.conversationId ? conversationIdAliases(opts.conversationId) : [];
|
|
15838
|
-
const where = [
|
|
15931
|
+
const where = sqlJoinClauses([
|
|
15839
15932
|
transientBrokerWorkingStatusPredicate("m"),
|
|
15840
|
-
conversationIds.length > 0 ? `m.conversation_id IN (${conversationIds.
|
|
15841
|
-
]
|
|
15933
|
+
conversationIds.length > 0 ? `m.conversation_id IN (${sqlPlaceholders(conversationIds.length)})` : null
|
|
15934
|
+
]);
|
|
15842
15935
|
const rows = db().prepare(`SELECT
|
|
15843
15936
|
m.id,
|
|
15844
15937
|
m.conversation_id,
|
|
@@ -15873,14 +15966,13 @@ function queryRecentMessages(limit = 80, opts) {
|
|
|
15873
15966
|
});
|
|
15874
15967
|
}
|
|
15875
15968
|
function queryFlights(opts) {
|
|
15876
|
-
const activeStates = "('running','waking','waiting','queued')";
|
|
15877
15969
|
const conversationIds = opts?.conversationId ? conversationIdAliases(opts.conversationId) : [];
|
|
15878
|
-
const where = [
|
|
15879
|
-
opts?.activeOnly ? `f.state IN ${
|
|
15970
|
+
const where = sqlJoinClauses([
|
|
15971
|
+
opts?.activeOnly ? `f.state IN ${ACTIVE_FLIGHT_STATES_SQL}` : null,
|
|
15880
15972
|
opts?.agentId ? `f.target_agent_id = ?` : null,
|
|
15881
|
-
conversationIds.length > 0 ? `inv.conversation_id IN (${conversationIds.
|
|
15973
|
+
conversationIds.length > 0 ? `inv.conversation_id IN (${sqlPlaceholders(conversationIds.length)})` : null,
|
|
15882
15974
|
opts?.collaborationRecordId ? `inv.collaboration_record_id = ?` : null
|
|
15883
|
-
]
|
|
15975
|
+
]);
|
|
15884
15976
|
const sql = `SELECT
|
|
15885
15977
|
f.id,
|
|
15886
15978
|
f.invocation_id,
|
|
@@ -15895,7 +15987,7 @@ function queryFlights(opts) {
|
|
|
15895
15987
|
FROM flights f
|
|
15896
15988
|
JOIN invocations inv ON inv.id = f.invocation_id
|
|
15897
15989
|
LEFT JOIN actors ac ON ac.id = f.target_agent_id
|
|
15898
|
-
${where
|
|
15990
|
+
${sqlWhereClause([where])}
|
|
15899
15991
|
ORDER BY f.started_at DESC NULLS LAST
|
|
15900
15992
|
LIMIT 100`;
|
|
15901
15993
|
const params = [];
|
|
@@ -15920,7 +16012,6 @@ function queryFlights(opts) {
|
|
|
15920
16012
|
}));
|
|
15921
16013
|
}
|
|
15922
16014
|
function queryWorkItemById(id) {
|
|
15923
|
-
const activeStates = "('open','working','waiting','review')";
|
|
15924
16015
|
const sql = `SELECT
|
|
15925
16016
|
cr.id,
|
|
15926
16017
|
cr.title,
|
|
@@ -15943,7 +16034,7 @@ function queryWorkItemById(id) {
|
|
|
15943
16034
|
FROM collaboration_records child
|
|
15944
16035
|
WHERE child.parent_id = cr.id
|
|
15945
16036
|
AND child.kind = 'work_item'
|
|
15946
|
-
AND child.state IN ${
|
|
16037
|
+
AND child.state IN ${ACTIVE_WORK_STATES_SQL}
|
|
15947
16038
|
) AS active_child_work_count,
|
|
15948
16039
|
(
|
|
15949
16040
|
SELECT COUNT(*)
|
|
@@ -16131,12 +16222,11 @@ function queryWorkTimeline(workId) {
|
|
|
16131
16222
|
return items.slice(0, 80);
|
|
16132
16223
|
}
|
|
16133
16224
|
function queryWorkItems(opts) {
|
|
16134
|
-
const
|
|
16135
|
-
const where = [
|
|
16225
|
+
const where = sqlJoinClauses([
|
|
16136
16226
|
"cr.kind = 'work_item'",
|
|
16137
|
-
opts?.activeOnly !== false ? `cr.state IN ${
|
|
16227
|
+
opts?.activeOnly !== false ? `cr.state IN ${ACTIVE_WORK_STATES_SQL}` : null,
|
|
16138
16228
|
opts?.agentId ? "(cr.owner_id = ? OR cr.next_move_owner_id = ?)" : null
|
|
16139
|
-
]
|
|
16229
|
+
]);
|
|
16140
16230
|
const sql = `SELECT
|
|
16141
16231
|
cr.id,
|
|
16142
16232
|
cr.title,
|
|
@@ -16156,7 +16246,7 @@ function queryWorkItems(opts) {
|
|
|
16156
16246
|
FROM collaboration_records child
|
|
16157
16247
|
WHERE child.parent_id = cr.id
|
|
16158
16248
|
AND child.kind = 'work_item'
|
|
16159
|
-
AND child.state IN ${
|
|
16249
|
+
AND child.state IN ${ACTIVE_WORK_STATES_SQL}
|
|
16160
16250
|
) AS active_child_work_count,
|
|
16161
16251
|
(
|
|
16162
16252
|
SELECT COUNT(*)
|
|
@@ -16234,7 +16324,7 @@ function queryWorkItems(opts) {
|
|
|
16234
16324
|
FROM collaboration_records cr
|
|
16235
16325
|
LEFT JOIN actors owner ON owner.id = cr.owner_id
|
|
16236
16326
|
LEFT JOIN actors next ON next.id = cr.next_move_owner_id
|
|
16237
|
-
${where
|
|
16327
|
+
${sqlWhereClause([where])}
|
|
16238
16328
|
ORDER BY sort_ts DESC, cr.updated_at DESC
|
|
16239
16329
|
LIMIT ?`;
|
|
16240
16330
|
const limit = opts?.limit ?? 50;
|
|
@@ -16565,7 +16655,7 @@ function queryFleetActivity(opts) {
|
|
|
16565
16655
|
ai.session_id
|
|
16566
16656
|
FROM activity_items ai
|
|
16567
16657
|
LEFT JOIN actors ac ON ac.id = ai.actor_id
|
|
16568
|
-
${filters
|
|
16658
|
+
${sqlWhereClause(filters, "OR")}
|
|
16569
16659
|
ORDER BY ai.ts DESC
|
|
16570
16660
|
LIMIT ?`;
|
|
16571
16661
|
const rows = db().prepare(sql).all(...params, opts?.limit ?? 80);
|
|
@@ -16592,7 +16682,7 @@ function fleetStatusLabel(status) {
|
|
|
16592
16682
|
}
|
|
16593
16683
|
}
|
|
16594
16684
|
function queryFleetAskRows(requesterIds, limit) {
|
|
16595
|
-
const requesterClause = requesterIds.
|
|
16685
|
+
const requesterClause = sqlPlaceholders(requesterIds.length);
|
|
16596
16686
|
return db().prepare(`SELECT
|
|
16597
16687
|
inv.id AS invocation_id,
|
|
16598
16688
|
inv.requester_id,
|
|
@@ -16689,7 +16779,7 @@ function projectFleetAsk(row, requesterIdSet) {
|
|
|
16689
16779
|
};
|
|
16690
16780
|
}
|
|
16691
16781
|
function queryFleetAttentionRows(requesterIds, limit) {
|
|
16692
|
-
const requesterClause = requesterIds.
|
|
16782
|
+
const requesterClause = sqlPlaceholders(requesterIds.length);
|
|
16693
16783
|
return db().prepare(`SELECT
|
|
16694
16784
|
cr.kind AS record_kind,
|
|
16695
16785
|
cr.id AS record_id,
|
|
@@ -16749,125 +16839,67 @@ function queryFleet(opts) {
|
|
|
16749
16839
|
activity
|
|
16750
16840
|
};
|
|
16751
16841
|
}
|
|
16752
|
-
var
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
const currentBucketStart2 = Math.floor(nowMs / bucketMs2) * bucketMs2;
|
|
16768
|
-
const alignedStart2 = currentBucketStart2 - (numBuckets - 1) * bucketMs2;
|
|
16769
|
-
if (_cachedTier === tier.label) {
|
|
16770
|
-
let sealedHits = 0;
|
|
16771
|
-
let filledSealed = 0;
|
|
16772
|
-
for (let i = 0;i < numBuckets - 1; i++) {
|
|
16773
|
-
const key = String(alignedStart2 + i * bucketMs2);
|
|
16774
|
-
if (_sealedCounts.has(key)) {
|
|
16775
|
-
sealedHits++;
|
|
16776
|
-
if (_sealedCounts.get(key) > 0)
|
|
16777
|
-
filledSealed++;
|
|
16778
|
-
}
|
|
16779
|
-
}
|
|
16780
|
-
if (sealedHits === numBuckets - 1 && filledSealed >= MIN_FILLED_BUCKETS) {
|
|
16781
|
-
chosenTier = tier;
|
|
16782
|
-
break;
|
|
16783
|
-
}
|
|
16784
|
-
}
|
|
16785
|
-
const rows2 = db().prepare(`SELECT ts FROM activity_items WHERE ts >= ? ORDER BY ts ASC`).all(alignedStart2);
|
|
16786
|
-
const counts2 = new Array(numBuckets).fill(0);
|
|
16787
|
-
for (const row of rows2) {
|
|
16788
|
-
const ms = row.ts < 1000000000000 ? row.ts * 1000 : row.ts;
|
|
16789
|
-
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)
|
|
16790
16857
|
continue;
|
|
16791
|
-
const
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
}
|
|
16795
|
-
const filled = counts2.filter((c) => c > 0).length;
|
|
16796
|
-
if (filled >= MIN_FILLED_BUCKETS) {
|
|
16797
|
-
if (_cachedTier !== tier.label) {
|
|
16798
|
-
_sealedCounts.clear();
|
|
16799
|
-
_cachedTier = tier.label;
|
|
16800
|
-
}
|
|
16801
|
-
for (let i = 0;i < numBuckets - 1; i++) {
|
|
16802
|
-
_sealedCounts.set(String(alignedStart2 + i * bucketMs2), counts2[i]);
|
|
16803
|
-
}
|
|
16804
|
-
const peak2 = Math.max(1, ...counts2);
|
|
16805
|
-
return {
|
|
16806
|
-
windowLabel: tier.label,
|
|
16807
|
-
buckets: counts2.map((count, i) => ({
|
|
16808
|
-
ts: Math.round(alignedStart2 + i * bucketMs2),
|
|
16809
|
-
count,
|
|
16810
|
-
value: count / peak2
|
|
16811
|
-
}))
|
|
16812
|
-
};
|
|
16858
|
+
const weight = weights[Math.abs(offset)] ?? 0;
|
|
16859
|
+
total += energy[nextIndex] * weight;
|
|
16860
|
+
weightTotal += weight;
|
|
16813
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`;
|
|
16869
|
+
}
|
|
16870
|
+
if (minutes % 60 === 0) {
|
|
16871
|
+
return `${minutes / 60}h buckets`;
|
|
16814
16872
|
}
|
|
16815
|
-
|
|
16873
|
+
return `${minutes}m buckets`;
|
|
16874
|
+
}
|
|
16875
|
+
function queryHeartrate(numBuckets = DEFAULT_HEARTRATE_BUCKETS, nowMs = Date.now()) {
|
|
16876
|
+
const bucketMs = HEARTRATE_WINDOW_MS / numBuckets;
|
|
16816
16877
|
const currentBucketStart = Math.floor(nowMs / bucketMs) * bucketMs;
|
|
16817
16878
|
const alignedStart = currentBucketStart - (numBuckets - 1) * bucketMs;
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
counts2[i] = _sealedCounts.get(key);
|
|
16825
|
-
} else {
|
|
16826
|
-
allSealed = false;
|
|
16827
|
-
break;
|
|
16828
|
-
}
|
|
16829
|
-
}
|
|
16830
|
-
if (allSealed) {
|
|
16831
|
-
const liveStart = alignedStart + (numBuckets - 1) * bucketMs;
|
|
16832
|
-
const row = db().prepare(`SELECT COUNT(*) AS c FROM activity_items WHERE ts >= ?`).get(liveStart);
|
|
16833
|
-
counts2[numBuckets - 1] = row.c;
|
|
16834
|
-
const peak2 = Math.max(1, ...counts2);
|
|
16835
|
-
return {
|
|
16836
|
-
windowLabel: chosenTier.label,
|
|
16837
|
-
buckets: counts2.map((count, i) => ({
|
|
16838
|
-
ts: Math.round(alignedStart + i * bucketMs),
|
|
16839
|
-
count,
|
|
16840
|
-
value: count / peak2
|
|
16841
|
-
}))
|
|
16842
|
-
};
|
|
16843
|
-
}
|
|
16844
|
-
}
|
|
16845
|
-
_sealedCounts.clear();
|
|
16846
|
-
_cachedTier = chosenTier.label;
|
|
16847
|
-
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);
|
|
16848
16885
|
const counts = new Array(numBuckets).fill(0);
|
|
16849
16886
|
for (const row of rows) {
|
|
16850
|
-
const ms = row.ts
|
|
16851
|
-
if (ms < alignedStart)
|
|
16887
|
+
const ms = normalizeActivityTimestampMs(row.ts);
|
|
16888
|
+
if (ms < alignedStart || ms > nowMs)
|
|
16852
16889
|
continue;
|
|
16853
16890
|
const idx = Math.min(numBuckets - 1, Math.floor((ms - alignedStart) / bucketMs));
|
|
16854
16891
|
if (idx >= 0)
|
|
16855
16892
|
counts[idx]++;
|
|
16856
16893
|
}
|
|
16857
|
-
|
|
16858
|
-
|
|
16859
|
-
}
|
|
16860
|
-
for (const key of _sealedCounts.keys()) {
|
|
16861
|
-
if (Number(key) < alignedStart)
|
|
16862
|
-
_sealedCounts.delete(key);
|
|
16863
|
-
}
|
|
16864
|
-
const peak = Math.max(1, ...counts);
|
|
16894
|
+
const smoothed = smoothHeartrateCounts(counts);
|
|
16895
|
+
const peak = Math.max(1, ...smoothed);
|
|
16865
16896
|
return {
|
|
16866
|
-
windowLabel:
|
|
16897
|
+
windowLabel: "trailing 7d",
|
|
16898
|
+
bucketLabel: formatHeartrateBucketLabel(bucketMs),
|
|
16867
16899
|
buckets: counts.map((count, i) => ({
|
|
16868
16900
|
ts: Math.round(alignedStart + i * bucketMs),
|
|
16869
16901
|
count,
|
|
16870
|
-
value:
|
|
16902
|
+
value: smoothed[i] / peak
|
|
16871
16903
|
}))
|
|
16872
16904
|
};
|
|
16873
16905
|
}
|
|
@@ -17796,7 +17828,7 @@ function whoEntryState(endpoints, registrationKind) {
|
|
|
17796
17828
|
|
|
17797
17829
|
// packages/web/server/core/observe/service.ts
|
|
17798
17830
|
init_src();
|
|
17799
|
-
import { existsSync as
|
|
17831
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
17800
17832
|
import { homedir as homedir12 } from "os";
|
|
17801
17833
|
import { join as join18, resolve as resolve8 } from "path";
|
|
17802
17834
|
|
|
@@ -19784,6 +19816,9 @@ function parseSelf(status) {
|
|
|
19784
19816
|
magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
|
|
19785
19817
|
};
|
|
19786
19818
|
}
|
|
19819
|
+
function isBackendRunning(status) {
|
|
19820
|
+
return (status.BackendState ?? "").trim().toLowerCase() === "running";
|
|
19821
|
+
}
|
|
19787
19822
|
async function readStatusJsonFromFile(filePath) {
|
|
19788
19823
|
const raw2 = await readFile7(filePath, "utf8");
|
|
19789
19824
|
return parseStatusJson(raw2);
|
|
@@ -19801,19 +19836,25 @@ async function readStatusJson() {
|
|
|
19801
19836
|
return null;
|
|
19802
19837
|
}
|
|
19803
19838
|
}
|
|
19804
|
-
async function
|
|
19805
|
-
const
|
|
19806
|
-
if (!
|
|
19807
|
-
return
|
|
19839
|
+
async function readTailscaleSelf() {
|
|
19840
|
+
const summary = await readTailscaleStatusSummary();
|
|
19841
|
+
if (!summary) {
|
|
19842
|
+
return null;
|
|
19808
19843
|
}
|
|
19809
|
-
return
|
|
19844
|
+
return summary.self;
|
|
19810
19845
|
}
|
|
19811
|
-
async function
|
|
19846
|
+
async function readTailscaleStatusSummary() {
|
|
19812
19847
|
const status = await readStatusJson();
|
|
19813
19848
|
if (!status) {
|
|
19814
19849
|
return null;
|
|
19815
19850
|
}
|
|
19816
|
-
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
|
+
};
|
|
19817
19858
|
}
|
|
19818
19859
|
|
|
19819
19860
|
// packages/runtime/src/index.ts
|
|
@@ -20071,6 +20112,9 @@ class ThreadEventPlane {
|
|
|
20071
20112
|
// packages/runtime/src/mobile-push.ts
|
|
20072
20113
|
init_support_paths();
|
|
20073
20114
|
// packages/web/server/core/observe/service.ts
|
|
20115
|
+
var HISTORY_SNAPSHOT_CACHE_LIMIT = 128;
|
|
20116
|
+
var historySnapshotCache = new Map;
|
|
20117
|
+
var OBSERVE_SUMMARY_TAIL_SIZE = 8;
|
|
20074
20118
|
function activeEndpoint(snapshot, agentId) {
|
|
20075
20119
|
const candidates = Object.values(snapshot.endpoints ?? {}).filter((endpoint) => endpoint.agentId === agentId);
|
|
20076
20120
|
const rank = (state) => {
|
|
@@ -20107,11 +20151,37 @@ function encodeClaudeProjectsSlug2(absolutePath) {
|
|
|
20107
20151
|
}
|
|
20108
20152
|
function resolveClaudeHistoryPath(cwd, sessionId) {
|
|
20109
20153
|
const normalizedCwd = expandHome(cwd)?.trim();
|
|
20154
|
+
if (!normalizedCwd) {
|
|
20155
|
+
return null;
|
|
20156
|
+
}
|
|
20157
|
+
const projectDir = join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd));
|
|
20110
20158
|
const normalizedSessionId = sessionId?.trim().replace(/\.jsonl$/u, "") || "";
|
|
20111
|
-
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 {
|
|
20112
20183
|
return null;
|
|
20113
20184
|
}
|
|
20114
|
-
return join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd), `${normalizedSessionId}.jsonl`);
|
|
20115
20185
|
}
|
|
20116
20186
|
function historyAdapterAlias(value) {
|
|
20117
20187
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -20175,20 +20245,43 @@ function readHistorySnapshot(candidate) {
|
|
|
20175
20245
|
if (!supportsHistorySessionSnapshotForPath(candidate.path, candidate.adapterType)) {
|
|
20176
20246
|
return null;
|
|
20177
20247
|
}
|
|
20178
|
-
if (!
|
|
20248
|
+
if (!existsSync12(candidate.path)) {
|
|
20179
20249
|
return null;
|
|
20180
20250
|
}
|
|
20181
20251
|
const stat5 = statSync4(candidate.path);
|
|
20252
|
+
const cached = historySnapshotCache.get(candidate.path);
|
|
20253
|
+
if (cached && cached.adapterType === candidate.adapterType && cached.mtimeMs === stat5.mtimeMs && cached.size === stat5.size) {
|
|
20254
|
+
return {
|
|
20255
|
+
historyPath: cached.historyPath,
|
|
20256
|
+
snapshot: cached.snapshot,
|
|
20257
|
+
timedEvents: cached.timedEvents
|
|
20258
|
+
};
|
|
20259
|
+
}
|
|
20182
20260
|
const replay = createHistorySessionSnapshot({
|
|
20183
20261
|
path: candidate.path,
|
|
20184
|
-
content:
|
|
20262
|
+
content: readFileSync8(candidate.path, "utf8"),
|
|
20185
20263
|
adapterType: candidate.adapterType,
|
|
20186
20264
|
baseTimestampMs: stat5.mtimeMs
|
|
20187
20265
|
});
|
|
20188
|
-
|
|
20266
|
+
const nextEntry = {
|
|
20189
20267
|
historyPath: candidate.path,
|
|
20190
20268
|
snapshot: replay.snapshot,
|
|
20191
|
-
timedEvents: normalizeTimedEvents(replay.events)
|
|
20269
|
+
timedEvents: normalizeTimedEvents(replay.events),
|
|
20270
|
+
adapterType: candidate.adapterType,
|
|
20271
|
+
mtimeMs: stat5.mtimeMs,
|
|
20272
|
+
size: stat5.size
|
|
20273
|
+
};
|
|
20274
|
+
historySnapshotCache.set(candidate.path, nextEntry);
|
|
20275
|
+
if (historySnapshotCache.size > HISTORY_SNAPSHOT_CACHE_LIMIT) {
|
|
20276
|
+
const oldestKey = historySnapshotCache.keys().next().value;
|
|
20277
|
+
if (typeof oldestKey === "string") {
|
|
20278
|
+
historySnapshotCache.delete(oldestKey);
|
|
20279
|
+
}
|
|
20280
|
+
}
|
|
20281
|
+
return {
|
|
20282
|
+
historyPath: nextEntry.historyPath,
|
|
20283
|
+
snapshot: nextEntry.snapshot,
|
|
20284
|
+
timedEvents: nextEntry.timedEvents
|
|
20192
20285
|
};
|
|
20193
20286
|
}
|
|
20194
20287
|
async function readLiveSnapshot(endpoint) {
|
|
@@ -20570,8 +20663,7 @@ function unavailableObserveData(agent) {
|
|
|
20570
20663
|
live: false
|
|
20571
20664
|
};
|
|
20572
20665
|
}
|
|
20573
|
-
async function resolveSnapshotSource(agent) {
|
|
20574
|
-
const broker2 = await loadScoutBrokerContext();
|
|
20666
|
+
async function resolveSnapshotSource(agent, broker2) {
|
|
20575
20667
|
const endpoint = broker2 ? activeEndpoint(broker2.snapshot, agent.id) : null;
|
|
20576
20668
|
const liveSnapshot = await readLiveSnapshot(endpoint);
|
|
20577
20669
|
const live = Boolean(liveSnapshot && (liveSnapshot.currentTurnId || liveSnapshot.session.status === "active"));
|
|
@@ -20615,15 +20707,11 @@ async function resolveSnapshotSource(agent) {
|
|
|
20615
20707
|
sessionId: null
|
|
20616
20708
|
};
|
|
20617
20709
|
}
|
|
20618
|
-
async function
|
|
20619
|
-
const
|
|
20620
|
-
if (!agent) {
|
|
20621
|
-
return null;
|
|
20622
|
-
}
|
|
20623
|
-
const source = await resolveSnapshotSource(agent);
|
|
20710
|
+
async function buildAgentObservePayload(agent, broker2) {
|
|
20711
|
+
const source = await resolveSnapshotSource(agent, broker2);
|
|
20624
20712
|
if (source.source === "unavailable") {
|
|
20625
20713
|
return {
|
|
20626
|
-
agentId,
|
|
20714
|
+
agentId: agent.id,
|
|
20627
20715
|
source: "unavailable",
|
|
20628
20716
|
fidelity: "synthetic",
|
|
20629
20717
|
historyPath: null,
|
|
@@ -20634,7 +20722,7 @@ async function loadAgentObservePayload(agentId) {
|
|
|
20634
20722
|
}
|
|
20635
20723
|
const fidelity = source.timedEvents.length > 0 ? "timestamped" : "synthetic";
|
|
20636
20724
|
return {
|
|
20637
|
-
agentId,
|
|
20725
|
+
agentId: agent.id,
|
|
20638
20726
|
source: source.source,
|
|
20639
20727
|
fidelity,
|
|
20640
20728
|
historyPath: source.historyPath,
|
|
@@ -20643,9 +20731,39 @@ async function loadAgentObservePayload(agentId) {
|
|
|
20643
20731
|
data: buildObserveDataFromSnapshot(source.snapshot, source.timedEvents, source.live)
|
|
20644
20732
|
};
|
|
20645
20733
|
}
|
|
20734
|
+
async function loadAgentObservePayloadsInternal(agentIds) {
|
|
20735
|
+
const agents = queryAgents(200);
|
|
20736
|
+
const filteredAgents = agentIds && agentIds.length > 0 ? agents.filter((agent) => agentIds.includes(agent.id)) : agents;
|
|
20737
|
+
if (filteredAgents.length === 0) {
|
|
20738
|
+
return [];
|
|
20739
|
+
}
|
|
20740
|
+
const broker2 = await loadScoutBrokerContext();
|
|
20741
|
+
return await Promise.all(filteredAgents.map((agent) => buildAgentObservePayload(agent, broker2)));
|
|
20742
|
+
}
|
|
20743
|
+
function summarizeAgentObservePayload(payload) {
|
|
20744
|
+
return {
|
|
20745
|
+
...payload,
|
|
20746
|
+
data: {
|
|
20747
|
+
...payload.data,
|
|
20748
|
+
events: payload.data.events.slice(-OBSERVE_SUMMARY_TAIL_SIZE),
|
|
20749
|
+
files: []
|
|
20750
|
+
}
|
|
20751
|
+
};
|
|
20752
|
+
}
|
|
20753
|
+
async function loadAgentObserveSummaries(agentIds) {
|
|
20754
|
+
const payloads = await loadAgentObservePayloadsInternal(agentIds);
|
|
20755
|
+
return payloads.map(summarizeAgentObservePayload);
|
|
20756
|
+
}
|
|
20757
|
+
async function loadAgentObservePayload(agentId) {
|
|
20758
|
+
const payloads = await loadAgentObservePayloadsInternal([agentId]);
|
|
20759
|
+
return payloads[0] ?? null;
|
|
20760
|
+
}
|
|
20646
20761
|
|
|
20647
20762
|
// packages/web/server/core/mesh/service.ts
|
|
20648
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);
|
|
20649
20767
|
function normalizeHost(host) {
|
|
20650
20768
|
return host.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
20651
20769
|
}
|
|
@@ -20676,9 +20794,13 @@ function stripTrailingDot(value) {
|
|
|
20676
20794
|
return trimmed.replace(/\.$/, "");
|
|
20677
20795
|
}
|
|
20678
20796
|
async function readTailscaleStatus() {
|
|
20679
|
-
const
|
|
20797
|
+
const summary = await readTailscaleStatusSummary();
|
|
20798
|
+
const peers = summary?.peers ?? [];
|
|
20680
20799
|
return {
|
|
20681
|
-
available:
|
|
20800
|
+
available: summary !== null,
|
|
20801
|
+
running: summary?.running ?? false,
|
|
20802
|
+
backendState: summary?.backendState ?? null,
|
|
20803
|
+
health: summary?.health ?? [],
|
|
20682
20804
|
peers,
|
|
20683
20805
|
onlineCount: peers.filter((p) => p.online).length
|
|
20684
20806
|
};
|
|
@@ -20699,6 +20821,16 @@ function computeIssues(health, localNode, nodes, tailscale2) {
|
|
|
20699
20821
|
});
|
|
20700
20822
|
return issues;
|
|
20701
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
|
+
}
|
|
20702
20834
|
if (localNode?.advertiseScope === "local") {
|
|
20703
20835
|
issues.push({
|
|
20704
20836
|
code: "local_only",
|
|
@@ -20866,6 +20998,24 @@ async function announceMeshVisibility() {
|
|
|
20866
20998
|
} catch {}
|
|
20867
20999
|
return loadMeshStatus();
|
|
20868
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
|
+
}
|
|
20869
21019
|
|
|
20870
21020
|
// packages/web/server/runtime-summary.ts
|
|
20871
21021
|
async function loadOpenScoutWebShellState() {
|
|
@@ -20902,598 +21052,266 @@ async function loadOpenScoutWebShellState() {
|
|
|
20902
21052
|
|
|
20903
21053
|
// packages/web/server/create-openscout-web-server.ts
|
|
20904
21054
|
init_setup();
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
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);
|
|
20911
21071
|
}
|
|
20912
|
-
function
|
|
20913
|
-
if (
|
|
20914
|
-
|
|
20915
|
-
return session.agentId;
|
|
20916
|
-
}
|
|
20917
|
-
const participants = session.participantIds.filter((participantId) => participantId.trim().length > 0);
|
|
20918
|
-
if (participants.length === 2) {
|
|
20919
|
-
const operatorCandidates = new Set([senderId.trim(), "operator"]);
|
|
20920
|
-
const nonOperatorParticipants = participants.filter((participantId) => !operatorCandidates.has(participantId));
|
|
20921
|
-
if (nonOperatorParticipants.length === 1) {
|
|
20922
|
-
return nonOperatorParticipants[0] ?? null;
|
|
20923
|
-
}
|
|
20924
|
-
const localSessionParticipant = nonOperatorParticipants.find((participantId) => participantId.startsWith("local-session-agent-")) ?? participants.find((participantId) => participantId.startsWith("local-session-agent-"));
|
|
20925
|
-
if (localSessionParticipant) {
|
|
20926
|
-
return localSessionParticipant;
|
|
20927
|
-
}
|
|
20928
|
-
return participants[0] ?? null;
|
|
20929
|
-
}
|
|
20930
|
-
}
|
|
20931
|
-
if (conversationId?.startsWith("dm.operator.")) {
|
|
20932
|
-
const legacyAgentId = conversationId.slice("dm.operator.".length);
|
|
20933
|
-
return legacyAgentId || null;
|
|
21072
|
+
function send(ws, data) {
|
|
21073
|
+
if (ws.readyState === 1) {
|
|
21074
|
+
ws.send(JSON.stringify(data));
|
|
20934
21075
|
}
|
|
20935
|
-
return null;
|
|
20936
21076
|
}
|
|
20937
|
-
function
|
|
20938
|
-
|
|
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
|
+
}
|
|
20939
21090
|
}
|
|
20940
|
-
function
|
|
20941
|
-
|
|
20942
|
-
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
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
|
+
}
|
|
20946
21099
|
}
|
|
20947
|
-
function
|
|
20948
|
-
return
|
|
21100
|
+
function findClaudeBin() {
|
|
21101
|
+
return findBin("claude", "CLAUDE_BIN");
|
|
20949
21102
|
}
|
|
20950
|
-
function
|
|
20951
|
-
return
|
|
21103
|
+
function findPiBin() {
|
|
21104
|
+
return findBin("pi", "PI_BIN");
|
|
20952
21105
|
}
|
|
20953
|
-
function
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
return
|
|
20957
|
-
}
|
|
20958
|
-
|
|
20959
|
-
if (existsSync12(resolve9(bundled, "index.html"))) {
|
|
20960
|
-
return bundled;
|
|
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;
|
|
20961
21112
|
}
|
|
20962
|
-
return resolveSourceStaticClientRoot(import.meta.url);
|
|
20963
21113
|
}
|
|
20964
|
-
|
|
20965
|
-
|
|
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
|
+
}
|
|
21128
|
+
}
|
|
20966
21129
|
}
|
|
20967
|
-
|
|
20968
|
-
const
|
|
20969
|
-
|
|
20970
|
-
|
|
20971
|
-
|
|
20972
|
-
|
|
20973
|
-
|
|
20974
|
-
|
|
20975
|
-
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
20983
|
-
|
|
20984
|
-
|
|
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
|
|
20985
21148
|
});
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
|
|
20989
|
-
|
|
20990
|
-
|
|
21149
|
+
}
|
|
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;
|
|
20991
21165
|
}
|
|
20992
|
-
|
|
20993
|
-
|
|
20994
|
-
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
return payload ? c.json(payload) : c.json({ error: "not found" }, 404);
|
|
21000
|
-
});
|
|
21001
|
-
app.get("/api/activity", (c) => c.json(queryActivity()));
|
|
21002
|
-
app.get("/api/heartrate", (c) => c.json(queryHeartrate()));
|
|
21003
|
-
app.get("/api/fleet", (c) => c.json(queryFleet({
|
|
21004
|
-
limit: parseOptionalPositiveInt(c.req.query("limit")),
|
|
21005
|
-
activityLimit: parseOptionalPositiveInt(c.req.query("activityLimit"))
|
|
21006
|
-
})));
|
|
21007
|
-
app.get("/api/messages", (c) => c.json(queryRecentMessages(parseOptionalPositiveInt(c.req.query("limit"), 80) ?? 80, { conversationId: c.req.query("conversationId") || undefined })));
|
|
21008
|
-
const handleListWork = (c) => {
|
|
21009
|
-
const agentId = c.req.query("agentId");
|
|
21010
|
-
const activeOnly = c.req.query("active") !== "false";
|
|
21011
|
-
return c.json(queryWorkItems({
|
|
21012
|
-
agentId: agentId || undefined,
|
|
21013
|
-
activeOnly
|
|
21014
|
-
}));
|
|
21015
|
-
};
|
|
21016
|
-
const handleWorkDetail = (c) => {
|
|
21017
|
-
const workId = c.req.param("id");
|
|
21018
|
-
if (!workId) {
|
|
21019
|
-
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;
|
|
21020
21173
|
}
|
|
21021
|
-
|
|
21022
|
-
|
|
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
|
|
21023
21242
|
};
|
|
21024
|
-
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
|
|
21028
|
-
|
|
21029
|
-
const agentId = c.req.query("agentId");
|
|
21030
|
-
const conversationId = c.req.query("conversationId");
|
|
21031
|
-
const collaborationRecordId = c.req.query("collaborationRecordId");
|
|
21032
|
-
const activeOnly = c.req.query("active") !== "false";
|
|
21033
|
-
return c.json(queryFlights({
|
|
21034
|
-
agentId: agentId || undefined,
|
|
21035
|
-
conversationId: conversationId || undefined,
|
|
21036
|
-
collaborationRecordId: collaborationRecordId || undefined,
|
|
21037
|
-
activeOnly
|
|
21038
|
-
}));
|
|
21039
|
-
});
|
|
21040
|
-
app.get("/api/sessions", (c) => c.json(querySessions()));
|
|
21041
|
-
app.get("/api/session/:id", (c) => {
|
|
21042
|
-
const session = querySessionById(c.req.param("id"));
|
|
21043
|
-
return session ? c.json(session) : c.json({ error: "not found" }, 404);
|
|
21044
|
-
});
|
|
21045
|
-
app.get("/api/mesh", async (c) => {
|
|
21046
|
-
try {
|
|
21047
|
-
return c.json(await loadMeshStatus());
|
|
21048
|
-
} catch (err) {
|
|
21049
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21050
|
-
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);
|
|
21051
21248
|
}
|
|
21052
|
-
|
|
21053
|
-
|
|
21054
|
-
try {
|
|
21055
|
-
return c.json(await announceMeshVisibility());
|
|
21056
|
-
} catch (err) {
|
|
21057
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21058
|
-
return c.json({ error: message }, 500);
|
|
21249
|
+
if (session.ws && session.ws.readyState === 1) {
|
|
21250
|
+
send(session.ws, { type: "terminal:data", data });
|
|
21059
21251
|
}
|
|
21060
21252
|
});
|
|
21061
|
-
|
|
21062
|
-
|
|
21063
|
-
|
|
21064
|
-
|
|
21065
|
-
const
|
|
21066
|
-
|
|
21067
|
-
|
|
21068
|
-
|
|
21069
|
-
|
|
21070
|
-
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
|
|
21074
|
-
hasProjectConfig,
|
|
21075
|
-
hasOperatorName: userName.length > 0,
|
|
21076
|
-
localConfigPath: localConfigPath(),
|
|
21077
|
-
localConfig: hasLocalConfig ? loadLocalConfig() : null,
|
|
21078
|
-
projectRoot,
|
|
21079
|
-
currentDirectory,
|
|
21080
|
-
contextRoot: configuredContextRoot,
|
|
21081
|
-
operatorName: userName || null,
|
|
21082
|
-
operatorNameSuggestion: resolveOperatorName()
|
|
21083
|
-
});
|
|
21084
|
-
});
|
|
21085
|
-
app.delete("/api/onboarding/state", (c) => {
|
|
21086
|
-
try {
|
|
21087
|
-
rmSync3(localConfigPath(), { force: true });
|
|
21088
|
-
} catch {}
|
|
21089
|
-
return c.json({ ok: true, localConfigPath: localConfigPath() });
|
|
21090
|
-
});
|
|
21091
|
-
app.post("/api/onboarding/project", async (c) => {
|
|
21092
|
-
const body = await c.req.json().catch(() => ({}));
|
|
21093
|
-
const contextRoot = body.contextRoot?.trim();
|
|
21094
|
-
if (!contextRoot) {
|
|
21095
|
-
return c.json({ error: "contextRoot is required" }, 400);
|
|
21096
|
-
}
|
|
21097
|
-
const sourceRoots = (body.sourceRoots ?? []).map((entry) => entry?.trim()).filter((entry) => Boolean(entry && entry.length > 0));
|
|
21098
|
-
const harness = body.defaultHarness === "codex" ? "codex" : "claude";
|
|
21099
|
-
try {
|
|
21100
|
-
await writeOpenScoutSettings({
|
|
21101
|
-
discovery: {
|
|
21102
|
-
contextRoot,
|
|
21103
|
-
workspaceRoots: sourceRoots
|
|
21104
|
-
},
|
|
21105
|
-
agents: { defaultHarness: harness }
|
|
21106
|
-
});
|
|
21107
|
-
const result = await initializeOpenScoutSetup({
|
|
21108
|
-
currentDirectory: contextRoot
|
|
21109
|
-
});
|
|
21110
|
-
return c.json({
|
|
21111
|
-
ok: true,
|
|
21112
|
-
projectConfigPath: result.currentProjectConfigPath
|
|
21113
|
-
});
|
|
21114
|
-
} catch (err) {
|
|
21115
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21116
|
-
console.error("[onboarding/project]", message);
|
|
21117
|
-
return c.json({ error: message }, 500);
|
|
21118
|
-
}
|
|
21119
|
-
});
|
|
21120
|
-
app.post("/api/onboarding/init", async (c) => {
|
|
21121
|
-
const body = await c.req.json().catch(() => ({}));
|
|
21122
|
-
writeLocalConfig({
|
|
21123
|
-
version: 1,
|
|
21124
|
-
host: body.host ?? DEFAULT_LOCAL_CONFIG.host,
|
|
21125
|
-
ports: {
|
|
21126
|
-
broker: body.ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker,
|
|
21127
|
-
web: body.ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web,
|
|
21128
|
-
pairing: body.ports?.pairing ?? DEFAULT_LOCAL_CONFIG.ports.pairing
|
|
21129
|
-
}
|
|
21130
|
-
});
|
|
21131
|
-
return c.json({
|
|
21132
|
-
ok: true,
|
|
21133
|
-
localConfig: loadLocalConfig(),
|
|
21134
|
-
localConfigPath: localConfigPath()
|
|
21135
|
-
});
|
|
21136
|
-
});
|
|
21137
|
-
app.post("/api/user", async (c) => {
|
|
21138
|
-
const { name } = await c.req.json();
|
|
21139
|
-
const config = loadUserConfig();
|
|
21140
|
-
if (name?.trim()) {
|
|
21141
|
-
config.name = name.trim();
|
|
21142
|
-
} else {
|
|
21143
|
-
delete config.name;
|
|
21144
|
-
}
|
|
21145
|
-
saveUserConfig(config);
|
|
21146
|
-
return c.json({ name: resolveOperatorName() });
|
|
21147
|
-
});
|
|
21148
|
-
app.post("/api/agents/:agentId/interrupt", async (c) => {
|
|
21149
|
-
const agentId = c.req.param("agentId");
|
|
21150
|
-
const { interruptLocalAgent: interruptLocalAgent2 } = await init_local_agents().then(() => exports_local_agents);
|
|
21151
|
-
const result = await interruptLocalAgent2(agentId);
|
|
21152
|
-
if (!result.ok)
|
|
21153
|
-
return c.json({ error: "Agent not found or not interruptible" }, 404);
|
|
21154
|
-
return c.json({ ok: true });
|
|
21155
|
-
});
|
|
21156
|
-
app.post("/api/send", async (c) => {
|
|
21157
|
-
const { body, conversationId } = await c.req.json();
|
|
21158
|
-
if (!body?.trim()) {
|
|
21159
|
-
return c.json({ error: "body is required" }, 400);
|
|
21160
|
-
}
|
|
21161
|
-
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21162
|
-
if (directAgentId) {
|
|
21163
|
-
const result2 = await sendScoutDirectMessage({
|
|
21164
|
-
agentId: directAgentId,
|
|
21165
|
-
body: body.trim(),
|
|
21166
|
-
currentDirectory,
|
|
21167
|
-
source: "scout-web"
|
|
21168
|
-
});
|
|
21169
|
-
return c.json(result2);
|
|
21170
|
-
}
|
|
21171
|
-
const result = await sendScoutMessage({
|
|
21172
|
-
senderId,
|
|
21173
|
-
body: body.trim(),
|
|
21174
|
-
currentDirectory
|
|
21175
|
-
});
|
|
21176
|
-
if (!result.usedBroker) {
|
|
21177
|
-
return c.json({ error: "broker unreachable" }, 502);
|
|
21178
|
-
}
|
|
21179
|
-
return c.json(result);
|
|
21180
|
-
});
|
|
21181
|
-
app.post("/api/ask", async (c) => {
|
|
21182
|
-
const { body, conversationId } = await c.req.json();
|
|
21183
|
-
if (!body?.trim()) {
|
|
21184
|
-
return c.json({ error: "body is required" }, 400);
|
|
21185
|
-
}
|
|
21186
|
-
const { directAgentId, senderId } = resolveConversationRouting(conversationId);
|
|
21187
|
-
if (!directAgentId) {
|
|
21188
|
-
return c.json({
|
|
21189
|
-
error: "ask is only available in a direct conversation with one agent"
|
|
21190
|
-
}, 400);
|
|
21191
|
-
}
|
|
21192
|
-
const result = await askScoutQuestion({
|
|
21193
|
-
senderId,
|
|
21194
|
-
targetLabel: directAgentId,
|
|
21195
|
-
body: body.trim(),
|
|
21196
|
-
currentDirectory
|
|
21197
|
-
});
|
|
21198
|
-
if (!result.usedBroker) {
|
|
21199
|
-
return c.json({ error: "broker unreachable" }, 502);
|
|
21200
|
-
}
|
|
21201
|
-
if (result.unresolvedTarget) {
|
|
21202
|
-
return c.json({
|
|
21203
|
-
error: `could not route ask to ${result.unresolvedTarget}`,
|
|
21204
|
-
targetDiagnostic: result.targetDiagnostic ?? null
|
|
21205
|
-
}, 409);
|
|
21206
|
-
}
|
|
21207
|
-
return c.json(result);
|
|
21208
|
-
});
|
|
21209
|
-
app.get("/api/events", async (c) => {
|
|
21210
|
-
const brokerHost = process.env.OPENSCOUT_BROKER_HOST ?? "127.0.0.1";
|
|
21211
|
-
const brokerPort = process.env.OPENSCOUT_BROKER_PORT ?? "65535";
|
|
21212
|
-
const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? `http://${brokerHost}:${brokerPort}`;
|
|
21213
|
-
try {
|
|
21214
|
-
return await relayEventStream(`${brokerUrl}/v1/events/stream`, {
|
|
21215
|
-
signal: c.req.raw.signal
|
|
21216
|
-
});
|
|
21217
|
-
} catch {
|
|
21218
|
-
return c.text("Broker unreachable", 502);
|
|
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}`);
|
|
21219
21266
|
}
|
|
21220
|
-
|
|
21221
|
-
|
|
21222
|
-
assetMode: options.assetMode,
|
|
21223
|
-
staticRoot: resolveStaticRoot(options.staticRoot),
|
|
21224
|
-
viteDevUrl: options.viteDevUrl,
|
|
21225
|
-
defaultViteUrl: "http://127.0.0.1:5180"
|
|
21226
|
-
});
|
|
21227
|
-
const warmupCaches = () => Promise.allSettled([
|
|
21228
|
-
shellStateCache.refresh(),
|
|
21229
|
-
loadPairingState(currentDirectory, true)
|
|
21230
|
-
]).then((results) => {
|
|
21231
|
-
for (const result of results) {
|
|
21232
|
-
if (result.status === "rejected") {
|
|
21233
|
-
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
21234
|
-
console.error("[openscout-web api] initial cache warmup failed:", message);
|
|
21235
|
-
}
|
|
21267
|
+
if (session.ws) {
|
|
21268
|
+
send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
|
|
21236
21269
|
}
|
|
21270
|
+
scheduleReap(session, 1e4);
|
|
21237
21271
|
});
|
|
21238
|
-
|
|
21239
|
-
}
|
|
21240
|
-
|
|
21241
|
-
// ../hudson/packages/hudson-relay/src/relay/session.ts
|
|
21242
|
-
import { createRequire } from "module";
|
|
21243
|
-
import { execSync as execSync2 } from "child_process";
|
|
21244
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
21245
|
-
import { join as join19, dirname as pathDirname } from "path";
|
|
21246
|
-
var require2 = createRequire(import.meta.url);
|
|
21247
|
-
var pty = require2("node-pty");
|
|
21248
|
-
var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
|
|
21249
|
-
var MAX_BUFFER_SIZE = 512 * 1024;
|
|
21250
|
-
var sessions3 = new Map;
|
|
21251
|
-
function generateId() {
|
|
21252
|
-
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;
|
|
21253
21275
|
}
|
|
21254
|
-
function
|
|
21255
|
-
if (
|
|
21256
|
-
|
|
21276
|
+
function attachSession(session, ws, cols, rows) {
|
|
21277
|
+
if (session.reapTimer) {
|
|
21278
|
+
clearTimeout(session.reapTimer);
|
|
21279
|
+
session.reapTimer = null;
|
|
21257
21280
|
}
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
|
|
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
|
+
}
|
|
21290
|
+
}
|
|
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 });
|
|
21271
21295
|
}
|
|
21296
|
+
console.log(`[relay] Session ${session.id} reconnected`);
|
|
21272
21297
|
}
|
|
21273
|
-
function
|
|
21274
|
-
|
|
21275
|
-
|
|
21276
|
-
|
|
21277
|
-
|
|
21278
|
-
|
|
21279
|
-
|
|
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)`);
|
|
21280
21305
|
}
|
|
21281
21306
|
}
|
|
21282
|
-
function
|
|
21283
|
-
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
21287
|
-
|
|
21288
|
-
|
|
21289
|
-
|
|
21290
|
-
execSync2(`tmux has-session -t ${name} 2>/dev/null`, { encoding: "utf8" });
|
|
21291
|
-
return true;
|
|
21292
|
-
} catch {
|
|
21293
|
-
return false;
|
|
21294
|
-
}
|
|
21295
|
-
}
|
|
21296
|
-
function bootstrapFiles(cwd, files, sessionId) {
|
|
21297
|
-
for (const [relPath, content] of Object.entries(files)) {
|
|
21298
|
-
const absPath = join19(cwd, relPath);
|
|
21299
|
-
if (!existsSync13(absPath)) {
|
|
21300
|
-
try {
|
|
21301
|
-
const dir = pathDirname(absPath);
|
|
21302
|
-
if (!existsSync13(dir))
|
|
21303
|
-
mkdirSync6(dir, { recursive: true });
|
|
21304
|
-
writeFileSync6(absPath, content, "utf-8");
|
|
21305
|
-
console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
|
|
21306
|
-
} catch (err) {
|
|
21307
|
-
console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
|
|
21308
|
-
}
|
|
21309
|
-
}
|
|
21310
|
-
}
|
|
21311
|
-
}
|
|
21312
|
-
function spawnTmuxSession(tmuxName, cols, rows, cwd, claudeBin, claudeArgs, env) {
|
|
21313
|
-
const exists = tmuxSessionExists2(tmuxName);
|
|
21314
|
-
if (!exists) {
|
|
21315
|
-
const shellCmd = [claudeBin, ...claudeArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
|
|
21316
|
-
execSync2(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
|
|
21317
|
-
console.log(`[relay] Created tmux session: ${tmuxName}`);
|
|
21318
|
-
} else {
|
|
21319
|
-
try {
|
|
21320
|
-
execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
|
|
21321
|
-
} catch {}
|
|
21322
|
-
console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
|
|
21323
|
-
}
|
|
21324
|
-
return pty.spawn("tmux", ["attach", "-t", tmuxName], {
|
|
21325
|
-
name: "xterm-256color",
|
|
21326
|
-
cols,
|
|
21327
|
-
rows,
|
|
21328
|
-
cwd,
|
|
21329
|
-
env
|
|
21330
|
-
});
|
|
21331
|
-
}
|
|
21332
|
-
function createSession(ws, msg) {
|
|
21333
|
-
const id = generateId();
|
|
21334
|
-
const cols = Math.max(msg.cols || 80, 20);
|
|
21335
|
-
const rows = Math.max(msg.rows || 24, 4);
|
|
21336
|
-
const backend = msg.backend || "pty";
|
|
21337
|
-
const tmuxName = msg.tmuxSession || `hudson-${id}`;
|
|
21338
|
-
const agent = msg.agent || "claude";
|
|
21339
|
-
let agentBin;
|
|
21340
|
-
if (agent === "pi") {
|
|
21341
|
-
agentBin = findPiBin();
|
|
21342
|
-
if (!agentBin) {
|
|
21343
|
-
const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
|
|
21344
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21345
|
-
send(ws, { type: "session:error", error: reason });
|
|
21346
|
-
return null;
|
|
21347
|
-
}
|
|
21348
|
-
} else {
|
|
21349
|
-
agentBin = findClaudeBin();
|
|
21350
|
-
if (!agentBin) {
|
|
21351
|
-
const reason = "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code";
|
|
21352
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21353
|
-
send(ws, { type: "session:error", error: reason });
|
|
21354
|
-
return null;
|
|
21355
|
-
}
|
|
21356
|
-
}
|
|
21357
|
-
if (!existsSync13(agentBin)) {
|
|
21358
|
-
const reason = `${agent} binary not found at ${agentBin}`;
|
|
21359
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21360
|
-
send(ws, { type: "session:error", error: reason });
|
|
21361
|
-
return null;
|
|
21362
|
-
}
|
|
21363
|
-
if (backend === "tmux" && !findBin("tmux")) {
|
|
21364
|
-
const reason = "tmux not found. Install it with: brew install tmux";
|
|
21365
|
-
console.error(`[relay] Session ${id} failed: ${reason}`);
|
|
21366
|
-
send(ws, { type: "session:error", error: reason });
|
|
21367
|
-
return null;
|
|
21368
|
-
}
|
|
21369
|
-
const cwd = resolveCwd(msg.cwd);
|
|
21370
|
-
if (msg.workspaceFiles) {
|
|
21371
|
-
bootstrapFiles(cwd, msg.workspaceFiles, id);
|
|
21372
|
-
}
|
|
21373
|
-
let agentArgs;
|
|
21374
|
-
if (agent === "pi") {
|
|
21375
|
-
agentArgs = ["--verbose"];
|
|
21376
|
-
if (msg.provider)
|
|
21377
|
-
agentArgs.push("--provider", msg.provider);
|
|
21378
|
-
if (msg.model)
|
|
21379
|
-
agentArgs.push("--model", msg.model);
|
|
21380
|
-
if (msg.systemPrompt)
|
|
21381
|
-
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21382
|
-
} else {
|
|
21383
|
-
agentArgs = ["--verbose"];
|
|
21384
|
-
if (msg.systemPrompt)
|
|
21385
|
-
agentArgs.push("--system-prompt", msg.systemPrompt);
|
|
21386
|
-
}
|
|
21387
|
-
const env = { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" };
|
|
21388
|
-
delete env.CLAUDECODE;
|
|
21389
|
-
let ptyProcess;
|
|
21390
|
-
try {
|
|
21391
|
-
if (backend === "tmux") {
|
|
21392
|
-
console.log(`[relay] Session ${id}: tmux backend (session: ${tmuxName}) in ${cwd} [agent: ${agent}]`);
|
|
21393
|
-
ptyProcess = spawnTmuxSession(tmuxName, cols, rows, cwd, agentBin, agentArgs, env);
|
|
21394
|
-
} else {
|
|
21395
|
-
console.log(`[relay] Session ${id}: pty backend, spawning ${agentBin} in ${cwd} [agent: ${agent}]`);
|
|
21396
|
-
ptyProcess = pty.spawn(agentBin, agentArgs, {
|
|
21397
|
-
name: "xterm-256color",
|
|
21398
|
-
cols,
|
|
21399
|
-
rows,
|
|
21400
|
-
cwd,
|
|
21401
|
-
env
|
|
21402
|
-
});
|
|
21403
|
-
}
|
|
21404
|
-
} catch (err) {
|
|
21405
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
21406
|
-
console.error(`[relay] Session ${id}: failed to spawn PTY \u2014 ${message}`);
|
|
21407
|
-
send(ws, { type: "session:error", error: `Failed to spawn terminal: ${message}` });
|
|
21408
|
-
return null;
|
|
21409
|
-
}
|
|
21410
|
-
const orphanTTL = msg.orphanTTL && msg.orphanTTL > 0 ? msg.orphanTTL : DEFAULT_ORPHAN_TTL_MS;
|
|
21411
|
-
const session = {
|
|
21412
|
-
id,
|
|
21413
|
-
pty: ptyProcess,
|
|
21414
|
-
ws,
|
|
21415
|
-
outputBuffer: "",
|
|
21416
|
-
cols,
|
|
21417
|
-
rows,
|
|
21418
|
-
reapTimer: null,
|
|
21419
|
-
orphanTTL,
|
|
21420
|
-
backend,
|
|
21421
|
-
...backend === "tmux" ? { tmuxSession: tmuxName } : {},
|
|
21422
|
-
exited: false,
|
|
21423
|
-
exitCode: null
|
|
21424
|
-
};
|
|
21425
|
-
const startTime = Date.now();
|
|
21426
|
-
ptyProcess.onData((data) => {
|
|
21427
|
-
session.outputBuffer += data;
|
|
21428
|
-
if (session.outputBuffer.length > MAX_BUFFER_SIZE) {
|
|
21429
|
-
session.outputBuffer = session.outputBuffer.slice(-MAX_BUFFER_SIZE);
|
|
21430
|
-
}
|
|
21431
|
-
if (session.ws && session.ws.readyState === 1) {
|
|
21432
|
-
send(session.ws, { type: "terminal:data", data });
|
|
21433
|
-
}
|
|
21434
|
-
});
|
|
21435
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
21436
|
-
session.exited = true;
|
|
21437
|
-
session.exitCode = exitCode;
|
|
21438
|
-
const uptime = Date.now() - startTime;
|
|
21439
|
-
const crashed = exitCode !== 0 && uptime < 5000;
|
|
21440
|
-
let reason;
|
|
21441
|
-
if (crashed) {
|
|
21442
|
-
const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
|
|
21443
|
-
const lines = clean.split(`
|
|
21444
|
-
`).filter((l) => l.trim()).slice(-5);
|
|
21445
|
-
reason = lines.join(`
|
|
21446
|
-
`) || `Process exited with code ${exitCode}`;
|
|
21447
|
-
console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
|
|
21448
|
-
}
|
|
21449
|
-
if (session.ws) {
|
|
21450
|
-
send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
|
|
21451
|
-
}
|
|
21452
|
-
scheduleReap(session, 1e4);
|
|
21453
|
-
});
|
|
21454
|
-
sessions3.set(id, session);
|
|
21455
|
-
console.log(`[relay] Session ${id} created (${cols}x${rows})`);
|
|
21456
|
-
return session;
|
|
21457
|
-
}
|
|
21458
|
-
function attachSession(session, ws, cols, rows) {
|
|
21459
|
-
if (session.reapTimer) {
|
|
21460
|
-
clearTimeout(session.reapTimer);
|
|
21461
|
-
session.reapTimer = null;
|
|
21462
|
-
}
|
|
21463
|
-
session.ws = ws;
|
|
21464
|
-
if (cols && rows) {
|
|
21465
|
-
const c = Math.max(cols, 20);
|
|
21466
|
-
const r = Math.max(rows, 4);
|
|
21467
|
-
if (c !== session.cols || r !== session.rows) {
|
|
21468
|
-
session.pty.resize(c, r);
|
|
21469
|
-
session.cols = c;
|
|
21470
|
-
session.rows = r;
|
|
21471
|
-
}
|
|
21472
|
-
}
|
|
21473
|
-
if (session.exited) {
|
|
21474
|
-
send(ws, { type: "session:exit", exitCode: session.exitCode });
|
|
21475
|
-
} else if (session.outputBuffer.length > 0) {
|
|
21476
|
-
send(ws, { type: "terminal:data", data: session.outputBuffer });
|
|
21477
|
-
}
|
|
21478
|
-
console.log(`[relay] Session ${session.id} reconnected`);
|
|
21479
|
-
}
|
|
21480
|
-
function detachSession(session) {
|
|
21481
|
-
session.ws = null;
|
|
21482
|
-
if (session.exited) {
|
|
21483
|
-
scheduleReap(session, 5000);
|
|
21484
|
-
} else {
|
|
21485
|
-
scheduleReap(session, session.orphanTTL);
|
|
21486
|
-
console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
|
|
21487
|
-
}
|
|
21488
|
-
}
|
|
21489
|
-
function scheduleReap(session, delay) {
|
|
21490
|
-
if (session.reapTimer)
|
|
21491
|
-
clearTimeout(session.reapTimer);
|
|
21492
|
-
session.reapTimer = setTimeout(() => {
|
|
21493
|
-
if (!session.ws) {
|
|
21494
|
-
destroy(session.id);
|
|
21495
|
-
}
|
|
21496
|
-
}, delay);
|
|
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);
|
|
21497
21315
|
}
|
|
21498
21316
|
function destroy(sessionId) {
|
|
21499
21317
|
const session = sessions3.get(sessionId);
|
|
@@ -21528,6 +21346,22 @@ async function handleRelayUpload(req) {
|
|
|
21528
21346
|
await Bun.write(filepath, Buffer.from(data, "base64"));
|
|
21529
21347
|
return Response.json({ path: filepath });
|
|
21530
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
|
+
}
|
|
21531
21365
|
function asRelay(ws) {
|
|
21532
21366
|
return ws;
|
|
21533
21367
|
}
|
|
@@ -21558,6 +21392,10 @@ var relayWebSocket = {
|
|
|
21558
21392
|
break;
|
|
21559
21393
|
ws.data.sessionId = session.id;
|
|
21560
21394
|
send(rws, { type: "session:ready", sessionId: session.id });
|
|
21395
|
+
const pending = drainPendingCommand();
|
|
21396
|
+
if (pending)
|
|
21397
|
+
setTimeout(() => session.pty.write(pending + `
|
|
21398
|
+
`), 400);
|
|
21561
21399
|
break;
|
|
21562
21400
|
}
|
|
21563
21401
|
case "session:reconnect": {
|
|
@@ -21578,6 +21416,10 @@ var relayWebSocket = {
|
|
|
21578
21416
|
sessionId: existing.id,
|
|
21579
21417
|
reconnected: true
|
|
21580
21418
|
});
|
|
21419
|
+
const pending = drainPendingCommand();
|
|
21420
|
+
if (pending)
|
|
21421
|
+
setTimeout(() => existing.pty.write(pending + `
|
|
21422
|
+
`), 400);
|
|
21581
21423
|
} else {
|
|
21582
21424
|
send(rws, { type: "session:expired", sessionId: msg.sessionId });
|
|
21583
21425
|
}
|
|
@@ -21621,8 +21463,433 @@ function destroyAllRelaySessions() {
|
|
|
21621
21463
|
destroy(id);
|
|
21622
21464
|
}
|
|
21623
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;
|
|
21470
|
+
}
|
|
21471
|
+
const parsed = Number.parseInt(value, 10);
|
|
21472
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
21473
|
+
}
|
|
21474
|
+
function inferDirectTargetAgentId(conversationId, session, senderId) {
|
|
21475
|
+
if (session?.kind === "direct") {
|
|
21476
|
+
if (session.agentId) {
|
|
21477
|
+
return session.agentId;
|
|
21478
|
+
}
|
|
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;
|
|
21491
|
+
}
|
|
21492
|
+
}
|
|
21493
|
+
if (conversationId?.startsWith("dm.operator.")) {
|
|
21494
|
+
const legacyAgentId = conversationId.slice("dm.operator.".length);
|
|
21495
|
+
return legacyAgentId || null;
|
|
21496
|
+
}
|
|
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;
|
|
21519
|
+
}
|
|
21520
|
+
const bundled = resolveBundledStaticClientRoot(import.meta.url);
|
|
21521
|
+
if (existsSync14(resolve9(bundled, "index.html"))) {
|
|
21522
|
+
return bundled;
|
|
21523
|
+
}
|
|
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);
|
|
21553
|
+
}
|
|
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
|
+
}));
|
|
21595
|
+
};
|
|
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);
|
|
21809
|
+
}
|
|
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);
|
|
21827
|
+
}
|
|
21828
|
+
return c.json(result);
|
|
21829
|
+
});
|
|
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);
|
|
21834
|
+
}
|
|
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);
|
|
21840
|
+
}
|
|
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);
|
|
21849
|
+
}
|
|
21850
|
+
if (result.unresolvedTarget) {
|
|
21851
|
+
return c.json({
|
|
21852
|
+
error: `could not route ask to ${result.unresolvedTarget}`,
|
|
21853
|
+
targetDiagnostic: result.targetDiagnostic ?? null
|
|
21854
|
+
}, 409);
|
|
21855
|
+
}
|
|
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);
|
|
21868
|
+
}
|
|
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
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
});
|
|
21887
|
+
return { app, warmupCaches };
|
|
21888
|
+
}
|
|
21889
|
+
|
|
21624
21890
|
// packages/web/server/index.ts
|
|
21625
21891
|
var port = Number(process.env.OPENSCOUT_WEB_PORT ?? process.env.SCOUT_WEB_PORT ?? "3200");
|
|
21892
|
+
var hostname2 = process.env.OPENSCOUT_WEB_HOST?.trim() || process.env.SCOUT_WEB_HOST?.trim() || "127.0.0.1";
|
|
21626
21893
|
var currentDirectory = process.env.OPENSCOUT_SETUP_CWD?.trim() || process.cwd();
|
|
21627
21894
|
var shellStateCacheTtlMs = Number.parseInt(process.env.OPENSCOUT_WEB_SHELL_CACHE_TTL_MS ?? "15000", 10);
|
|
21628
21895
|
function resolveStaticRoot2() {
|
|
@@ -21631,11 +21898,11 @@ function resolveStaticRoot2() {
|
|
|
21631
21898
|
}
|
|
21632
21899
|
const selfDir = dirname13(fileURLToPath8(import.meta.url));
|
|
21633
21900
|
const siblingClientRoot = join21(selfDir, "client");
|
|
21634
|
-
if (
|
|
21901
|
+
if (existsSync15(join21(siblingClientRoot, "index.html"))) {
|
|
21635
21902
|
return siblingClientRoot;
|
|
21636
21903
|
}
|
|
21637
21904
|
const sourceDistClientRoot = resolve10(selfDir, "../dist/client");
|
|
21638
|
-
if (
|
|
21905
|
+
if (existsSync15(join21(sourceDistClientRoot, "index.html"))) {
|
|
21639
21906
|
return sourceDistClientRoot;
|
|
21640
21907
|
}
|
|
21641
21908
|
return;
|
|
@@ -21654,6 +21921,7 @@ var { app, warmupCaches } = await createOpenScoutWebServer({
|
|
|
21654
21921
|
var honoFetch = app.fetch;
|
|
21655
21922
|
var server = Bun.serve({
|
|
21656
21923
|
port,
|
|
21924
|
+
hostname: hostname2,
|
|
21657
21925
|
idleTimeout: idleTimeoutSeconds,
|
|
21658
21926
|
fetch(req, server2) {
|
|
21659
21927
|
const url = new URL(req.url);
|
|
@@ -21680,6 +21948,6 @@ var shutdown = () => {
|
|
|
21680
21948
|
};
|
|
21681
21949
|
process.on("SIGINT", shutdown);
|
|
21682
21950
|
process.on("SIGTERM", shutdown);
|
|
21683
|
-
console.log(`OpenScout Web -> http
|
|
21684
|
-
console.log(`Relay WebSocket -> ws
|
|
21951
|
+
console.log(`OpenScout Web -> http://${hostname2}:${server.port}`);
|
|
21952
|
+
console.log(`Relay WebSocket -> ws://${hostname2}:${server.port}`);
|
|
21685
21953
|
warmupCaches();
|