@openscout/scout 0.2.58 → 0.2.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc.es-Bkj9Bkvx.js → arc.es-DgeMPL-I.js} +1 -1
- package/dist/client/assets/index-25TJx0iN.js +140 -0
- package/dist/client/assets/index-B1kglsyi.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/main.mjs +1275 -94
- package/dist/pair-supervisor.mjs +1079 -73
- package/dist/scout-control-plane-web.mjs +629 -461
- package/package.json +2 -2
- package/dist/client/assets/index-BDyu05Hi.js +0 -140
- package/dist/client/assets/index-pbcZpgCR.css +0 -1
|
@@ -8902,8 +8902,107 @@ var init_claude_stream_json = __esm(() => {
|
|
|
8902
8902
|
|
|
8903
8903
|
// packages/runtime/src/codex-app-server.ts
|
|
8904
8904
|
import { spawn as spawn4 } from "child_process";
|
|
8905
|
-
import {
|
|
8905
|
+
import { constants as constants4 } from "fs";
|
|
8906
|
+
import { access as access3, appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
8906
8907
|
import { delimiter as delimiter3, join as join14 } from "path";
|
|
8908
|
+
function normalizeCodexModelValue(value) {
|
|
8909
|
+
const trimmed = value?.trim();
|
|
8910
|
+
return trimmed ? trimmed : null;
|
|
8911
|
+
}
|
|
8912
|
+
function encodeCodexModelConfig(model) {
|
|
8913
|
+
return `model=${JSON.stringify(model)}`;
|
|
8914
|
+
}
|
|
8915
|
+
function parseCodexModelConfig(value) {
|
|
8916
|
+
const trimmed = value?.trim();
|
|
8917
|
+
if (!trimmed) {
|
|
8918
|
+
return null;
|
|
8919
|
+
}
|
|
8920
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
8921
|
+
if (separatorIndex <= 0) {
|
|
8922
|
+
return null;
|
|
8923
|
+
}
|
|
8924
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
8925
|
+
if (key !== "model") {
|
|
8926
|
+
return null;
|
|
8927
|
+
}
|
|
8928
|
+
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
|
8929
|
+
if (!rawValue) {
|
|
8930
|
+
return null;
|
|
8931
|
+
}
|
|
8932
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
8933
|
+
return rawValue.slice(1, -1) || null;
|
|
8934
|
+
}
|
|
8935
|
+
return rawValue;
|
|
8936
|
+
}
|
|
8937
|
+
function normalizeCodexAppServerLaunchArgs(launchArgs) {
|
|
8938
|
+
const args = Array.isArray(launchArgs) ? launchArgs.map((entry) => String(entry).trim()).filter(Boolean) : [];
|
|
8939
|
+
const normalized = [];
|
|
8940
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
8941
|
+
const current = args[index] ?? "";
|
|
8942
|
+
if (current === "--model" || current === "-m") {
|
|
8943
|
+
const model = normalizeCodexModelValue(args[index + 1]);
|
|
8944
|
+
if (model) {
|
|
8945
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
8946
|
+
index += 1;
|
|
8947
|
+
continue;
|
|
8948
|
+
}
|
|
8949
|
+
normalized.push(current);
|
|
8950
|
+
continue;
|
|
8951
|
+
}
|
|
8952
|
+
if (current.startsWith("--model=")) {
|
|
8953
|
+
const model = normalizeCodexModelValue(current.slice("--model=".length));
|
|
8954
|
+
if (model) {
|
|
8955
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
8956
|
+
continue;
|
|
8957
|
+
}
|
|
8958
|
+
}
|
|
8959
|
+
if (current.startsWith("-m=")) {
|
|
8960
|
+
const model = normalizeCodexModelValue(current.slice(3));
|
|
8961
|
+
if (model) {
|
|
8962
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
8963
|
+
continue;
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
8966
|
+
if (current === "-c" || current === "--config") {
|
|
8967
|
+
const next = args[index + 1];
|
|
8968
|
+
if (typeof next === "string") {
|
|
8969
|
+
const model = parseCodexModelConfig(next);
|
|
8970
|
+
normalized.push(current === "--config" ? "--config" : "-c", model ? encodeCodexModelConfig(model) : next);
|
|
8971
|
+
index += 1;
|
|
8972
|
+
continue;
|
|
8973
|
+
}
|
|
8974
|
+
}
|
|
8975
|
+
if (current.startsWith("--config=")) {
|
|
8976
|
+
const value = current.slice("--config=".length);
|
|
8977
|
+
const model = parseCodexModelConfig(value);
|
|
8978
|
+
normalized.push(model ? `--config=${encodeCodexModelConfig(model)}` : current);
|
|
8979
|
+
continue;
|
|
8980
|
+
}
|
|
8981
|
+
normalized.push(current);
|
|
8982
|
+
}
|
|
8983
|
+
return normalized;
|
|
8984
|
+
}
|
|
8985
|
+
function readCodexAppServerModelFromLaunchArgs(launchArgs) {
|
|
8986
|
+
const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
|
|
8987
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
8988
|
+
const current = normalized[index] ?? "";
|
|
8989
|
+
if (current === "-c" || current === "--config") {
|
|
8990
|
+
const model = parseCodexModelConfig(normalized[index + 1]);
|
|
8991
|
+
if (model) {
|
|
8992
|
+
return model;
|
|
8993
|
+
}
|
|
8994
|
+
index += 1;
|
|
8995
|
+
continue;
|
|
8996
|
+
}
|
|
8997
|
+
if (current.startsWith("--config=")) {
|
|
8998
|
+
const model = parseCodexModelConfig(current.slice("--config=".length));
|
|
8999
|
+
if (model) {
|
|
9000
|
+
return model;
|
|
9001
|
+
}
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
return null;
|
|
9005
|
+
}
|
|
8907
9006
|
function isCodexThreadGlobalMessage(message) {
|
|
8908
9007
|
const result = message.result;
|
|
8909
9008
|
const thread = result?.thread;
|
|
@@ -9706,6 +9805,13 @@ function isMissingCodexRolloutError2(error) {
|
|
|
9706
9805
|
const message = errorMessage3(error).toLowerCase();
|
|
9707
9806
|
return message.includes("no rollout found for thread id");
|
|
9708
9807
|
}
|
|
9808
|
+
function resolveCodexCompletionGraceMs() {
|
|
9809
|
+
const parsed = Number.parseInt(process.env.OPENSCOUT_CODEX_COMPLETION_GRACE_MS ?? "", 10);
|
|
9810
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
9811
|
+
return parsed;
|
|
9812
|
+
}
|
|
9813
|
+
return 60000;
|
|
9814
|
+
}
|
|
9709
9815
|
|
|
9710
9816
|
class CodexAppServerSession {
|
|
9711
9817
|
options;
|
|
@@ -9895,7 +10001,7 @@ class CodexAppServerSession {
|
|
|
9895
10001
|
systemPrompt: options.systemPrompt,
|
|
9896
10002
|
threadId: options.threadId ?? null,
|
|
9897
10003
|
requireExistingThread: options.requireExistingThread === true,
|
|
9898
|
-
launchArgs:
|
|
10004
|
+
launchArgs: normalizeCodexAppServerLaunchArgs(options.launchArgs)
|
|
9899
10005
|
});
|
|
9900
10006
|
}
|
|
9901
10007
|
async ensureStarted() {
|
|
@@ -9917,6 +10023,7 @@ class CodexAppServerSession {
|
|
|
9917
10023
|
await mkdir5(this.options.logsDirectory, { recursive: true });
|
|
9918
10024
|
await writeFile5(join14(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
|
|
9919
10025
|
const codexExecutable = await resolveCodexExecutable2();
|
|
10026
|
+
const launchArgs = normalizeCodexAppServerLaunchArgs(this.options.launchArgs);
|
|
9920
10027
|
const env = buildManagedAgentEnvironment({
|
|
9921
10028
|
agentName: this.options.agentName,
|
|
9922
10029
|
currentDirectory: this.options.cwd,
|
|
@@ -9927,7 +10034,8 @@ class CodexAppServerSession {
|
|
|
9927
10034
|
...buildScoutMcpCodexLaunchArgs({
|
|
9928
10035
|
currentDirectory: this.options.cwd,
|
|
9929
10036
|
env
|
|
9930
|
-
})
|
|
10037
|
+
}),
|
|
10038
|
+
...launchArgs
|
|
9931
10039
|
], {
|
|
9932
10040
|
cwd: this.options.cwd,
|
|
9933
10041
|
env,
|
|
@@ -10025,6 +10133,8 @@ class CodexAppServerSession {
|
|
|
10025
10133
|
turnId: "",
|
|
10026
10134
|
startedAt: Date.now(),
|
|
10027
10135
|
timer: null,
|
|
10136
|
+
graceTimer: null,
|
|
10137
|
+
timedOutAt: null,
|
|
10028
10138
|
messageOrder: [],
|
|
10029
10139
|
messageByItemId: new Map,
|
|
10030
10140
|
resolve: resolve5,
|
|
@@ -10032,20 +10142,43 @@ class CodexAppServerSession {
|
|
|
10032
10142
|
watchers: []
|
|
10033
10143
|
};
|
|
10034
10144
|
turn.timer = setTimeout(() => {
|
|
10035
|
-
this.
|
|
10036
|
-
return;
|
|
10037
|
-
});
|
|
10038
|
-
if (this.activeTurn === turn) {
|
|
10039
|
-
this.activeTurn = null;
|
|
10040
|
-
}
|
|
10041
|
-
for (const watcher of this.drainTurnWatchers(turn)) {
|
|
10042
|
-
watcher.reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
|
|
10043
|
-
}
|
|
10044
|
-
reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
|
|
10145
|
+
this.scheduleTurnTimeout(turn, timeoutMs);
|
|
10045
10146
|
}, timeoutMs);
|
|
10046
10147
|
this.activeTurn = turn;
|
|
10047
10148
|
return turn;
|
|
10048
10149
|
}
|
|
10150
|
+
buildTurnTimeoutError(timeoutMs) {
|
|
10151
|
+
return new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`);
|
|
10152
|
+
}
|
|
10153
|
+
scheduleTurnTimeout(turn, timeoutMs) {
|
|
10154
|
+
if (turn.timedOutAt !== null) {
|
|
10155
|
+
return;
|
|
10156
|
+
}
|
|
10157
|
+
turn.timedOutAt = Date.now();
|
|
10158
|
+
this.interrupt().catch(() => {
|
|
10159
|
+
return;
|
|
10160
|
+
});
|
|
10161
|
+
const graceMs = resolveCodexCompletionGraceMs();
|
|
10162
|
+
if (graceMs <= 0) {
|
|
10163
|
+
this.rejectTimedOutTurn(turn, timeoutMs);
|
|
10164
|
+
return;
|
|
10165
|
+
}
|
|
10166
|
+
turn.graceTimer = setTimeout(() => {
|
|
10167
|
+
this.rejectTimedOutTurn(turn, timeoutMs);
|
|
10168
|
+
}, graceMs);
|
|
10169
|
+
}
|
|
10170
|
+
rejectTimedOutTurn(turn, timeoutMs) {
|
|
10171
|
+
if (this.activeTurn !== turn) {
|
|
10172
|
+
this.clearActiveTurn(turn);
|
|
10173
|
+
return;
|
|
10174
|
+
}
|
|
10175
|
+
this.clearActiveTurn(turn);
|
|
10176
|
+
const error = this.buildTurnTimeoutError(timeoutMs);
|
|
10177
|
+
for (const watcher of this.drainTurnWatchers(turn)) {
|
|
10178
|
+
watcher.reject(error);
|
|
10179
|
+
}
|
|
10180
|
+
turn.reject(error);
|
|
10181
|
+
}
|
|
10049
10182
|
addTurnWatcher(turn, resolve5, reject, timeoutMs) {
|
|
10050
10183
|
const watcher = {
|
|
10051
10184
|
resolve: resolve5,
|
|
@@ -10079,6 +10212,9 @@ class CodexAppServerSession {
|
|
|
10079
10212
|
if (turn.timer) {
|
|
10080
10213
|
clearTimeout(turn.timer);
|
|
10081
10214
|
}
|
|
10215
|
+
if (turn.graceTimer) {
|
|
10216
|
+
clearTimeout(turn.graceTimer);
|
|
10217
|
+
}
|
|
10082
10218
|
if (this.activeTurn === turn) {
|
|
10083
10219
|
this.activeTurn = null;
|
|
10084
10220
|
}
|
|
@@ -11454,6 +11590,128 @@ function normalizeLocalAgentCapabilities(value) {
|
|
|
11454
11590
|
function normalizeLocalAgentLaunchArgs(value) {
|
|
11455
11591
|
return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
|
|
11456
11592
|
}
|
|
11593
|
+
function normalizeRequestedModel(value) {
|
|
11594
|
+
const trimmed = value?.trim();
|
|
11595
|
+
return trimmed ? trimmed : undefined;
|
|
11596
|
+
}
|
|
11597
|
+
function readClaudeLaunchModel(launchArgs) {
|
|
11598
|
+
for (let index = 0;index < launchArgs.length; index += 1) {
|
|
11599
|
+
const current = launchArgs[index] ?? "";
|
|
11600
|
+
if (current === "--model") {
|
|
11601
|
+
const next = launchArgs[index + 1]?.trim();
|
|
11602
|
+
return next || undefined;
|
|
11603
|
+
}
|
|
11604
|
+
if (current.startsWith("--model=")) {
|
|
11605
|
+
const next = current.slice("--model=".length).trim();
|
|
11606
|
+
return next || undefined;
|
|
11607
|
+
}
|
|
11608
|
+
}
|
|
11609
|
+
return;
|
|
11610
|
+
}
|
|
11611
|
+
function normalizeLaunchArgsForHarness(harness, value) {
|
|
11612
|
+
const normalized = normalizeLocalAgentLaunchArgs(value);
|
|
11613
|
+
if (harness === "codex") {
|
|
11614
|
+
return normalizeCodexAppServerLaunchArgs(normalized);
|
|
11615
|
+
}
|
|
11616
|
+
return normalized;
|
|
11617
|
+
}
|
|
11618
|
+
function readLaunchModelForHarness(harness, launchArgs) {
|
|
11619
|
+
if (harness === "codex") {
|
|
11620
|
+
return readCodexAppServerModelFromLaunchArgs(launchArgs) ?? undefined;
|
|
11621
|
+
}
|
|
11622
|
+
if (harness === "claude") {
|
|
11623
|
+
return readClaudeLaunchModel(launchArgs ?? []);
|
|
11624
|
+
}
|
|
11625
|
+
return;
|
|
11626
|
+
}
|
|
11627
|
+
function stripLaunchModelForHarness(harness, launchArgs) {
|
|
11628
|
+
if (harness === "codex") {
|
|
11629
|
+
const next = [];
|
|
11630
|
+
const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
|
|
11631
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
11632
|
+
const current = normalized[index] ?? "";
|
|
11633
|
+
if (current === "-c" || current === "--config") {
|
|
11634
|
+
const value = normalized[index + 1];
|
|
11635
|
+
if (readCodexAppServerModelFromLaunchArgs(value ? [current, value] : [current])) {
|
|
11636
|
+
index += 1;
|
|
11637
|
+
continue;
|
|
11638
|
+
}
|
|
11639
|
+
next.push(current);
|
|
11640
|
+
if (value) {
|
|
11641
|
+
next.push(value);
|
|
11642
|
+
index += 1;
|
|
11643
|
+
}
|
|
11644
|
+
continue;
|
|
11645
|
+
}
|
|
11646
|
+
if (current.startsWith("--config=")) {
|
|
11647
|
+
if (readCodexAppServerModelFromLaunchArgs([current])) {
|
|
11648
|
+
continue;
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
next.push(current);
|
|
11652
|
+
}
|
|
11653
|
+
return next;
|
|
11654
|
+
}
|
|
11655
|
+
if (harness === "claude") {
|
|
11656
|
+
const next = [];
|
|
11657
|
+
const normalized = normalizeLocalAgentLaunchArgs(launchArgs);
|
|
11658
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
11659
|
+
const current = normalized[index] ?? "";
|
|
11660
|
+
if (current === "--model") {
|
|
11661
|
+
index += 1;
|
|
11662
|
+
continue;
|
|
11663
|
+
}
|
|
11664
|
+
if (current.startsWith("--model=")) {
|
|
11665
|
+
continue;
|
|
11666
|
+
}
|
|
11667
|
+
next.push(current);
|
|
11668
|
+
}
|
|
11669
|
+
return next;
|
|
11670
|
+
}
|
|
11671
|
+
return normalizeLocalAgentLaunchArgs(launchArgs);
|
|
11672
|
+
}
|
|
11673
|
+
function buildLaunchArgsForRequestedModel(harness, model) {
|
|
11674
|
+
if (harness === "codex") {
|
|
11675
|
+
return normalizeCodexAppServerLaunchArgs(["--model", model]);
|
|
11676
|
+
}
|
|
11677
|
+
if (harness === "claude") {
|
|
11678
|
+
return ["--model", model];
|
|
11679
|
+
}
|
|
11680
|
+
return [];
|
|
11681
|
+
}
|
|
11682
|
+
function applyRequestedModelToLaunchArgs(harness, launchArgs, model) {
|
|
11683
|
+
const normalized = normalizeLaunchArgsForHarness(harness, launchArgs);
|
|
11684
|
+
const requestedModel = normalizeRequestedModel(model);
|
|
11685
|
+
if (!requestedModel) {
|
|
11686
|
+
return normalized;
|
|
11687
|
+
}
|
|
11688
|
+
return [
|
|
11689
|
+
...stripLaunchModelForHarness(harness, normalized),
|
|
11690
|
+
...buildLaunchArgsForRequestedModel(harness, requestedModel)
|
|
11691
|
+
];
|
|
11692
|
+
}
|
|
11693
|
+
function defaultHarnessForOverride(override, fallback = "claude") {
|
|
11694
|
+
return normalizeManagedHarness2(override.defaultHarness ?? override.runtime?.harness, fallback);
|
|
11695
|
+
}
|
|
11696
|
+
function launchArgsForOverrideHarness(override, harness) {
|
|
11697
|
+
const profileLaunchArgs = override.harnessProfiles?.[harness]?.launchArgs;
|
|
11698
|
+
if (profileLaunchArgs) {
|
|
11699
|
+
return normalizeLaunchArgsForHarness(harness, profileLaunchArgs);
|
|
11700
|
+
}
|
|
11701
|
+
if (harness === defaultHarnessForOverride(override, harness)) {
|
|
11702
|
+
return normalizeLaunchArgsForHarness(harness, override.launchArgs);
|
|
11703
|
+
}
|
|
11704
|
+
return [];
|
|
11705
|
+
}
|
|
11706
|
+
function overrideHarnessProfile(override, harness) {
|
|
11707
|
+
const profile = override.harnessProfiles?.[harness];
|
|
11708
|
+
return {
|
|
11709
|
+
cwd: normalizeProjectPath(profile?.cwd || override.runtime?.cwd || override.projectRoot || process.cwd()),
|
|
11710
|
+
transport: normalizeLocalAgentTransport(profile?.transport ?? override.runtime?.transport, harness),
|
|
11711
|
+
sessionId: normalizeTmuxSessionName2(profile?.sessionId, `${override.agentId}-${harness}`),
|
|
11712
|
+
launchArgs: launchArgsForOverrideHarness(override, harness)
|
|
11713
|
+
};
|
|
11714
|
+
}
|
|
11457
11715
|
function normalizeManagedHarness2(value, fallback) {
|
|
11458
11716
|
return value === "codex" ? "codex" : value === "claude" ? "claude" : fallback;
|
|
11459
11717
|
}
|
|
@@ -11469,7 +11727,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
11469
11727
|
cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
|
|
11470
11728
|
transport: normalizeLocalAgentTransport(profile.transport, harness),
|
|
11471
11729
|
sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
|
|
11472
|
-
launchArgs:
|
|
11730
|
+
launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
|
|
11473
11731
|
};
|
|
11474
11732
|
}
|
|
11475
11733
|
const runtimeHarness = normalizeManagedHarness2(record.harness, defaultHarness);
|
|
@@ -11478,7 +11736,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
11478
11736
|
cwd: normalizeProjectPath(record.cwd || process.cwd()),
|
|
11479
11737
|
transport: normalizeLocalAgentTransport(record.transport, runtimeHarness),
|
|
11480
11738
|
sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${runtimeHarness}`),
|
|
11481
|
-
launchArgs:
|
|
11739
|
+
launchArgs: normalizeLaunchArgsForHarness(runtimeHarness, record.launchArgs)
|
|
11482
11740
|
};
|
|
11483
11741
|
}
|
|
11484
11742
|
if (!nextProfiles[defaultHarness]) {
|
|
@@ -11486,7 +11744,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
11486
11744
|
cwd: normalizeProjectPath(record.cwd || process.cwd()),
|
|
11487
11745
|
transport: normalizeLocalAgentTransport(record.transport, defaultHarness),
|
|
11488
11746
|
sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${defaultHarness}`),
|
|
11489
|
-
launchArgs:
|
|
11747
|
+
launchArgs: normalizeLaunchArgsForHarness(defaultHarness, record.launchArgs)
|
|
11490
11748
|
};
|
|
11491
11749
|
}
|
|
11492
11750
|
for (const harness of ["claude", "codex"]) {
|
|
@@ -11498,7 +11756,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
11498
11756
|
cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
|
|
11499
11757
|
transport: normalizeLocalAgentTransport(profile.transport, harness),
|
|
11500
11758
|
sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
|
|
11501
|
-
launchArgs:
|
|
11759
|
+
launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
|
|
11502
11760
|
};
|
|
11503
11761
|
}
|
|
11504
11762
|
return nextProfiles;
|
|
@@ -11522,14 +11780,14 @@ function recordForHarness(record, harnessOverride) {
|
|
|
11522
11780
|
tmuxSession: fallbackSessionId,
|
|
11523
11781
|
cwd: fallbackCwd,
|
|
11524
11782
|
transport: fallbackTransport,
|
|
11525
|
-
launchArgs:
|
|
11783
|
+
launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs),
|
|
11526
11784
|
harnessProfiles: {
|
|
11527
11785
|
...normalized.harnessProfiles,
|
|
11528
11786
|
[selectedHarness]: {
|
|
11529
11787
|
cwd: fallbackCwd,
|
|
11530
11788
|
transport: fallbackTransport,
|
|
11531
11789
|
sessionId: fallbackSessionId,
|
|
11532
|
-
launchArgs:
|
|
11790
|
+
launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs)
|
|
11533
11791
|
}
|
|
11534
11792
|
}
|
|
11535
11793
|
};
|
|
@@ -11571,7 +11829,7 @@ function normalizeLocalAgentRecord(agentId, record) {
|
|
|
11571
11829
|
harnessProfiles,
|
|
11572
11830
|
transport: activeProfile?.transport ?? normalizeLocalAgentTransport(record.transport, harness),
|
|
11573
11831
|
capabilities: normalizeLocalAgentCapabilities(record.capabilities),
|
|
11574
|
-
launchArgs: activeProfile?.launchArgs ??
|
|
11832
|
+
launchArgs: activeProfile?.launchArgs ?? normalizeLaunchArgsForHarness(harness, record.launchArgs)
|
|
11575
11833
|
};
|
|
11576
11834
|
}
|
|
11577
11835
|
function localAgentStatusFromRecord(agentId, record, source) {
|
|
@@ -11640,7 +11898,7 @@ function relayAgentOverrideFromLocalAgentRecord(agentId, record, existing) {
|
|
|
11640
11898
|
source: existing?.source && existing.source !== "inferred" ? existing.source : "manual",
|
|
11641
11899
|
startedAt: normalizedRecord.startedAt,
|
|
11642
11900
|
systemPrompt: normalizedRecord.systemPrompt,
|
|
11643
|
-
launchArgs:
|
|
11901
|
+
launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs),
|
|
11644
11902
|
capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
|
|
11645
11903
|
defaultHarness: normalizeManagedHarness2(normalizedRecord.defaultHarness, "claude"),
|
|
11646
11904
|
harnessProfiles: normalizedRecord.harnessProfiles,
|
|
@@ -11665,7 +11923,7 @@ function buildLocalAgentConfigState(agentId, record) {
|
|
|
11665
11923
|
sessionId: record.tmuxSession,
|
|
11666
11924
|
wakePolicy: "on_demand"
|
|
11667
11925
|
},
|
|
11668
|
-
launchArgs:
|
|
11926
|
+
launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs),
|
|
11669
11927
|
capabilities: normalizeLocalAgentCapabilities(record.capabilities),
|
|
11670
11928
|
applyMode: "restart",
|
|
11671
11929
|
templateHint: LOCAL_AGENT_SYSTEM_PROMPT_TEMPLATE_HINT
|
|
@@ -11791,7 +12049,7 @@ function buildCodexAgentSessionOptions(agentName, record, systemPrompt) {
|
|
|
11791
12049
|
systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "codex_app_server" }),
|
|
11792
12050
|
runtimeDirectory: relayAgentRuntimeDirectory(agentName),
|
|
11793
12051
|
logsDirectory: relayAgentLogsDirectory(agentName),
|
|
11794
|
-
launchArgs:
|
|
12052
|
+
launchArgs: normalizeLaunchArgsForHarness("codex", record.launchArgs)
|
|
11795
12053
|
};
|
|
11796
12054
|
}
|
|
11797
12055
|
function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
|
|
@@ -11802,7 +12060,7 @@ function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
|
|
|
11802
12060
|
systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "claude_stream_json" }),
|
|
11803
12061
|
runtimeDirectory: relayAgentRuntimeDirectory(agentName),
|
|
11804
12062
|
logsDirectory: relayAgentLogsDirectory(agentName),
|
|
11805
|
-
launchArgs:
|
|
12063
|
+
launchArgs: normalizeLaunchArgsForHarness("claude", record.launchArgs)
|
|
11806
12064
|
};
|
|
11807
12065
|
}
|
|
11808
12066
|
function endpointMetadataString(endpoint, key) {
|
|
@@ -12007,7 +12265,7 @@ function buildLocalAgentBootstrapPrompt(_harness, _systemPrompt, initialMessage)
|
|
|
12007
12265
|
return initialMessage;
|
|
12008
12266
|
}
|
|
12009
12267
|
function buildLocalAgentLaunchCommand(agentName, record, projectPath, promptFile, workerScript) {
|
|
12010
|
-
const extraArgs = shellQuoteArguments(
|
|
12268
|
+
const extraArgs = shellQuoteArguments(normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs));
|
|
12011
12269
|
if (normalizeLocalAgentHarness(record.harness) === "codex") {
|
|
12012
12270
|
return `exec bash ${JSON.stringify(workerScript ?? join16(relayAgentRuntimeDirectory(agentName), "codex-worker.sh"))}`;
|
|
12013
12271
|
}
|
|
@@ -12379,6 +12637,7 @@ async function startLocalAgent(input) {
|
|
|
12379
12637
|
const effectiveHarness = normalizeManagedHarness2(preferredHarness ?? configDefaultHarness, "claude");
|
|
12380
12638
|
const transport = normalizeLocalAgentTransport(undefined, effectiveHarness);
|
|
12381
12639
|
const sessionId = normalizeTmuxSessionName2(undefined, `${instance.id}-${effectiveHarness}`);
|
|
12640
|
+
const launchArgs = applyRequestedModelToLaunchArgs(effectiveHarness, [], input.model);
|
|
12382
12641
|
const existingForInstance = overrides[instance.id];
|
|
12383
12642
|
if (existingForInstance && normalizeProjectPath(existingForInstance.projectRoot) !== projectRoot) {
|
|
12384
12643
|
throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
|
|
@@ -12392,13 +12651,14 @@ async function startLocalAgent(input) {
|
|
|
12392
12651
|
projectConfigPath: coldProjectConfigPath,
|
|
12393
12652
|
source: "manual",
|
|
12394
12653
|
startedAt: nowSeconds2(),
|
|
12654
|
+
launchArgs,
|
|
12395
12655
|
defaultHarness: effectiveHarness,
|
|
12396
12656
|
harnessProfiles: {
|
|
12397
12657
|
[effectiveHarness]: {
|
|
12398
12658
|
cwd: effectiveCwd ?? projectRoot,
|
|
12399
12659
|
transport,
|
|
12400
12660
|
sessionId,
|
|
12401
|
-
launchArgs
|
|
12661
|
+
launchArgs
|
|
12402
12662
|
}
|
|
12403
12663
|
},
|
|
12404
12664
|
runtime: {
|
|
@@ -12419,6 +12679,9 @@ async function startLocalAgent(input) {
|
|
|
12419
12679
|
throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
|
|
12420
12680
|
}
|
|
12421
12681
|
const resolvedHarness = preferredHarness ?? matchingOverride.runtime?.harness;
|
|
12682
|
+
const nextHarness = normalizeManagedHarness2(resolvedHarness, "claude");
|
|
12683
|
+
const nextDefaultHarness = preferredHarness ? normalizeManagedHarness2(preferredHarness, "claude") : defaultHarnessForOverride(matchingOverride, "claude");
|
|
12684
|
+
const nextLaunchArgs = applyRequestedModelToLaunchArgs(nextHarness, launchArgsForOverrideHarness(matchingOverride, nextHarness), input.model);
|
|
12422
12685
|
overrides[instance.id] = {
|
|
12423
12686
|
agentId: instance.id,
|
|
12424
12687
|
definitionId: requestedDefinitionId,
|
|
@@ -12429,10 +12692,16 @@ async function startLocalAgent(input) {
|
|
|
12429
12692
|
source: "manual",
|
|
12430
12693
|
startedAt: matchingOverride.startedAt ?? nowSeconds2(),
|
|
12431
12694
|
systemPrompt: matchingOverride.systemPrompt,
|
|
12432
|
-
launchArgs: matchingOverride.launchArgs,
|
|
12695
|
+
launchArgs: nextHarness === nextDefaultHarness ? nextLaunchArgs : matchingOverride.launchArgs,
|
|
12433
12696
|
capabilities: matchingOverride.capabilities,
|
|
12434
|
-
defaultHarness:
|
|
12435
|
-
harnessProfiles:
|
|
12697
|
+
defaultHarness: nextDefaultHarness,
|
|
12698
|
+
harnessProfiles: {
|
|
12699
|
+
...matchingOverride.harnessProfiles ?? {},
|
|
12700
|
+
[nextHarness]: {
|
|
12701
|
+
...overrideHarnessProfile(matchingOverride, nextHarness),
|
|
12702
|
+
launchArgs: nextLaunchArgs
|
|
12703
|
+
}
|
|
12704
|
+
},
|
|
12436
12705
|
runtime: {
|
|
12437
12706
|
cwd: effectiveCwd ?? matchingOverride.runtime?.cwd ?? matchingProjectRoot,
|
|
12438
12707
|
harness: resolvedHarness,
|
|
@@ -12444,6 +12713,22 @@ async function startLocalAgent(input) {
|
|
|
12444
12713
|
await writeRelayAgentOverrides(overrides);
|
|
12445
12714
|
targetAgentId = instance.id;
|
|
12446
12715
|
} else {
|
|
12716
|
+
if (input.model) {
|
|
12717
|
+
const existingHarness = normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness ?? matchingOverride.runtime?.harness, "claude");
|
|
12718
|
+
const nextLaunchArgs = applyRequestedModelToLaunchArgs(existingHarness, launchArgsForOverrideHarness(matchingOverride, existingHarness), input.model);
|
|
12719
|
+
overrides[matchingAgentId] = {
|
|
12720
|
+
...matchingOverride,
|
|
12721
|
+
launchArgs: existingHarness === defaultHarnessForOverride(matchingOverride, existingHarness) ? nextLaunchArgs : matchingOverride.launchArgs,
|
|
12722
|
+
harnessProfiles: {
|
|
12723
|
+
...matchingOverride.harnessProfiles ?? {},
|
|
12724
|
+
[existingHarness]: {
|
|
12725
|
+
...overrideHarnessProfile(matchingOverride, existingHarness),
|
|
12726
|
+
launchArgs: nextLaunchArgs
|
|
12727
|
+
}
|
|
12728
|
+
}
|
|
12729
|
+
};
|
|
12730
|
+
await writeRelayAgentOverrides(overrides);
|
|
12731
|
+
}
|
|
12447
12732
|
targetAgentId = matchingAgentId;
|
|
12448
12733
|
}
|
|
12449
12734
|
await ensureLocalAgentBindingOnline(targetAgentId, process.env.OPENSCOUT_NODE_ID ?? "local", {
|
|
@@ -12556,6 +12841,7 @@ function readPersistedClaudeSessionId(agentName) {
|
|
|
12556
12841
|
}
|
|
12557
12842
|
function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
12558
12843
|
const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
|
|
12844
|
+
const configuredModel = readLaunchModelForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs);
|
|
12559
12845
|
const definitionId = normalizedRecord.definitionId ?? agentId;
|
|
12560
12846
|
const displayName = titleCaseLocalAgentName(definitionId);
|
|
12561
12847
|
const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
|
|
@@ -12580,7 +12866,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
12580
12866
|
defaultSelector: instance.defaultSelector,
|
|
12581
12867
|
nodeQualifier: instance.nodeQualifier,
|
|
12582
12868
|
workspaceQualifier: instance.workspaceQualifier,
|
|
12583
|
-
branch: instance.branch
|
|
12869
|
+
branch: instance.branch,
|
|
12870
|
+
...configuredModel ? { model: configuredModel } : {}
|
|
12584
12871
|
}
|
|
12585
12872
|
},
|
|
12586
12873
|
agent: {
|
|
@@ -12607,7 +12894,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
12607
12894
|
defaultSelector: instance.defaultSelector,
|
|
12608
12895
|
nodeQualifier: instance.nodeQualifier,
|
|
12609
12896
|
workspaceQualifier: instance.workspaceQualifier,
|
|
12610
|
-
branch: instance.branch
|
|
12897
|
+
branch: instance.branch,
|
|
12898
|
+
...configuredModel ? { model: configuredModel } : {}
|
|
12611
12899
|
},
|
|
12612
12900
|
agentClass: "general",
|
|
12613
12901
|
capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
|
|
@@ -12640,6 +12928,7 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
12640
12928
|
nodeQualifier: instance.nodeQualifier,
|
|
12641
12929
|
workspaceQualifier: instance.workspaceQualifier,
|
|
12642
12930
|
branch: instance.branch,
|
|
12931
|
+
...configuredModel ? { model: configuredModel } : {},
|
|
12643
12932
|
...externalSessionId ? { externalSessionId } : {}
|
|
12644
12933
|
}
|
|
12645
12934
|
}
|
|
@@ -12841,11 +13130,11 @@ var init_local_agents = __esm(async () => {
|
|
|
12841
13130
|
|
|
12842
13131
|
// packages/web/server/index.ts
|
|
12843
13132
|
import { existsSync as existsSync15 } from "fs";
|
|
12844
|
-
import { dirname as
|
|
12845
|
-
import { fileURLToPath as
|
|
13133
|
+
import { dirname as dirname14, join as join20, resolve as resolve11 } from "path";
|
|
13134
|
+
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
12846
13135
|
|
|
12847
13136
|
// packages/web/server/create-openscout-web-server.ts
|
|
12848
|
-
import { existsSync as
|
|
13137
|
+
import { existsSync as existsSync13, rmSync as rmSync3 } from "fs";
|
|
12849
13138
|
import { dirname as dirname12, resolve as resolve9 } from "path";
|
|
12850
13139
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
12851
13140
|
|
|
@@ -15745,6 +16034,12 @@ function transientBrokerWorkingStatusPredicate(alias) {
|
|
|
15745
16034
|
AND ${alias}.body LIKE '% is working.'
|
|
15746
16035
|
)`;
|
|
15747
16036
|
}
|
|
16037
|
+
function staleFlightActivityPredicate(alias) {
|
|
16038
|
+
return `NOT (
|
|
16039
|
+
${alias}.kind = 'flight_updated'
|
|
16040
|
+
AND COALESCE(${alias}.summary, '') LIKE 'Stale running flight reconciled:%'
|
|
16041
|
+
)`;
|
|
16042
|
+
}
|
|
15748
16043
|
function workPhaseFromFlightState(state) {
|
|
15749
16044
|
switch (state) {
|
|
15750
16045
|
case "queued":
|
|
@@ -15908,10 +16203,7 @@ function queryActivity(limit = 60) {
|
|
|
15908
16203
|
FROM activity_items ai
|
|
15909
16204
|
LEFT JOIN actors ac ON ac.id = ai.actor_id
|
|
15910
16205
|
WHERE ai.kind != 'ask_replied'
|
|
15911
|
-
AND
|
|
15912
|
-
ai.kind = 'flight_updated'
|
|
15913
|
-
AND COALESCE(ai.summary, '') LIKE 'Stale running flight reconciled:%'
|
|
15914
|
-
)
|
|
16206
|
+
AND ${staleFlightActivityPredicate("ai")}
|
|
15915
16207
|
ORDER BY ai.ts DESC
|
|
15916
16208
|
LIMIT ?`).all(limit);
|
|
15917
16209
|
const items = rows.map((r) => ({
|
|
@@ -16637,6 +16929,7 @@ function queryFleetActivity(opts) {
|
|
|
16637
16929
|
filters.push("ai.conversation_id = ?");
|
|
16638
16930
|
params.push(opts.conversationId);
|
|
16639
16931
|
}
|
|
16932
|
+
const scopedFilters = sqlJoinClauses(filters, "OR");
|
|
16640
16933
|
const sql = `SELECT
|
|
16641
16934
|
ai.id,
|
|
16642
16935
|
ai.kind,
|
|
@@ -16655,7 +16948,10 @@ function queryFleetActivity(opts) {
|
|
|
16655
16948
|
ai.session_id
|
|
16656
16949
|
FROM activity_items ai
|
|
16657
16950
|
LEFT JOIN actors ac ON ac.id = ai.actor_id
|
|
16658
|
-
${sqlWhereClause(
|
|
16951
|
+
${sqlWhereClause([
|
|
16952
|
+
staleFlightActivityPredicate("ai"),
|
|
16953
|
+
scopedFilters ? `(${scopedFilters})` : null
|
|
16954
|
+
])}
|
|
16659
16955
|
ORDER BY ai.ts DESC
|
|
16660
16956
|
LIMIT ?`;
|
|
16661
16957
|
const rows = db().prepare(sql).all(...params, opts?.limit ?? 80);
|
|
@@ -16738,6 +17034,10 @@ function queryFleetAskRows(requesterIds, limit) {
|
|
|
16738
17034
|
)
|
|
16739
17035
|
LEFT JOIN collaboration_records cr ON cr.id = inv.collaboration_record_id
|
|
16740
17036
|
WHERE inv.requester_id IN (${requesterClause})
|
|
17037
|
+
AND NOT (
|
|
17038
|
+
COALESCE(f.state, '') = 'failed'
|
|
17039
|
+
AND COALESCE(f.error, '') LIKE 'Stale running flight reconciled:%'
|
|
17040
|
+
)
|
|
16741
17041
|
ORDER BY COALESCE(f.completed_at, f.started_at, inv.created_at) DESC
|
|
16742
17042
|
LIMIT ?`).all(...requesterIds, limit);
|
|
16743
17043
|
}
|
|
@@ -21055,415 +21355,6 @@ init_setup();
|
|
|
21055
21355
|
init_support_paths();
|
|
21056
21356
|
init_claude_stream_json();
|
|
21057
21357
|
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);
|
|
21071
|
-
}
|
|
21072
|
-
function send(ws, data) {
|
|
21073
|
-
if (ws.readyState === 1) {
|
|
21074
|
-
ws.send(JSON.stringify(data));
|
|
21075
|
-
}
|
|
21076
|
-
}
|
|
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
|
-
}
|
|
21090
|
-
}
|
|
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
|
-
}
|
|
21099
|
-
}
|
|
21100
|
-
function findClaudeBin() {
|
|
21101
|
-
return findBin("claude", "CLAUDE_BIN");
|
|
21102
|
-
}
|
|
21103
|
-
function findPiBin() {
|
|
21104
|
-
return findBin("pi", "PI_BIN");
|
|
21105
|
-
}
|
|
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;
|
|
21112
|
-
}
|
|
21113
|
-
}
|
|
21114
|
-
function bootstrapFiles(cwd, files, sessionId) {
|
|
21115
|
-
for (const [relPath, content] of Object.entries(files)) {
|
|
21116
|
-
const absPath = join19(cwd, relPath);
|
|
21117
|
-
if (!existsSync13(absPath)) {
|
|
21118
|
-
try {
|
|
21119
|
-
const dir = pathDirname(absPath);
|
|
21120
|
-
if (!existsSync13(dir))
|
|
21121
|
-
mkdirSync6(dir, { recursive: true });
|
|
21122
|
-
writeFileSync6(absPath, content, "utf-8");
|
|
21123
|
-
console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
|
|
21124
|
-
} catch (err) {
|
|
21125
|
-
console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
|
|
21126
|
-
}
|
|
21127
|
-
}
|
|
21128
|
-
}
|
|
21129
|
-
}
|
|
21130
|
-
function spawnTmuxSession(tmuxName, cols, rows, cwd, claudeBin, claudeArgs, env) {
|
|
21131
|
-
const exists = tmuxSessionExists2(tmuxName);
|
|
21132
|
-
if (!exists) {
|
|
21133
|
-
const shellCmd = [claudeBin, ...claudeArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
|
|
21134
|
-
execSync2(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
|
|
21135
|
-
console.log(`[relay] Created tmux session: ${tmuxName}`);
|
|
21136
|
-
} else {
|
|
21137
|
-
try {
|
|
21138
|
-
execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
|
|
21139
|
-
} catch {}
|
|
21140
|
-
console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
|
|
21141
|
-
}
|
|
21142
|
-
return pty.spawn("tmux", ["attach", "-t", tmuxName], {
|
|
21143
|
-
name: "xterm-256color",
|
|
21144
|
-
cols,
|
|
21145
|
-
rows,
|
|
21146
|
-
cwd,
|
|
21147
|
-
env
|
|
21148
|
-
});
|
|
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;
|
|
21165
|
-
}
|
|
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;
|
|
21173
|
-
}
|
|
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
|
|
21242
|
-
};
|
|
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);
|
|
21248
|
-
}
|
|
21249
|
-
if (session.ws && session.ws.readyState === 1) {
|
|
21250
|
-
send(session.ws, { type: "terminal:data", data });
|
|
21251
|
-
}
|
|
21252
|
-
});
|
|
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}`);
|
|
21266
|
-
}
|
|
21267
|
-
if (session.ws) {
|
|
21268
|
-
send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
|
|
21269
|
-
}
|
|
21270
|
-
scheduleReap(session, 1e4);
|
|
21271
|
-
});
|
|
21272
|
-
sessions3.set(id, session);
|
|
21273
|
-
console.log(`[relay] Session ${id} created (${cols}x${rows})`);
|
|
21274
|
-
return session;
|
|
21275
|
-
}
|
|
21276
|
-
function attachSession(session, ws, cols, rows) {
|
|
21277
|
-
if (session.reapTimer) {
|
|
21278
|
-
clearTimeout(session.reapTimer);
|
|
21279
|
-
session.reapTimer = null;
|
|
21280
|
-
}
|
|
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 });
|
|
21295
|
-
}
|
|
21296
|
-
console.log(`[relay] Session ${session.id} reconnected`);
|
|
21297
|
-
}
|
|
21298
|
-
function detachSession(session) {
|
|
21299
|
-
session.ws = null;
|
|
21300
|
-
if (session.exited) {
|
|
21301
|
-
scheduleReap(session, 5000);
|
|
21302
|
-
} else {
|
|
21303
|
-
scheduleReap(session, session.orphanTTL);
|
|
21304
|
-
console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
|
|
21305
|
-
}
|
|
21306
|
-
}
|
|
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);
|
|
21315
|
-
}
|
|
21316
|
-
function destroy(sessionId) {
|
|
21317
|
-
const session = sessions3.get(sessionId);
|
|
21318
|
-
if (!session)
|
|
21319
|
-
return;
|
|
21320
|
-
if (session.reapTimer)
|
|
21321
|
-
clearTimeout(session.reapTimer);
|
|
21322
|
-
try {
|
|
21323
|
-
session.pty.kill();
|
|
21324
|
-
} catch {}
|
|
21325
|
-
sessions3.delete(sessionId);
|
|
21326
|
-
if (session.backend === "tmux") {
|
|
21327
|
-
console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
|
|
21328
|
-
} else {
|
|
21329
|
-
console.log(`[relay] Session ${sessionId} destroyed`);
|
|
21330
|
-
}
|
|
21331
|
-
}
|
|
21332
|
-
|
|
21333
|
-
// packages/web/server/relay.ts
|
|
21334
|
-
import { mkdir as mkdir7 } from "fs/promises";
|
|
21335
|
-
import { join as join20 } from "path";
|
|
21336
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
21337
|
-
var UPLOAD_DIR = "/tmp/scout-uploads";
|
|
21338
|
-
async function handleRelayUpload(req) {
|
|
21339
|
-
const { name, data } = await req.json();
|
|
21340
|
-
if (!name || !data) {
|
|
21341
|
-
return Response.json({ error: "Missing name or data" }, { status: 400 });
|
|
21342
|
-
}
|
|
21343
|
-
await mkdir7(UPLOAD_DIR, { recursive: true });
|
|
21344
|
-
const filename = `${randomUUID3()}-${name}`;
|
|
21345
|
-
const filepath = join20(UPLOAD_DIR, filename);
|
|
21346
|
-
await Bun.write(filepath, Buffer.from(data, "base64"));
|
|
21347
|
-
return Response.json({ path: filepath });
|
|
21348
|
-
}
|
|
21349
|
-
var _pendingCommand = null;
|
|
21350
|
-
function queueTerminalCommand(cmd) {
|
|
21351
|
-
for (const [, session] of sessions3) {
|
|
21352
|
-
if (!session.exited) {
|
|
21353
|
-
session.pty.write(cmd + `
|
|
21354
|
-
`);
|
|
21355
|
-
return;
|
|
21356
|
-
}
|
|
21357
|
-
}
|
|
21358
|
-
_pendingCommand = cmd;
|
|
21359
|
-
}
|
|
21360
|
-
function drainPendingCommand() {
|
|
21361
|
-
const cmd = _pendingCommand;
|
|
21362
|
-
_pendingCommand = null;
|
|
21363
|
-
return cmd;
|
|
21364
|
-
}
|
|
21365
|
-
function asRelay(ws) {
|
|
21366
|
-
return ws;
|
|
21367
|
-
}
|
|
21368
|
-
function parseMessage(raw2) {
|
|
21369
|
-
try {
|
|
21370
|
-
const msg = JSON.parse(raw2);
|
|
21371
|
-
if (typeof msg.type === "string")
|
|
21372
|
-
return msg;
|
|
21373
|
-
} catch {}
|
|
21374
|
-
return null;
|
|
21375
|
-
}
|
|
21376
|
-
var relayWebSocket = {
|
|
21377
|
-
open(_ws) {},
|
|
21378
|
-
message(ws, raw2) {
|
|
21379
|
-
const msg = parseMessage(typeof raw2 === "string" ? raw2 : raw2.toString());
|
|
21380
|
-
if (!msg)
|
|
21381
|
-
return;
|
|
21382
|
-
const rws = asRelay(ws);
|
|
21383
|
-
switch (msg.type) {
|
|
21384
|
-
case "session:init": {
|
|
21385
|
-
if (ws.data.sessionId) {
|
|
21386
|
-
const prev = sessions3.get(ws.data.sessionId);
|
|
21387
|
-
if (prev)
|
|
21388
|
-
detachSession(prev);
|
|
21389
|
-
}
|
|
21390
|
-
const session = createSession(rws, msg);
|
|
21391
|
-
if (!session)
|
|
21392
|
-
break;
|
|
21393
|
-
ws.data.sessionId = session.id;
|
|
21394
|
-
send(rws, { type: "session:ready", sessionId: session.id });
|
|
21395
|
-
const pending = drainPendingCommand();
|
|
21396
|
-
if (pending)
|
|
21397
|
-
setTimeout(() => session.pty.write(pending + `
|
|
21398
|
-
`), 400);
|
|
21399
|
-
break;
|
|
21400
|
-
}
|
|
21401
|
-
case "session:reconnect": {
|
|
21402
|
-
const existing = sessions3.get(msg.sessionId);
|
|
21403
|
-
if (existing && !existing.exited) {
|
|
21404
|
-
if (ws.data.sessionId && ws.data.sessionId !== msg.sessionId) {
|
|
21405
|
-
const prev = sessions3.get(ws.data.sessionId);
|
|
21406
|
-
if (prev)
|
|
21407
|
-
detachSession(prev);
|
|
21408
|
-
}
|
|
21409
|
-
if (existing.ws && existing.ws !== rws) {
|
|
21410
|
-
send(existing.ws, { type: "session:detached" });
|
|
21411
|
-
}
|
|
21412
|
-
ws.data.sessionId = existing.id;
|
|
21413
|
-
attachSession(existing, rws, msg.cols, msg.rows);
|
|
21414
|
-
send(rws, {
|
|
21415
|
-
type: "session:ready",
|
|
21416
|
-
sessionId: existing.id,
|
|
21417
|
-
reconnected: true
|
|
21418
|
-
});
|
|
21419
|
-
const pending = drainPendingCommand();
|
|
21420
|
-
if (pending)
|
|
21421
|
-
setTimeout(() => existing.pty.write(pending + `
|
|
21422
|
-
`), 400);
|
|
21423
|
-
} else {
|
|
21424
|
-
send(rws, { type: "session:expired", sessionId: msg.sessionId });
|
|
21425
|
-
}
|
|
21426
|
-
break;
|
|
21427
|
-
}
|
|
21428
|
-
case "terminal:input": {
|
|
21429
|
-
if (!ws.data.sessionId)
|
|
21430
|
-
return;
|
|
21431
|
-
const session = sessions3.get(ws.data.sessionId);
|
|
21432
|
-
if (session && !session.exited) {
|
|
21433
|
-
session.pty.write(msg.data);
|
|
21434
|
-
}
|
|
21435
|
-
break;
|
|
21436
|
-
}
|
|
21437
|
-
case "terminal:resize": {
|
|
21438
|
-
if (!ws.data.sessionId)
|
|
21439
|
-
return;
|
|
21440
|
-
const session = sessions3.get(ws.data.sessionId);
|
|
21441
|
-
if (session && !session.exited) {
|
|
21442
|
-
const cols = Math.max(msg.cols || 80, 20);
|
|
21443
|
-
const rows = Math.max(msg.rows || 24, 4);
|
|
21444
|
-
session.pty.resize(cols, rows);
|
|
21445
|
-
session.cols = cols;
|
|
21446
|
-
session.rows = rows;
|
|
21447
|
-
}
|
|
21448
|
-
break;
|
|
21449
|
-
}
|
|
21450
|
-
}
|
|
21451
|
-
},
|
|
21452
|
-
close(ws) {
|
|
21453
|
-
if (ws.data.sessionId) {
|
|
21454
|
-
const session = sessions3.get(ws.data.sessionId);
|
|
21455
|
-
if (session)
|
|
21456
|
-
detachSession(session);
|
|
21457
|
-
ws.data.sessionId = null;
|
|
21458
|
-
}
|
|
21459
|
-
}
|
|
21460
|
-
};
|
|
21461
|
-
function destroyAllRelaySessions() {
|
|
21462
|
-
for (const [id] of sessions3)
|
|
21463
|
-
destroy(id);
|
|
21464
|
-
}
|
|
21465
|
-
|
|
21466
|
-
// packages/web/server/create-openscout-web-server.ts
|
|
21467
21358
|
function parseOptionalPositiveInt(value, fallback) {
|
|
21468
21359
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
21469
21360
|
return fallback;
|
|
@@ -21518,7 +21409,7 @@ function resolveStaticRoot(staticRoot) {
|
|
|
21518
21409
|
return configured;
|
|
21519
21410
|
}
|
|
21520
21411
|
const bundled = resolveBundledStaticClientRoot(import.meta.url);
|
|
21521
|
-
if (
|
|
21412
|
+
if (existsSync13(resolve9(bundled, "index.html"))) {
|
|
21522
21413
|
return bundled;
|
|
21523
21414
|
}
|
|
21524
21415
|
return resolveSourceStaticClientRoot(import.meta.url);
|
|
@@ -21791,7 +21682,15 @@ async function createOpenScoutWebServer(options) {
|
|
|
21791
21682
|
const body = await c.req.json();
|
|
21792
21683
|
if (!body.command)
|
|
21793
21684
|
return c.json({ error: "missing command" }, 400);
|
|
21794
|
-
|
|
21685
|
+
if (!options.runTerminalCommand) {
|
|
21686
|
+
return c.json({ error: "terminal relay is unavailable" }, 503);
|
|
21687
|
+
}
|
|
21688
|
+
try {
|
|
21689
|
+
await options.runTerminalCommand(body.command);
|
|
21690
|
+
} catch (error) {
|
|
21691
|
+
const message = error instanceof Error ? error.message : "failed to queue command";
|
|
21692
|
+
return c.json({ error: message }, 503);
|
|
21693
|
+
}
|
|
21795
21694
|
return c.json({ ok: true });
|
|
21796
21695
|
});
|
|
21797
21696
|
app.post("/api/agents/:agentId/interrupt", async (c) => {
|
|
@@ -21887,6 +21786,254 @@ async function createOpenScoutWebServer(options) {
|
|
|
21887
21786
|
return { app, warmupCaches };
|
|
21888
21787
|
}
|
|
21889
21788
|
|
|
21789
|
+
// packages/web/server/relay.ts
|
|
21790
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
21791
|
+
import { join as join19 } from "path";
|
|
21792
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
21793
|
+
var UPLOAD_DIR = "/tmp/scout-uploads";
|
|
21794
|
+
async function handleRelayUpload(req) {
|
|
21795
|
+
const { name, data } = await req.json();
|
|
21796
|
+
if (!name || !data) {
|
|
21797
|
+
return Response.json({ error: "Missing name or data" }, { status: 400 });
|
|
21798
|
+
}
|
|
21799
|
+
await mkdir7(UPLOAD_DIR, { recursive: true });
|
|
21800
|
+
const filename = `${randomUUID3()}-${name}`;
|
|
21801
|
+
const filepath = join19(UPLOAD_DIR, filename);
|
|
21802
|
+
await Bun.write(filepath, Buffer.from(data, "base64"));
|
|
21803
|
+
return Response.json({ path: filepath });
|
|
21804
|
+
}
|
|
21805
|
+
function flushPending(ws) {
|
|
21806
|
+
const upstream = ws.data.upstream;
|
|
21807
|
+
if (!upstream || upstream.readyState !== WebSocket.OPEN) {
|
|
21808
|
+
return;
|
|
21809
|
+
}
|
|
21810
|
+
for (const payload of ws.data.pending) {
|
|
21811
|
+
upstream.send(payload);
|
|
21812
|
+
}
|
|
21813
|
+
ws.data.pending.length = 0;
|
|
21814
|
+
}
|
|
21815
|
+
function forwardToClient(ws, data) {
|
|
21816
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
21817
|
+
return;
|
|
21818
|
+
}
|
|
21819
|
+
if (typeof data === "string") {
|
|
21820
|
+
ws.send(data);
|
|
21821
|
+
return;
|
|
21822
|
+
}
|
|
21823
|
+
if (data instanceof Blob) {
|
|
21824
|
+
data.arrayBuffer().then((buffer) => {
|
|
21825
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
21826
|
+
ws.send(buffer);
|
|
21827
|
+
}
|
|
21828
|
+
}).catch(() => {});
|
|
21829
|
+
return;
|
|
21830
|
+
}
|
|
21831
|
+
ws.send(data);
|
|
21832
|
+
}
|
|
21833
|
+
function createRelayWebSocketProxy(targetUrl) {
|
|
21834
|
+
return {
|
|
21835
|
+
open(ws) {
|
|
21836
|
+
ws.data.pending = [];
|
|
21837
|
+
const upstream = new WebSocket(targetUrl);
|
|
21838
|
+
upstream.binaryType = "arraybuffer";
|
|
21839
|
+
ws.data.upstream = upstream;
|
|
21840
|
+
upstream.onopen = () => {
|
|
21841
|
+
flushPending(ws);
|
|
21842
|
+
};
|
|
21843
|
+
upstream.onmessage = (event) => {
|
|
21844
|
+
forwardToClient(ws, event.data);
|
|
21845
|
+
};
|
|
21846
|
+
upstream.onerror = () => {
|
|
21847
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
21848
|
+
ws.close();
|
|
21849
|
+
}
|
|
21850
|
+
};
|
|
21851
|
+
upstream.onclose = () => {
|
|
21852
|
+
ws.data.upstream = null;
|
|
21853
|
+
ws.data.pending.length = 0;
|
|
21854
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
21855
|
+
ws.close();
|
|
21856
|
+
}
|
|
21857
|
+
};
|
|
21858
|
+
},
|
|
21859
|
+
message(ws, raw2) {
|
|
21860
|
+
const upstream = ws.data.upstream;
|
|
21861
|
+
const payload = raw2;
|
|
21862
|
+
if (!upstream || upstream.readyState === WebSocket.CONNECTING) {
|
|
21863
|
+
ws.data.pending.push(payload);
|
|
21864
|
+
return;
|
|
21865
|
+
}
|
|
21866
|
+
if (upstream.readyState === WebSocket.OPEN) {
|
|
21867
|
+
upstream.send(payload);
|
|
21868
|
+
}
|
|
21869
|
+
},
|
|
21870
|
+
close(ws) {
|
|
21871
|
+
const upstream = ws.data.upstream;
|
|
21872
|
+
ws.data.upstream = null;
|
|
21873
|
+
ws.data.pending.length = 0;
|
|
21874
|
+
if (!upstream) {
|
|
21875
|
+
return;
|
|
21876
|
+
}
|
|
21877
|
+
if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
|
|
21878
|
+
upstream.close();
|
|
21879
|
+
}
|
|
21880
|
+
}
|
|
21881
|
+
};
|
|
21882
|
+
}
|
|
21883
|
+
|
|
21884
|
+
// packages/web/server/managed-terminal-relay.ts
|
|
21885
|
+
import { existsSync as existsSync14 } from "fs";
|
|
21886
|
+
import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
|
|
21887
|
+
import { dirname as dirname13, resolve as resolve10 } from "path";
|
|
21888
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
21889
|
+
function relayPortForWebPort(webPort) {
|
|
21890
|
+
const configured = process.env.OPENSCOUT_WEB_TERMINAL_RELAY_PORT?.trim();
|
|
21891
|
+
if (configured) {
|
|
21892
|
+
const parsed = Number.parseInt(configured, 10);
|
|
21893
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
21894
|
+
return parsed;
|
|
21895
|
+
}
|
|
21896
|
+
}
|
|
21897
|
+
return webPort + 1;
|
|
21898
|
+
}
|
|
21899
|
+
function relayHostForWebHost(hostname2) {
|
|
21900
|
+
return process.env.OPENSCOUT_WEB_TERMINAL_RELAY_HOST?.trim() || hostname2;
|
|
21901
|
+
}
|
|
21902
|
+
function relayLoopbackHost(hostname2) {
|
|
21903
|
+
if (hostname2 === "0.0.0.0" || hostname2 === "::") {
|
|
21904
|
+
return "127.0.0.1";
|
|
21905
|
+
}
|
|
21906
|
+
return hostname2;
|
|
21907
|
+
}
|
|
21908
|
+
async function isHealthy(targetHttpUrl) {
|
|
21909
|
+
try {
|
|
21910
|
+
const response = await fetch(`${targetHttpUrl}/health`, {
|
|
21911
|
+
signal: AbortSignal.timeout(1000)
|
|
21912
|
+
});
|
|
21913
|
+
return response.ok;
|
|
21914
|
+
} catch {
|
|
21915
|
+
return false;
|
|
21916
|
+
}
|
|
21917
|
+
}
|
|
21918
|
+
async function waitForHealthy(targetHttpUrl, timeoutMs) {
|
|
21919
|
+
const deadline = Date.now() + timeoutMs;
|
|
21920
|
+
while (Date.now() < deadline) {
|
|
21921
|
+
if (await isHealthy(targetHttpUrl)) {
|
|
21922
|
+
return true;
|
|
21923
|
+
}
|
|
21924
|
+
await new Promise((resolveTimer) => setTimeout(resolveTimer, 100));
|
|
21925
|
+
}
|
|
21926
|
+
return false;
|
|
21927
|
+
}
|
|
21928
|
+
function resolveRuntimeEntry() {
|
|
21929
|
+
const selfDir = dirname13(fileURLToPath8(import.meta.url));
|
|
21930
|
+
const sourceEntry = resolve10(selfDir, "terminal-relay-node.ts");
|
|
21931
|
+
const bundledEntry = resolve10(selfDir, "openscout-terminal-relay.mjs");
|
|
21932
|
+
const sourceBundle = resolve10(selfDir, "../dist/openscout-terminal-relay.mjs");
|
|
21933
|
+
if (existsSync14(sourceEntry)) {
|
|
21934
|
+
const build = spawnSync2("bun", [
|
|
21935
|
+
"build",
|
|
21936
|
+
sourceEntry,
|
|
21937
|
+
"--target=node",
|
|
21938
|
+
"--format=esm",
|
|
21939
|
+
"--outfile",
|
|
21940
|
+
sourceBundle,
|
|
21941
|
+
"--external",
|
|
21942
|
+
"node-pty"
|
|
21943
|
+
], {
|
|
21944
|
+
cwd: resolve10(selfDir, ".."),
|
|
21945
|
+
stdio: "inherit"
|
|
21946
|
+
});
|
|
21947
|
+
if ((build.status ?? 1) !== 0) {
|
|
21948
|
+
throw new Error("Failed to build terminal relay bundle");
|
|
21949
|
+
}
|
|
21950
|
+
return sourceBundle;
|
|
21951
|
+
}
|
|
21952
|
+
if (existsSync14(bundledEntry)) {
|
|
21953
|
+
return bundledEntry;
|
|
21954
|
+
}
|
|
21955
|
+
throw new Error("Could not locate the terminal relay runtime entry");
|
|
21956
|
+
}
|
|
21957
|
+
async function startManagedTerminalRelay(args) {
|
|
21958
|
+
const relayPort = relayPortForWebPort(args.webPort);
|
|
21959
|
+
const bindHost = relayHostForWebHost(args.hostname);
|
|
21960
|
+
const loopbackHost = relayLoopbackHost(bindHost);
|
|
21961
|
+
const targetHttpUrl = `http://${loopbackHost}:${relayPort}`;
|
|
21962
|
+
const targetWebSocketUrl = `ws://${loopbackHost}:${relayPort}`;
|
|
21963
|
+
if (await isHealthy(targetHttpUrl)) {
|
|
21964
|
+
return {
|
|
21965
|
+
healthcheck: () => isHealthy(targetHttpUrl),
|
|
21966
|
+
queueCommand: async (command) => {
|
|
21967
|
+
const response = await fetch(`${targetHttpUrl}/api/terminal/run`, {
|
|
21968
|
+
method: "POST",
|
|
21969
|
+
headers: { "Content-Type": "application/json" },
|
|
21970
|
+
body: JSON.stringify({ command })
|
|
21971
|
+
});
|
|
21972
|
+
if (!response.ok) {
|
|
21973
|
+
throw new Error("Terminal relay rejected queued command");
|
|
21974
|
+
}
|
|
21975
|
+
},
|
|
21976
|
+
shutdown() {},
|
|
21977
|
+
targetHttpUrl,
|
|
21978
|
+
targetWebSocketUrl
|
|
21979
|
+
};
|
|
21980
|
+
}
|
|
21981
|
+
const runtimeEntry = resolveRuntimeEntry();
|
|
21982
|
+
const child = spawn5("node", [runtimeEntry], {
|
|
21983
|
+
cwd: resolve10(dirname13(runtimeEntry), ".."),
|
|
21984
|
+
env: {
|
|
21985
|
+
...process.env,
|
|
21986
|
+
OPENSCOUT_WEB_TERMINAL_RELAY_HOST: bindHost,
|
|
21987
|
+
OPENSCOUT_WEB_TERMINAL_RELAY_PORT: String(relayPort)
|
|
21988
|
+
},
|
|
21989
|
+
stdio: "inherit"
|
|
21990
|
+
});
|
|
21991
|
+
let spawnError = null;
|
|
21992
|
+
child.once("error", (error) => {
|
|
21993
|
+
spawnError = error;
|
|
21994
|
+
});
|
|
21995
|
+
child.on("exit", (code, signal) => {
|
|
21996
|
+
if (signal === "SIGTERM" || signal === "SIGINT") {
|
|
21997
|
+
return;
|
|
21998
|
+
}
|
|
21999
|
+
console.error(`[relay] Managed terminal relay exited unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`);
|
|
22000
|
+
});
|
|
22001
|
+
const ready = await waitForHealthy(targetHttpUrl, 5000);
|
|
22002
|
+
if (!ready) {
|
|
22003
|
+
if (!child.killed) {
|
|
22004
|
+
child.kill("SIGTERM");
|
|
22005
|
+
}
|
|
22006
|
+
if (spawnError) {
|
|
22007
|
+
throw spawnError;
|
|
22008
|
+
}
|
|
22009
|
+
throw new Error(`Terminal relay did not become healthy at ${targetHttpUrl}`);
|
|
22010
|
+
}
|
|
22011
|
+
return {
|
|
22012
|
+
healthcheck: () => isHealthy(targetHttpUrl),
|
|
22013
|
+
queueCommand: async (command) => {
|
|
22014
|
+
const response = await fetch(`${targetHttpUrl}/api/terminal/run`, {
|
|
22015
|
+
method: "POST",
|
|
22016
|
+
headers: { "Content-Type": "application/json" },
|
|
22017
|
+
body: JSON.stringify({ command })
|
|
22018
|
+
});
|
|
22019
|
+
if (!response.ok) {
|
|
22020
|
+
throw new Error("Terminal relay rejected queued command");
|
|
22021
|
+
}
|
|
22022
|
+
},
|
|
22023
|
+
shutdown() {
|
|
22024
|
+
terminateChild(child);
|
|
22025
|
+
},
|
|
22026
|
+
targetHttpUrl,
|
|
22027
|
+
targetWebSocketUrl
|
|
22028
|
+
};
|
|
22029
|
+
}
|
|
22030
|
+
function terminateChild(child) {
|
|
22031
|
+
if (child.killed) {
|
|
22032
|
+
return;
|
|
22033
|
+
}
|
|
22034
|
+
child.kill("SIGTERM");
|
|
22035
|
+
}
|
|
22036
|
+
|
|
21890
22037
|
// packages/web/server/index.ts
|
|
21891
22038
|
var port = Number(process.env.OPENSCOUT_WEB_PORT ?? process.env.SCOUT_WEB_PORT ?? "3200");
|
|
21892
22039
|
var hostname2 = process.env.OPENSCOUT_WEB_HOST?.trim() || process.env.SCOUT_WEB_HOST?.trim() || "127.0.0.1";
|
|
@@ -21896,13 +22043,13 @@ function resolveStaticRoot2() {
|
|
|
21896
22043
|
if (process.env.OPENSCOUT_WEB_STATIC_ROOT?.trim()) {
|
|
21897
22044
|
return process.env.OPENSCOUT_WEB_STATIC_ROOT.trim();
|
|
21898
22045
|
}
|
|
21899
|
-
const selfDir =
|
|
21900
|
-
const siblingClientRoot =
|
|
21901
|
-
if (existsSync15(
|
|
22046
|
+
const selfDir = dirname14(fileURLToPath9(import.meta.url));
|
|
22047
|
+
const siblingClientRoot = join20(selfDir, "client");
|
|
22048
|
+
if (existsSync15(join20(siblingClientRoot, "index.html"))) {
|
|
21902
22049
|
return siblingClientRoot;
|
|
21903
22050
|
}
|
|
21904
|
-
const sourceDistClientRoot =
|
|
21905
|
-
if (existsSync15(
|
|
22051
|
+
const sourceDistClientRoot = resolve11(selfDir, "../dist/client");
|
|
22052
|
+
if (existsSync15(join20(sourceDistClientRoot, "index.html"))) {
|
|
21906
22053
|
return sourceDistClientRoot;
|
|
21907
22054
|
}
|
|
21908
22055
|
return;
|
|
@@ -21911,26 +22058,47 @@ var staticRoot = resolveStaticRoot2();
|
|
|
21911
22058
|
var viteDevUrl = process.env.OPENSCOUT_WEB_VITE_URL?.trim() || undefined;
|
|
21912
22059
|
var useViteProxy = Boolean(viteDevUrl) || !staticRoot;
|
|
21913
22060
|
var idleTimeoutSeconds = Number.parseInt(process.env.OPENSCOUT_WEB_IDLE_TIMEOUT_SECONDS?.trim() || (useViteProxy ? "180" : "30"), 10);
|
|
22061
|
+
var terminalRelay = null;
|
|
22062
|
+
try {
|
|
22063
|
+
terminalRelay = await startManagedTerminalRelay({
|
|
22064
|
+
hostname: hostname2,
|
|
22065
|
+
webPort: port
|
|
22066
|
+
});
|
|
22067
|
+
} catch (error) {
|
|
22068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22069
|
+
console.warn(`[scout] Terminal relay unavailable: ${message}`);
|
|
22070
|
+
}
|
|
21914
22071
|
var { app, warmupCaches } = await createOpenScoutWebServer({
|
|
21915
22072
|
currentDirectory,
|
|
21916
22073
|
shellStateCacheTtlMs,
|
|
21917
22074
|
assetMode: useViteProxy ? "vite-proxy" : "static",
|
|
21918
22075
|
viteDevUrl,
|
|
21919
|
-
staticRoot
|
|
22076
|
+
staticRoot,
|
|
22077
|
+
runTerminalCommand: terminalRelay?.queueCommand
|
|
21920
22078
|
});
|
|
21921
22079
|
var honoFetch = app.fetch;
|
|
22080
|
+
var relayWebSocket = terminalRelay ? createRelayWebSocketProxy(terminalRelay.targetWebSocketUrl) : {
|
|
22081
|
+
open(ws) {
|
|
22082
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
22083
|
+
ws.close(1013, "Terminal relay unavailable");
|
|
22084
|
+
}
|
|
22085
|
+
},
|
|
22086
|
+
message() {},
|
|
22087
|
+
close() {}
|
|
22088
|
+
};
|
|
21922
22089
|
var server = Bun.serve({
|
|
21923
22090
|
port,
|
|
21924
22091
|
hostname: hostname2,
|
|
21925
22092
|
idleTimeout: idleTimeoutSeconds,
|
|
21926
|
-
fetch(req, server2) {
|
|
22093
|
+
async fetch(req, server2) {
|
|
21927
22094
|
const url = new URL(req.url);
|
|
21928
22095
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
21929
|
-
const ok = server2.upgrade(req, { data: {
|
|
22096
|
+
const ok = server2.upgrade(req, { data: { upstream: null, pending: [] } });
|
|
21930
22097
|
return ok ? undefined : new Response("WebSocket upgrade failed", { status: 500 });
|
|
21931
22098
|
}
|
|
21932
22099
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
21933
|
-
|
|
22100
|
+
const relayOk = terminalRelay ? await terminalRelay.healthcheck() : false;
|
|
22101
|
+
return Response.json({ ok: relayOk, relay: relayOk ? "up" : "down" }, { status: relayOk ? 200 : 503 });
|
|
21934
22102
|
}
|
|
21935
22103
|
if (req.method === "POST" && (url.pathname === "/api/upload" || url.pathname === "/api/relay/upload")) {
|
|
21936
22104
|
return handleRelayUpload(req);
|
|
@@ -21941,8 +22109,8 @@ var server = Bun.serve({
|
|
|
21941
22109
|
});
|
|
21942
22110
|
var shutdown = () => {
|
|
21943
22111
|
console.log(`
|
|
21944
|
-
[scout] Shutting down relay
|
|
21945
|
-
|
|
22112
|
+
[scout] Shutting down terminal relay...`);
|
|
22113
|
+
terminalRelay?.shutdown();
|
|
21946
22114
|
server.stop();
|
|
21947
22115
|
process.exit(0);
|
|
21948
22116
|
};
|