@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.
@@ -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, rm as rm3, writeFile as writeFile4 } from "fs/promises";
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
- sessionStatePath;
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.sessionStatePath = join13(options.runtimeDirectory, "claude-session-id.txt");
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 rm3(this.sessionStatePath, { force: true });
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
- this.claudeSessionId = await readOptionalFile2(this.sessionStatePath);
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
- writeFile4(this.sessionStatePath, `${nextSessionId}
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 sessionStatePath = join13(options.runtimeDirectory, "claude-session-id.txt");
8798
- const [rawLog, persistedSessionId] = await Promise.all([
8875
+ const [rawLog, catalog] = await Promise.all([
8799
8876
  readOptionalFile2(stdoutLogPath),
8800
- readOptionalFile2(sessionStatePath)
8877
+ readSessionCatalog(options.runtimeDirectory)
8801
8878
  ]);
8802
8879
  if (!rawLog) {
8803
8880
  return null;
8804
8881
  }
8805
- return buildClaudeStreamJsonSessionSnapshot(rawLog, options, persistedSessionId);
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 rm3(join13(options.runtimeDirectory, "claude-session-id.txt"), { force: true });
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 existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
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 existsSync9(join15(candidate, "package.json")) && existsSync9(join15(candidate, "bin", "openscout-runtime.mjs"));
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 (existsSync9(join15(candidate, "package.json")) && existsSync9(join15(candidate, "src"))) {
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") && existsSync9(process.execPath)) {
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 (existsSync9(candidate)) {
10607
+ if (existsSync10(candidate)) {
10529
10608
  return candidate;
10530
10609
  }
10531
10610
  }
10532
10611
  const homeBun = join15(homedir11(), ".bun", "bin", "bun");
10533
- if (existsSync9(homeBun)) {
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 (!existsSync9(path)) {
10802
+ if (!existsSync10(path)) {
10724
10803
  return [];
10725
10804
  }
10726
- return readFileSync5(path, "utf8").split(`
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 = existsSync9(config.launchAgentPath);
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 (existsSync9(config.launchAgentPath)) {
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 existsSync10, readFileSync as readFileSync6 } from "fs";
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 (!existsSync10(supportPaths.settingsPath)) {
11094
+ if (!existsSync11(supportPaths.settingsPath)) {
11015
11095
  return dirname11(projectPath);
11016
11096
  }
11017
- const raw2 = JSON.parse(readFileSync6(supportPaths.settingsPath, "utf8"));
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 `node ${JSON.stringify(join16(OPENSCOUT_REPO_ROOT, "packages", "cli", "bin", "scout.mjs"))}`;
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 (existsSync10(path)) {
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 = existsSync10(stderrLogFile) ? readFileSync6(stderrLogFile, "utf8").trim().split(/\r?\n/).slice(-10).join(`
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 existsSync14 } from "fs";
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 existsSync12, rmSync as rmSync3 } from "fs";
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 _dbOpenedAt = 0;
15541
- var DB_REOPEN_MS = 2000;
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.exec("PRAGMA busy_timeout = 5000");
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 endpointSessionId ?? metadataString(metadata, "externalSessionId");
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.map(() => "?").join(", ")})` : null
15841
- ].filter(Boolean).join(" AND ");
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 ${activeStates}` : null,
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.map(() => "?").join(", ")})` : null,
15973
+ conversationIds.length > 0 ? `inv.conversation_id IN (${sqlPlaceholders(conversationIds.length)})` : null,
15882
15974
  opts?.collaborationRecordId ? `inv.collaboration_record_id = ?` : null
15883
- ].filter(Boolean).join(" AND ");
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 ? `WHERE ${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 ${activeStates}
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 activeStates = "('open','working','waiting','review')";
16135
- const where = [
16225
+ const where = sqlJoinClauses([
16136
16226
  "cr.kind = 'work_item'",
16137
- opts?.activeOnly !== false ? `cr.state IN ${activeStates}` : null,
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
- ].filter(Boolean).join(" AND ");
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 ${activeStates}
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 ? `WHERE ${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.length ? `WHERE ${filters.join(" OR ")}` : ""}
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.map(() => "?").join(", ");
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.map(() => "?").join(", ");
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 WINDOW_TIERS = [
16753
- { ms: 30 * 60000, label: "30m" },
16754
- { ms: 60 * 60000, label: "1h" },
16755
- { ms: 6 * 60 * 60000, label: "6h" },
16756
- { ms: 24 * 60 * 60000, label: "24h" },
16757
- { ms: 7 * 24 * 60 * 60000, label: "7d" }
16758
- ];
16759
- var MIN_FILLED_BUCKETS = 3;
16760
- var _sealedCounts = new Map;
16761
- var _cachedTier = null;
16762
- function queryHeartrate(numBuckets = 48) {
16763
- const nowMs = Date.now();
16764
- let chosenTier = WINDOW_TIERS[WINDOW_TIERS.length - 1];
16765
- for (const tier of WINDOW_TIERS) {
16766
- const bucketMs2 = tier.ms / numBuckets;
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 idx = Math.min(numBuckets - 1, Math.floor((ms - alignedStart2) / bucketMs2));
16792
- if (idx >= 0)
16793
- counts2[idx]++;
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
- const bucketMs = chosenTier.ms / numBuckets;
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
- if (_cachedTier === chosenTier.label) {
16819
- const counts2 = new Array(numBuckets).fill(0);
16820
- let allSealed = true;
16821
- for (let i = 0;i < numBuckets - 1; i++) {
16822
- const key = String(alignedStart + i * bucketMs);
16823
- if (_sealedCounts.has(key)) {
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 < 1000000000000 ? row.ts * 1000 : 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
- for (let i = 0;i < numBuckets - 1; i++) {
16858
- _sealedCounts.set(String(alignedStart + i * bucketMs), counts[i]);
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: chosenTier.label,
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: count / peak
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 existsSync11, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
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 readTailscalePeers() {
19805
- const status = await readStatusJson();
19806
- if (!status) {
19807
- return [];
19839
+ async function readTailscaleSelf() {
19840
+ const summary = await readTailscaleStatusSummary();
19841
+ if (!summary) {
19842
+ return null;
19808
19843
  }
19809
- return parsePeers(status);
19844
+ return summary.self;
19810
19845
  }
19811
- async function readTailscaleSelf() {
19846
+ async function readTailscaleStatusSummary() {
19812
19847
  const status = await readStatusJson();
19813
19848
  if (!status) {
19814
19849
  return null;
19815
19850
  }
19816
- return parseSelf(status);
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 (!normalizedCwd || !normalizedSessionId) {
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 (!existsSync11(candidate.path)) {
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: readFileSync7(candidate.path, "utf8"),
20262
+ content: readFileSync8(candidate.path, "utf8"),
20185
20263
  adapterType: candidate.adapterType,
20186
20264
  baseTimestampMs: stat5.mtimeMs
20187
20265
  });
20188
- return {
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 loadAgentObservePayload(agentId) {
20619
- const agent = queryAgents(200).find((entry) => entry.id === agentId) ?? null;
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 peers = await readTailscalePeers();
20797
+ const summary = await readTailscaleStatusSummary();
20798
+ const peers = summary?.peers ?? [];
20680
20799
  return {
20681
- available: peers.length > 0,
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
- function parseOptionalPositiveInt(value, fallback) {
20906
- if (typeof value !== "string" || value.trim().length === 0) {
20907
- return fallback;
20908
- }
20909
- const parsed = Number.parseInt(value, 10);
20910
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
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 inferDirectTargetAgentId(conversationId, session, senderId) {
20913
- if (session?.kind === "direct") {
20914
- if (session.agentId) {
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 inferDirectSenderId(_session, _fallbackSenderId, _directTargetAgentId) {
20938
- return "operator";
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 resolveConversationRouting(conversationId) {
20941
- const fallbackSenderId = "operator";
20942
- const session = conversationId ? querySessionById(conversationId) : null;
20943
- const directAgentId = inferDirectTargetAgentId(conversationId, session, fallbackSenderId);
20944
- const senderId = inferDirectSenderId(session, fallbackSenderId, directAgentId);
20945
- return { directAgentId, senderId };
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 resolveBundledStaticClientRoot(moduleUrl = import.meta.url) {
20948
- return resolve9(dirname12(fileURLToPath7(moduleUrl)), "client");
21100
+ function findClaudeBin() {
21101
+ return findBin("claude", "CLAUDE_BIN");
20949
21102
  }
20950
- function resolveSourceStaticClientRoot(moduleUrl = import.meta.url) {
20951
- return resolve9(dirname12(fileURLToPath7(moduleUrl)), "../dist/client");
21103
+ function findPiBin() {
21104
+ return findBin("pi", "PI_BIN");
20952
21105
  }
20953
- function resolveStaticRoot(staticRoot) {
20954
- const configured = staticRoot?.trim();
20955
- if (configured) {
20956
- return configured;
20957
- }
20958
- const bundled = resolveBundledStaticClientRoot(import.meta.url);
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
- async function loadPairingState(currentDirectory, refresh) {
20965
- return refresh ? refreshScoutWebPairingState(currentDirectory) : getScoutWebPairingState(currentDirectory);
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
- async function createOpenScoutWebServer(options) {
20968
- const shellTtl = options.shellStateCacheTtlMs ?? 15000;
20969
- const currentDirectory = options.currentDirectory;
20970
- const app = new Hono2;
20971
- const shellStateCache = createCachedSnapshot(loadOpenScoutWebShellState, shellTtl);
20972
- installScoutApiMiddleware(app, "openscout-web api");
20973
- app.get("/api/health", (c) => c.json({
20974
- ok: true,
20975
- surface: "openscout-web",
20976
- currentDirectory
20977
- }));
20978
- app.get("/api/pairing-state", async (c) => c.json(await loadPairingState(currentDirectory, false)));
20979
- app.get("/api/pairing-state/refresh", async (c) => c.json(await loadPairingState(currentDirectory, true)));
20980
- app.post("/api/pairing/control", async (c) => {
20981
- const { action } = await c.req.json();
20982
- const result = await controlScoutWebPairingService(action, currentDirectory);
20983
- shellStateCache.invalidate();
20984
- return c.json(result);
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
- app.delete("/api/pairing/peers/:fingerprint", async (c) => {
20987
- const fingerprint = c.req.param("fingerprint");
20988
- const removed = removeScoutPairingTrustedPeer(fingerprint);
20989
- if (!removed) {
20990
- return c.json({ error: "Peer not found" }, 404);
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
- return c.json({ ok: true });
20993
- });
20994
- app.get("/api/shell-state", async (c) => c.json(await shellStateCache.get()));
20995
- app.get("/api/shell-state/refresh", async (c) => c.json(await shellStateCache.refresh()));
20996
- app.get("/api/agents", (c) => c.json(queryAgents()));
20997
- app.get("/api/agents/:id/observe", async (c) => {
20998
- const payload = await loadAgentObservePayload(c.req.param("id"));
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
- const detail = queryWorkItemById(workId);
21022
- return detail ? c.json(detail) : c.json({ error: "not found" }, 404);
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
- app.get("/api/work", handleListWork);
21025
- app.get("/api/tasks", handleListWork);
21026
- app.get("/api/work/:id", handleWorkDetail);
21027
- app.get("/api/tasks/:id", handleWorkDetail);
21028
- app.get("/api/flights", (c) => {
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
- app.post("/api/mesh/announce", async (c) => {
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
- app.get("/api/user", (c) => {
21062
- return c.json({ name: resolveOperatorName() });
21063
- });
21064
- app.get("/api/onboarding/state", async (c) => {
21065
- const hasLocalConfig = localConfigExists();
21066
- const settings = await readOpenScoutSettings({ currentDirectory }).catch(() => null);
21067
- const configuredContextRoot = settings?.discovery.contextRoot ?? null;
21068
- const discoveryStart = configuredContextRoot ?? currentDirectory;
21069
- const projectRoot = await findNearestProjectRoot(discoveryStart).catch(() => null);
21070
- const hasProjectConfig = projectRoot !== null;
21071
- const userName = loadUserConfig().name?.trim() ?? "";
21072
- return c.json({
21073
- hasLocalConfig,
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
- await registerScoutWebAssets(app, {
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
- return { app, warmupCaches };
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 send(ws, data) {
21255
- if (ws.readyState === 1) {
21256
- ws.send(JSON.stringify(data));
21276
+ function attachSession(session, ws, cols, rows) {
21277
+ if (session.reapTimer) {
21278
+ clearTimeout(session.reapTimer);
21279
+ session.reapTimer = null;
21257
21280
  }
21258
- }
21259
- function resolveCwd(raw2) {
21260
- const home = process.env.HOME || "/tmp";
21261
- const expanded = (raw2 || home).replace(/^~/, home);
21262
- if (existsSync13(expanded))
21263
- return expanded;
21264
- try {
21265
- mkdirSync6(expanded, { recursive: true });
21266
- console.log(`[relay] Created missing cwd: ${expanded}`);
21267
- return expanded;
21268
- } catch {
21269
- console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
21270
- return home;
21281
+ session.ws = ws;
21282
+ if (cols && rows) {
21283
+ const c = Math.max(cols, 20);
21284
+ const r = Math.max(rows, 4);
21285
+ if (c !== session.cols || r !== session.rows) {
21286
+ session.pty.resize(c, r);
21287
+ session.cols = c;
21288
+ session.rows = r;
21289
+ }
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 findBin(name, envOverride) {
21274
- if (envOverride && process.env[envOverride])
21275
- return process.env[envOverride];
21276
- try {
21277
- return execSync2(`which ${name}`, { encoding: "utf8" }).trim() || null;
21278
- } catch {
21279
- return null;
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 findClaudeBin() {
21283
- return findBin("claude", "CLAUDE_BIN");
21284
- }
21285
- function findPiBin() {
21286
- return findBin("pi", "PI_BIN");
21287
- }
21288
- function tmuxSessionExists2(name) {
21289
- try {
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 (existsSync14(join21(siblingClientRoot, "index.html"))) {
21901
+ if (existsSync15(join21(siblingClientRoot, "index.html"))) {
21635
21902
  return siblingClientRoot;
21636
21903
  }
21637
21904
  const sourceDistClientRoot = resolve10(selfDir, "../dist/client");
21638
- if (existsSync14(join21(sourceDistClientRoot, "index.html"))) {
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://localhost:${server.port}`);
21684
- console.log(`Relay WebSocket -> ws://localhost:${server.port}`);
21951
+ console.log(`OpenScout Web -> http://${hostname2}:${server.port}`);
21952
+ console.log(`Relay WebSocket -> ws://${hostname2}:${server.port}`);
21685
21953
  warmupCaches();