@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
package/dist/pair-supervisor.mjs
CHANGED
|
@@ -956,19 +956,58 @@ function findStoredCerts() {
|
|
|
956
956
|
return null;
|
|
957
957
|
}
|
|
958
958
|
}
|
|
959
|
-
function
|
|
959
|
+
function readTailscaleStatus() {
|
|
960
960
|
try {
|
|
961
961
|
const output = execSync("tailscale status --self=true --peers=false --json", {
|
|
962
962
|
stdio: ["pipe", "pipe", "pipe"],
|
|
963
963
|
timeout: 5000
|
|
964
964
|
}).toString();
|
|
965
965
|
const data = JSON.parse(output);
|
|
966
|
-
const dnsName = data
|
|
967
|
-
return
|
|
966
|
+
const dnsName = typeof data.Self?.DNSName === "string" ? data.Self.DNSName.replace(/\.$/, "") : "";
|
|
967
|
+
return {
|
|
968
|
+
backendState: typeof data.BackendState === "string" ? data.BackendState : null,
|
|
969
|
+
dnsName: dnsName || null,
|
|
970
|
+
online: data.Self?.Online !== false,
|
|
971
|
+
health: Array.isArray(data.Health) ? data.Health.filter((entry) => typeof entry === "string") : []
|
|
972
|
+
};
|
|
968
973
|
} catch {
|
|
969
974
|
return null;
|
|
970
975
|
}
|
|
971
976
|
}
|
|
977
|
+
function resolveRelayEndpointForTailscaleStatus(port, tailscale) {
|
|
978
|
+
const backendState = tailscale?.backendState?.trim().toLowerCase() ?? "";
|
|
979
|
+
const hostname = tailscale?.dnsName ?? null;
|
|
980
|
+
const tailscaleRunning = backendState === "running" && tailscale?.online !== false && Boolean(hostname);
|
|
981
|
+
const tls = resolveTls(tailscaleRunning ? hostname : null);
|
|
982
|
+
if (tailscaleRunning && hostname && tls) {
|
|
983
|
+
return {
|
|
984
|
+
relayUrl: `wss://${hostname}:${port}`,
|
|
985
|
+
options: { tls }
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (tailscaleRunning && hostname) {
|
|
989
|
+
pairingLog.warn("relay", "tailscale is running without TLS; falling back to insecure websocket relay", {
|
|
990
|
+
hostname,
|
|
991
|
+
port
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
relayUrl: `ws://${hostname}:${port}`,
|
|
995
|
+
options: {}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (hostname && backendState && backendState !== "running") {
|
|
999
|
+
pairingLog.warn("relay", "tailscale hostname detected but tailscale is not running; using local-only relay endpoint", {
|
|
1000
|
+
hostname,
|
|
1001
|
+
backendState,
|
|
1002
|
+
health: tailscale?.health ?? [],
|
|
1003
|
+
port
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
relayUrl: `ws://127.0.0.1:${port}`,
|
|
1008
|
+
options: {}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
972
1011
|
function generateTailscaleCerts(hostname) {
|
|
973
1012
|
mkdirSync3(PAIRING_DIR, { recursive: true });
|
|
974
1013
|
const certPath = join(PAIRING_DIR, `${hostname}.crt`);
|
|
@@ -1014,28 +1053,7 @@ function resolveTls(hostname) {
|
|
|
1014
1053
|
return generateTailscaleCerts(hostname);
|
|
1015
1054
|
}
|
|
1016
1055
|
function resolveRelayEndpoint(port) {
|
|
1017
|
-
|
|
1018
|
-
const tls = resolveTls(hostname);
|
|
1019
|
-
if (hostname && tls) {
|
|
1020
|
-
return {
|
|
1021
|
-
relayUrl: `wss://${hostname}:${port}`,
|
|
1022
|
-
options: { tls }
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
if (hostname) {
|
|
1026
|
-
pairingLog.warn("relay", "tailscale hostname detected without TLS; falling back to insecure websocket relay", {
|
|
1027
|
-
hostname,
|
|
1028
|
-
port
|
|
1029
|
-
});
|
|
1030
|
-
return {
|
|
1031
|
-
relayUrl: `ws://${hostname}:${port}`,
|
|
1032
|
-
options: {}
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
return {
|
|
1036
|
-
relayUrl: `ws://127.0.0.1:${port}`,
|
|
1037
|
-
options: {}
|
|
1038
|
-
};
|
|
1056
|
+
return resolveRelayEndpointForTailscaleStatus(port, readTailscaleStatus());
|
|
1039
1057
|
}
|
|
1040
1058
|
function startManagedRelay(port = 7889) {
|
|
1041
1059
|
const endpoint = resolveRelayEndpoint(port);
|
|
@@ -7869,7 +7887,73 @@ function resolveConfig() {
|
|
|
7869
7887
|
// ../../apps/desktop/src/core/pairing/runtime/bridge/fileserver.ts
|
|
7870
7888
|
import { isAbsolute } from "path";
|
|
7871
7889
|
import { homedir as homedir9 } from "os";
|
|
7890
|
+
|
|
7891
|
+
// ../../apps/desktop/src/core/pairing/runtime/bridge/web-handoff.ts
|
|
7892
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
7893
|
+
var SCOUT_WEB_HANDOFF_COOKIE = "scout_handoff";
|
|
7894
|
+
var WEB_HANDOFF_TTL_MS = 5 * 60 * 1000;
|
|
7895
|
+
var activeWebHandoffs = new Map;
|
|
7896
|
+
function pruneExpiredWebHandoffs(now = Date.now()) {
|
|
7897
|
+
for (const [token, record] of activeWebHandoffs) {
|
|
7898
|
+
if (record.expiresAt <= now) {
|
|
7899
|
+
activeWebHandoffs.delete(token);
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
}
|
|
7903
|
+
function scopesMatch(left, right) {
|
|
7904
|
+
if (left.kind !== right.kind) {
|
|
7905
|
+
return false;
|
|
7906
|
+
}
|
|
7907
|
+
if (left.sessionId !== right.sessionId) {
|
|
7908
|
+
return false;
|
|
7909
|
+
}
|
|
7910
|
+
if (left.kind === "session") {
|
|
7911
|
+
return true;
|
|
7912
|
+
}
|
|
7913
|
+
return left.turnId === right.turnId && left.blockId === right.blockId;
|
|
7914
|
+
}
|
|
7915
|
+
function pathForWebHandoffScope(scope) {
|
|
7916
|
+
switch (scope.kind) {
|
|
7917
|
+
case "session":
|
|
7918
|
+
return `/handoff/session/${encodeURIComponent(scope.sessionId)}`;
|
|
7919
|
+
case "file_change":
|
|
7920
|
+
return `/handoff/file-change/${encodeURIComponent(scope.sessionId)}/${encodeURIComponent(scope.turnId)}/${encodeURIComponent(scope.blockId)}`;
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
function issueWebHandoff(scope, deviceId) {
|
|
7924
|
+
pruneExpiredWebHandoffs();
|
|
7925
|
+
const token = randomBytes2(24).toString("base64url");
|
|
7926
|
+
const expiresAt = Date.now() + WEB_HANDOFF_TTL_MS;
|
|
7927
|
+
activeWebHandoffs.set(token, {
|
|
7928
|
+
token,
|
|
7929
|
+
deviceId: deviceId?.trim() || null,
|
|
7930
|
+
expiresAt,
|
|
7931
|
+
scope
|
|
7932
|
+
});
|
|
7933
|
+
return { token, expiresAt, scope };
|
|
7934
|
+
}
|
|
7935
|
+
function readAuthorizedWebHandoff(token, scope) {
|
|
7936
|
+
pruneExpiredWebHandoffs();
|
|
7937
|
+
if (!token) {
|
|
7938
|
+
return null;
|
|
7939
|
+
}
|
|
7940
|
+
const record = activeWebHandoffs.get(token);
|
|
7941
|
+
if (!record) {
|
|
7942
|
+
return null;
|
|
7943
|
+
}
|
|
7944
|
+
if (!scopesMatch(record.scope, scope)) {
|
|
7945
|
+
return null;
|
|
7946
|
+
}
|
|
7947
|
+
return record;
|
|
7948
|
+
}
|
|
7949
|
+
|
|
7950
|
+
// ../../apps/desktop/src/core/pairing/runtime/bridge/fileserver.ts
|
|
7872
7951
|
var ALLOWED_ROOTS = [homedir9(), "/tmp"];
|
|
7952
|
+
var LOOPBACK_IPV4_HOST_PATTERN = /^127(?:\.\d{1,3}){3}$/;
|
|
7953
|
+
function isTrustedLoopbackHostname(hostname) {
|
|
7954
|
+
const normalized = hostname.trim().toLowerCase();
|
|
7955
|
+
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || LOOPBACK_IPV4_HOST_PATTERN.test(normalized);
|
|
7956
|
+
}
|
|
7873
7957
|
function isAllowedPath(filePath) {
|
|
7874
7958
|
if (!isAbsolute(filePath))
|
|
7875
7959
|
return false;
|
|
@@ -7880,7 +7964,7 @@ function isAllowedPath(filePath) {
|
|
|
7880
7964
|
return ALLOWED_ROOTS.some((root) => filePath.startsWith(root));
|
|
7881
7965
|
}
|
|
7882
7966
|
function startFileServer(options) {
|
|
7883
|
-
const { port } = options;
|
|
7967
|
+
const { port, bridge } = options;
|
|
7884
7968
|
let server = null;
|
|
7885
7969
|
function start() {
|
|
7886
7970
|
try {
|
|
@@ -7888,7 +7972,7 @@ function startFileServer(options) {
|
|
|
7888
7972
|
port,
|
|
7889
7973
|
fetch(req) {
|
|
7890
7974
|
try {
|
|
7891
|
-
return route(new URL(req.url));
|
|
7975
|
+
return route(req, new URL(req.url), bridge);
|
|
7892
7976
|
} catch (err) {
|
|
7893
7977
|
console.error(`[fileserver] ${req.url} \u2192`, err.message);
|
|
7894
7978
|
return new Response("Internal error", { status: 500 });
|
|
@@ -7911,12 +7995,19 @@ function startFileServer(options) {
|
|
|
7911
7995
|
start();
|
|
7912
7996
|
return { port, stop, restart };
|
|
7913
7997
|
}
|
|
7914
|
-
function route(url) {
|
|
7998
|
+
function route(req, url, bridge) {
|
|
7915
7999
|
const path2 = url.pathname;
|
|
7916
|
-
if (path2 === "/file")
|
|
8000
|
+
if (path2 === "/file") {
|
|
8001
|
+
if (!isTrustedLoopbackHostname(url.hostname)) {
|
|
8002
|
+
return new Response("Forbidden", { status: 403 });
|
|
8003
|
+
}
|
|
7917
8004
|
return serveFile(url);
|
|
8005
|
+
}
|
|
7918
8006
|
if (path2 === "/health")
|
|
7919
8007
|
return Response.json({ ok: true });
|
|
8008
|
+
if (path2.startsWith("/handoff/")) {
|
|
8009
|
+
return serveHandoffPage(req, url, bridge);
|
|
8010
|
+
}
|
|
7920
8011
|
return new Response("Pairing file server", { status: 200 });
|
|
7921
8012
|
}
|
|
7922
8013
|
function serveFile(url) {
|
|
@@ -7930,6 +8021,520 @@ function serveFile(url) {
|
|
|
7930
8021
|
return new Response("Not found", { status: 404 });
|
|
7931
8022
|
return new Response(file);
|
|
7932
8023
|
}
|
|
8024
|
+
function serveHandoffPage(req, url, bridge) {
|
|
8025
|
+
if (!bridge) {
|
|
8026
|
+
return new Response("Secure handoff unavailable", { status: 503 });
|
|
8027
|
+
}
|
|
8028
|
+
const scope = parseHandoffScope(url);
|
|
8029
|
+
if (!scope) {
|
|
8030
|
+
return new Response("Not found", { status: 404 });
|
|
8031
|
+
}
|
|
8032
|
+
const token = readHandoffToken(req);
|
|
8033
|
+
const authorized = readAuthorizedWebHandoff(token, scope);
|
|
8034
|
+
if (!authorized) {
|
|
8035
|
+
return unauthorizedHandoffResponse();
|
|
8036
|
+
}
|
|
8037
|
+
let html;
|
|
8038
|
+
switch (scope.kind) {
|
|
8039
|
+
case "session": {
|
|
8040
|
+
const snapshot = bridge.getSessionSnapshot(scope.sessionId);
|
|
8041
|
+
if (!snapshot) {
|
|
8042
|
+
return new Response("Session handoff expired", { status: 404 });
|
|
8043
|
+
}
|
|
8044
|
+
html = renderSessionHandoffPage(snapshot);
|
|
8045
|
+
break;
|
|
8046
|
+
}
|
|
8047
|
+
case "file_change": {
|
|
8048
|
+
const snapshot = bridge.getSessionSnapshot(scope.sessionId);
|
|
8049
|
+
if (!snapshot) {
|
|
8050
|
+
return new Response("Session handoff expired", { status: 404 });
|
|
8051
|
+
}
|
|
8052
|
+
const target = findFileChangeBlock(snapshot, scope.turnId, scope.blockId);
|
|
8053
|
+
if (!target) {
|
|
8054
|
+
return new Response("File change handoff expired", { status: 404 });
|
|
8055
|
+
}
|
|
8056
|
+
html = renderFileChangeHandoffPage(snapshot, target.turn, target.block);
|
|
8057
|
+
break;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
const headers = new Headers({
|
|
8061
|
+
"content-type": "text/html; charset=utf-8",
|
|
8062
|
+
"cache-control": "no-store",
|
|
8063
|
+
"x-content-type-options": "nosniff"
|
|
8064
|
+
});
|
|
8065
|
+
headers.set("set-cookie", `${SCOUT_WEB_HANDOFF_COOKIE}=${authorized.token}; Max-Age=300; Path=/handoff; HttpOnly; SameSite=Strict`);
|
|
8066
|
+
return new Response(html, { status: 200, headers });
|
|
8067
|
+
}
|
|
8068
|
+
function unauthorizedHandoffResponse() {
|
|
8069
|
+
return new Response("Secure handoff required", {
|
|
8070
|
+
status: 401,
|
|
8071
|
+
headers: {
|
|
8072
|
+
"cache-control": "no-store",
|
|
8073
|
+
"set-cookie": `${SCOUT_WEB_HANDOFF_COOKIE}=; Max-Age=0; Path=/handoff; HttpOnly; SameSite=Strict`
|
|
8074
|
+
}
|
|
8075
|
+
});
|
|
8076
|
+
}
|
|
8077
|
+
function parseHandoffScope(url) {
|
|
8078
|
+
const parts = url.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
8079
|
+
if (parts[0] !== "handoff") {
|
|
8080
|
+
return null;
|
|
8081
|
+
}
|
|
8082
|
+
if (parts[1] === "session" && parts[2]) {
|
|
8083
|
+
return { kind: "session", sessionId: parts[2] };
|
|
8084
|
+
}
|
|
8085
|
+
if (parts[1] === "file-change" && parts[2] && parts[3] && parts[4]) {
|
|
8086
|
+
return {
|
|
8087
|
+
kind: "file_change",
|
|
8088
|
+
sessionId: parts[2],
|
|
8089
|
+
turnId: parts[3],
|
|
8090
|
+
blockId: parts[4]
|
|
8091
|
+
};
|
|
8092
|
+
}
|
|
8093
|
+
return null;
|
|
8094
|
+
}
|
|
8095
|
+
function readHandoffToken(req) {
|
|
8096
|
+
const headerToken = req.headers.get("x-scout-handoff-token")?.trim();
|
|
8097
|
+
if (headerToken) {
|
|
8098
|
+
return headerToken;
|
|
8099
|
+
}
|
|
8100
|
+
const cookieHeader = req.headers.get("cookie");
|
|
8101
|
+
if (!cookieHeader) {
|
|
8102
|
+
return null;
|
|
8103
|
+
}
|
|
8104
|
+
for (const part of cookieHeader.split(";")) {
|
|
8105
|
+
const [name, ...rest] = part.trim().split("=");
|
|
8106
|
+
if (name === SCOUT_WEB_HANDOFF_COOKIE) {
|
|
8107
|
+
const value = rest.join("=").trim();
|
|
8108
|
+
return value || null;
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
return null;
|
|
8112
|
+
}
|
|
8113
|
+
function findFileChangeBlock(snapshot, turnId, blockId) {
|
|
8114
|
+
const turn = snapshot.turns.find((candidate) => candidate.id === turnId);
|
|
8115
|
+
if (!turn) {
|
|
8116
|
+
return null;
|
|
8117
|
+
}
|
|
8118
|
+
const blockState = turn.blocks.find((candidate) => candidate.block.id === blockId);
|
|
8119
|
+
const block = blockState?.block;
|
|
8120
|
+
if (!block || block.type !== "action" || block.action.kind !== "file_change") {
|
|
8121
|
+
return null;
|
|
8122
|
+
}
|
|
8123
|
+
return { turn, block };
|
|
8124
|
+
}
|
|
8125
|
+
function renderPage(title, body) {
|
|
8126
|
+
return `<!doctype html>
|
|
8127
|
+
<html lang="en">
|
|
8128
|
+
<head>
|
|
8129
|
+
<meta charset="utf-8" />
|
|
8130
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8131
|
+
<title>${escapeHtml(title)}</title>
|
|
8132
|
+
<style>
|
|
8133
|
+
:root {
|
|
8134
|
+
color-scheme: dark;
|
|
8135
|
+
--bg: #0b0f14;
|
|
8136
|
+
--panel: rgba(19, 27, 39, 0.92);
|
|
8137
|
+
--panel-strong: rgba(12, 18, 28, 0.96);
|
|
8138
|
+
--line: rgba(148, 163, 184, 0.16);
|
|
8139
|
+
--line-strong: rgba(148, 163, 184, 0.24);
|
|
8140
|
+
--text: #e5ecf5;
|
|
8141
|
+
--muted: #90a0b4;
|
|
8142
|
+
--accent: #63d0ff;
|
|
8143
|
+
--green: #54d38a;
|
|
8144
|
+
--amber: #f8c36a;
|
|
8145
|
+
--red: #ff7b72;
|
|
8146
|
+
--surface-add: rgba(84, 211, 138, 0.08);
|
|
8147
|
+
--surface-del: rgba(255, 123, 114, 0.08);
|
|
8148
|
+
}
|
|
8149
|
+
* { box-sizing: border-box; }
|
|
8150
|
+
body {
|
|
8151
|
+
margin: 0;
|
|
8152
|
+
background:
|
|
8153
|
+
radial-gradient(circle at top left, rgba(99, 208, 255, 0.12), transparent 32%),
|
|
8154
|
+
linear-gradient(180deg, #090d12 0%, #0b0f14 55%, #111723 100%);
|
|
8155
|
+
color: var(--text);
|
|
8156
|
+
font: 14px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
|
8157
|
+
}
|
|
8158
|
+
.wrap {
|
|
8159
|
+
width: min(980px, calc(100vw - 24px));
|
|
8160
|
+
margin: 0 auto;
|
|
8161
|
+
padding: 20px 0 40px;
|
|
8162
|
+
}
|
|
8163
|
+
.hero, .panel {
|
|
8164
|
+
background: var(--panel);
|
|
8165
|
+
border: 1px solid var(--line);
|
|
8166
|
+
border-radius: 18px;
|
|
8167
|
+
backdrop-filter: blur(18px);
|
|
8168
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
|
|
8169
|
+
}
|
|
8170
|
+
.hero {
|
|
8171
|
+
padding: 18px 18px 16px;
|
|
8172
|
+
margin-bottom: 14px;
|
|
8173
|
+
}
|
|
8174
|
+
.eyebrow {
|
|
8175
|
+
display: inline-flex;
|
|
8176
|
+
align-items: center;
|
|
8177
|
+
gap: 8px;
|
|
8178
|
+
color: var(--muted);
|
|
8179
|
+
font-size: 11px;
|
|
8180
|
+
letter-spacing: 0.14em;
|
|
8181
|
+
text-transform: uppercase;
|
|
8182
|
+
}
|
|
8183
|
+
.dot {
|
|
8184
|
+
width: 8px;
|
|
8185
|
+
height: 8px;
|
|
8186
|
+
border-radius: 999px;
|
|
8187
|
+
background: var(--accent);
|
|
8188
|
+
box-shadow: 0 0 14px rgba(99, 208, 255, 0.65);
|
|
8189
|
+
}
|
|
8190
|
+
h1 {
|
|
8191
|
+
margin: 12px 0 10px;
|
|
8192
|
+
font-size: 24px;
|
|
8193
|
+
line-height: 1.15;
|
|
8194
|
+
}
|
|
8195
|
+
.meta, .stack {
|
|
8196
|
+
display: flex;
|
|
8197
|
+
flex-wrap: wrap;
|
|
8198
|
+
gap: 8px;
|
|
8199
|
+
}
|
|
8200
|
+
.pill {
|
|
8201
|
+
display: inline-flex;
|
|
8202
|
+
align-items: center;
|
|
8203
|
+
gap: 6px;
|
|
8204
|
+
padding: 5px 10px;
|
|
8205
|
+
border-radius: 999px;
|
|
8206
|
+
border: 1px solid var(--line-strong);
|
|
8207
|
+
background: rgba(255, 255, 255, 0.03);
|
|
8208
|
+
color: var(--muted);
|
|
8209
|
+
font-size: 12px;
|
|
8210
|
+
}
|
|
8211
|
+
.pill strong {
|
|
8212
|
+
color: var(--text);
|
|
8213
|
+
font-weight: 600;
|
|
8214
|
+
}
|
|
8215
|
+
.stack { flex-direction: column; }
|
|
8216
|
+
.panel {
|
|
8217
|
+
margin-top: 12px;
|
|
8218
|
+
overflow: hidden;
|
|
8219
|
+
}
|
|
8220
|
+
.panel-inner {
|
|
8221
|
+
padding: 16px;
|
|
8222
|
+
}
|
|
8223
|
+
.turn {
|
|
8224
|
+
border-top: 1px solid var(--line);
|
|
8225
|
+
}
|
|
8226
|
+
.turn:first-child {
|
|
8227
|
+
border-top: none;
|
|
8228
|
+
}
|
|
8229
|
+
.turn-head {
|
|
8230
|
+
padding: 14px 16px;
|
|
8231
|
+
display: flex;
|
|
8232
|
+
justify-content: space-between;
|
|
8233
|
+
gap: 12px;
|
|
8234
|
+
align-items: center;
|
|
8235
|
+
background: rgba(255, 255, 255, 0.02);
|
|
8236
|
+
}
|
|
8237
|
+
.turn-title {
|
|
8238
|
+
font-size: 13px;
|
|
8239
|
+
font-weight: 600;
|
|
8240
|
+
letter-spacing: 0.02em;
|
|
8241
|
+
}
|
|
8242
|
+
.turn-subtitle {
|
|
8243
|
+
color: var(--muted);
|
|
8244
|
+
font-size: 12px;
|
|
8245
|
+
margin-top: 2px;
|
|
8246
|
+
}
|
|
8247
|
+
.blocks {
|
|
8248
|
+
padding: 8px;
|
|
8249
|
+
}
|
|
8250
|
+
.block {
|
|
8251
|
+
padding: 12px;
|
|
8252
|
+
border-radius: 14px;
|
|
8253
|
+
border: 1px solid var(--line);
|
|
8254
|
+
background: var(--panel-strong);
|
|
8255
|
+
margin: 8px;
|
|
8256
|
+
}
|
|
8257
|
+
.block-head {
|
|
8258
|
+
display: flex;
|
|
8259
|
+
justify-content: space-between;
|
|
8260
|
+
align-items: center;
|
|
8261
|
+
gap: 12px;
|
|
8262
|
+
margin-bottom: 10px;
|
|
8263
|
+
}
|
|
8264
|
+
.block-type {
|
|
8265
|
+
color: var(--muted);
|
|
8266
|
+
text-transform: uppercase;
|
|
8267
|
+
letter-spacing: 0.12em;
|
|
8268
|
+
font-size: 11px;
|
|
8269
|
+
}
|
|
8270
|
+
.label {
|
|
8271
|
+
color: var(--muted);
|
|
8272
|
+
font-size: 12px;
|
|
8273
|
+
}
|
|
8274
|
+
.label strong {
|
|
8275
|
+
color: var(--text);
|
|
8276
|
+
}
|
|
8277
|
+
pre {
|
|
8278
|
+
margin: 0;
|
|
8279
|
+
white-space: pre-wrap;
|
|
8280
|
+
word-break: break-word;
|
|
8281
|
+
overflow-wrap: anywhere;
|
|
8282
|
+
font: 12.5px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
8283
|
+
}
|
|
8284
|
+
.code {
|
|
8285
|
+
padding: 12px;
|
|
8286
|
+
border-radius: 12px;
|
|
8287
|
+
border: 1px solid var(--line);
|
|
8288
|
+
background: rgba(255, 255, 255, 0.02);
|
|
8289
|
+
}
|
|
8290
|
+
.diff-line-add { background: var(--surface-add); color: #8ef3b7; }
|
|
8291
|
+
.diff-line-del { background: var(--surface-del); color: #ff9d96; }
|
|
8292
|
+
.details {
|
|
8293
|
+
margin-top: 10px;
|
|
8294
|
+
border: 1px solid var(--line);
|
|
8295
|
+
border-radius: 12px;
|
|
8296
|
+
overflow: hidden;
|
|
8297
|
+
}
|
|
8298
|
+
details > summary {
|
|
8299
|
+
list-style: none;
|
|
8300
|
+
cursor: pointer;
|
|
8301
|
+
padding: 10px 12px;
|
|
8302
|
+
background: rgba(255, 255, 255, 0.03);
|
|
8303
|
+
color: var(--muted);
|
|
8304
|
+
font-size: 12px;
|
|
8305
|
+
text-transform: uppercase;
|
|
8306
|
+
letter-spacing: 0.1em;
|
|
8307
|
+
}
|
|
8308
|
+
details[open] > summary {
|
|
8309
|
+
border-bottom: 1px solid var(--line);
|
|
8310
|
+
}
|
|
8311
|
+
.empty {
|
|
8312
|
+
padding: 28px 18px;
|
|
8313
|
+
color: var(--muted);
|
|
8314
|
+
}
|
|
8315
|
+
.grid {
|
|
8316
|
+
display: grid;
|
|
8317
|
+
gap: 10px;
|
|
8318
|
+
}
|
|
8319
|
+
.grid.two {
|
|
8320
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
8321
|
+
}
|
|
8322
|
+
a.inline {
|
|
8323
|
+
color: var(--accent);
|
|
8324
|
+
text-decoration: none;
|
|
8325
|
+
}
|
|
8326
|
+
.status-streaming { color: var(--amber); }
|
|
8327
|
+
.status-completed { color: var(--green); }
|
|
8328
|
+
.status-failed, .status-error { color: var(--red); }
|
|
8329
|
+
</style>
|
|
8330
|
+
</head>
|
|
8331
|
+
<body>
|
|
8332
|
+
<main class="wrap">
|
|
8333
|
+
${body}
|
|
8334
|
+
</main>
|
|
8335
|
+
</body>
|
|
8336
|
+
</html>`;
|
|
8337
|
+
}
|
|
8338
|
+
function renderSessionHandoffPage(snapshot) {
|
|
8339
|
+
const title = snapshot.session.name || snapshot.session.id;
|
|
8340
|
+
const hero = `
|
|
8341
|
+
<section class="hero">
|
|
8342
|
+
<div class="eyebrow"><span class="dot"></span> Secure Proxy Session Handoff</div>
|
|
8343
|
+
<h1>${escapeHtml(title)}</h1>
|
|
8344
|
+
<div class="meta">
|
|
8345
|
+
<span class="pill"><strong>ID</strong> ${escapeHtml(snapshot.session.id)}</span>
|
|
8346
|
+
<span class="pill"><strong>Adapter</strong> ${escapeHtml(snapshot.session.adapterType)}</span>
|
|
8347
|
+
<span class="pill"><strong>Status</strong> ${escapeHtml(snapshot.session.status)}</span>
|
|
8348
|
+
${snapshot.session.model ? `<span class="pill"><strong>Model</strong> ${escapeHtml(snapshot.session.model)}</span>` : ""}
|
|
8349
|
+
${snapshot.session.cwd ? `<span class="pill"><strong>Workspace</strong> ${escapeHtml(snapshot.session.cwd)}</span>` : ""}
|
|
8350
|
+
</div>
|
|
8351
|
+
</section>
|
|
8352
|
+
`;
|
|
8353
|
+
const turns = snapshot.turns.length > 0 ? snapshot.turns.map((turn) => {
|
|
8354
|
+
const turnBody = turn.blocks.length > 0 ? turn.blocks.map(({ block }) => renderBlock(block)).join("") : `<div class="empty">No blocks were captured for this turn.</div>`;
|
|
8355
|
+
return `
|
|
8356
|
+
<section class="turn">
|
|
8357
|
+
<div class="turn-head">
|
|
8358
|
+
<div>
|
|
8359
|
+
<div class="turn-title">${escapeHtml(turn.id)}</div>
|
|
8360
|
+
<div class="turn-subtitle">${formatTimestamp(turn.startedAt)}${turn.endedAt ? ` to ${formatTimestamp(turn.endedAt)}` : ""}</div>
|
|
8361
|
+
</div>
|
|
8362
|
+
<span class="pill"><strong>Turn</strong> <span class="status-${escapeHtml(turn.status)}">${escapeHtml(turn.status)}</span></span>
|
|
8363
|
+
</div>
|
|
8364
|
+
<div class="blocks">${turnBody}</div>
|
|
8365
|
+
</section>
|
|
8366
|
+
`;
|
|
8367
|
+
}).join("") : `<div class="empty">This session has not produced any turns yet.</div>`;
|
|
8368
|
+
return renderPage(title, `${hero}<section class="panel">${turns}</section>`);
|
|
8369
|
+
}
|
|
8370
|
+
function renderFileChangeHandoffPage(snapshot, turn, block) {
|
|
8371
|
+
const action = block.action;
|
|
8372
|
+
const title = action.path || snapshot.session.name || snapshot.session.id;
|
|
8373
|
+
const hero = `
|
|
8374
|
+
<section class="hero">
|
|
8375
|
+
<div class="eyebrow"><span class="dot"></span> Secure Proxy File Change</div>
|
|
8376
|
+
<h1>${escapeHtml(title)}</h1>
|
|
8377
|
+
<div class="meta">
|
|
8378
|
+
<span class="pill"><strong>Session</strong> ${escapeHtml(snapshot.session.name || snapshot.session.id)}</span>
|
|
8379
|
+
<span class="pill"><strong>Turn</strong> ${escapeHtml(turn.id)}</span>
|
|
8380
|
+
<span class="pill"><strong>Status</strong> ${escapeHtml(action.status)}</span>
|
|
8381
|
+
</div>
|
|
8382
|
+
</section>
|
|
8383
|
+
`;
|
|
8384
|
+
const body = `
|
|
8385
|
+
<section class="panel">
|
|
8386
|
+
<div class="panel-inner stack">
|
|
8387
|
+
<div class="grid two">
|
|
8388
|
+
<div class="pill"><strong>Path</strong> ${escapeHtml(action.path)}</div>
|
|
8389
|
+
<div class="pill"><strong>Block</strong> ${escapeHtml(block.id)}</div>
|
|
8390
|
+
</div>
|
|
8391
|
+
${action.diff ? renderDetailsSection("Diff", renderDiff(action.diff), true) : `<div class="empty">No diff was recorded for this action.</div>`}
|
|
8392
|
+
${action.output ? renderDetailsSection("Output", `<div class="code"><pre>${escapeHtml(action.output)}</pre></div>`) : ""}
|
|
8393
|
+
</div>
|
|
8394
|
+
</section>
|
|
8395
|
+
`;
|
|
8396
|
+
return renderPage(title, `${hero}${body}`);
|
|
8397
|
+
}
|
|
8398
|
+
function renderBlock(block) {
|
|
8399
|
+
switch (block.type) {
|
|
8400
|
+
case "text":
|
|
8401
|
+
return renderTextLikeBlock("Text", block.text);
|
|
8402
|
+
case "reasoning":
|
|
8403
|
+
return renderTextLikeBlock("Reasoning", block.text);
|
|
8404
|
+
case "error":
|
|
8405
|
+
return `
|
|
8406
|
+
<article class="block">
|
|
8407
|
+
<div class="block-head">
|
|
8408
|
+
<div class="block-type">Error</div>
|
|
8409
|
+
<span class="pill status-error">${escapeHtml(block.status)}</span>
|
|
8410
|
+
</div>
|
|
8411
|
+
<div class="code"><pre>${escapeHtml(block.message)}</pre></div>
|
|
8412
|
+
</article>
|
|
8413
|
+
`;
|
|
8414
|
+
case "file":
|
|
8415
|
+
return `
|
|
8416
|
+
<article class="block">
|
|
8417
|
+
<div class="block-head">
|
|
8418
|
+
<div class="block-type">File</div>
|
|
8419
|
+
<span class="pill">${escapeHtml(block.status)}</span>
|
|
8420
|
+
</div>
|
|
8421
|
+
<div class="grid">
|
|
8422
|
+
${block.name ? `<div class="label"><strong>Name</strong> ${escapeHtml(block.name)}</div>` : ""}
|
|
8423
|
+
<div class="label"><strong>MIME</strong> ${escapeHtml(block.mimeType)}</div>
|
|
8424
|
+
</div>
|
|
8425
|
+
</article>
|
|
8426
|
+
`;
|
|
8427
|
+
case "question":
|
|
8428
|
+
return renderQuestionBlock(block);
|
|
8429
|
+
case "action":
|
|
8430
|
+
return renderActionBlock(block);
|
|
8431
|
+
}
|
|
8432
|
+
}
|
|
8433
|
+
function renderTextLikeBlock(label, text) {
|
|
8434
|
+
return `
|
|
8435
|
+
<article class="block">
|
|
8436
|
+
<div class="block-head">
|
|
8437
|
+
<div class="block-type">${escapeHtml(label)}</div>
|
|
8438
|
+
</div>
|
|
8439
|
+
<div class="code"><pre>${escapeHtml(text)}</pre></div>
|
|
8440
|
+
</article>
|
|
8441
|
+
`;
|
|
8442
|
+
}
|
|
8443
|
+
function renderQuestionBlock(block) {
|
|
8444
|
+
const answer = block.answer?.length ? block.answer.join(", ") : "Awaiting answer";
|
|
8445
|
+
const options = block.options?.length ? `<div class="label"><strong>Options</strong> ${escapeHtml(block.options.map((option) => option.label).join(", "))}</div>` : "";
|
|
8446
|
+
return `
|
|
8447
|
+
<article class="block">
|
|
8448
|
+
<div class="block-head">
|
|
8449
|
+
<div class="block-type">Question</div>
|
|
8450
|
+
<span class="pill">${escapeHtml(block.questionStatus)}</span>
|
|
8451
|
+
</div>
|
|
8452
|
+
${block.header ? `<div class="label"><strong>${escapeHtml(block.header)}</strong></div>` : ""}
|
|
8453
|
+
<div class="code"><pre>${escapeHtml(block.question)}</pre></div>
|
|
8454
|
+
<div class="stack" style="margin-top: 10px;">
|
|
8455
|
+
${options}
|
|
8456
|
+
<div class="label"><strong>Answer</strong> ${escapeHtml(answer)}</div>
|
|
8457
|
+
</div>
|
|
8458
|
+
</article>
|
|
8459
|
+
`;
|
|
8460
|
+
}
|
|
8461
|
+
function renderActionBlock(block) {
|
|
8462
|
+
const action = block.action;
|
|
8463
|
+
const parts = [];
|
|
8464
|
+
switch (action.kind) {
|
|
8465
|
+
case "file_change":
|
|
8466
|
+
parts.push(`<div class="label"><strong>Path</strong> ${escapeHtml(action.path)}</div>`);
|
|
8467
|
+
if (action.diff) {
|
|
8468
|
+
parts.push(renderDetailsSection("Diff", renderDiff(action.diff)));
|
|
8469
|
+
}
|
|
8470
|
+
break;
|
|
8471
|
+
case "command":
|
|
8472
|
+
parts.push(`<div class="label"><strong>Command</strong></div><div class="code"><pre>${escapeHtml(action.command)}</pre></div>`);
|
|
8473
|
+
if (typeof action.exitCode === "number") {
|
|
8474
|
+
parts.push(`<div class="label"><strong>Exit Code</strong> ${escapeHtml(String(action.exitCode))}</div>`);
|
|
8475
|
+
}
|
|
8476
|
+
break;
|
|
8477
|
+
case "tool_call":
|
|
8478
|
+
parts.push(`<div class="label"><strong>Tool</strong> ${escapeHtml(action.toolName)}</div>`);
|
|
8479
|
+
if (action.toolCallId) {
|
|
8480
|
+
parts.push(`<div class="label"><strong>Call ID</strong> ${escapeHtml(action.toolCallId)}</div>`);
|
|
8481
|
+
}
|
|
8482
|
+
break;
|
|
8483
|
+
case "subagent":
|
|
8484
|
+
parts.push(`<div class="label"><strong>Agent</strong> ${escapeHtml(action.agentName || action.agentId)}</div>`);
|
|
8485
|
+
if (action.prompt) {
|
|
8486
|
+
parts.push(`<div class="code"><pre>${escapeHtml(action.prompt)}</pre></div>`);
|
|
8487
|
+
}
|
|
8488
|
+
break;
|
|
8489
|
+
}
|
|
8490
|
+
if (action.approval) {
|
|
8491
|
+
parts.push(`<div class="label"><strong>Approval</strong> ${escapeHtml(action.approval.risk || "medium")} risk${action.approval.description ? ` - ${escapeHtml(action.approval.description)}` : ""}</div>`);
|
|
8492
|
+
}
|
|
8493
|
+
if (action.output) {
|
|
8494
|
+
parts.push(renderDetailsSection("Output", `<div class="code"><pre>${escapeHtml(action.output)}</pre></div>`));
|
|
8495
|
+
}
|
|
8496
|
+
return `
|
|
8497
|
+
<article class="block">
|
|
8498
|
+
<div class="block-head">
|
|
8499
|
+
<div class="block-type">${escapeHtml(action.kind.replace("_", " "))}</div>
|
|
8500
|
+
<span class="pill">${escapeHtml(action.status)}</span>
|
|
8501
|
+
</div>
|
|
8502
|
+
<div class="stack">${parts.join("")}</div>
|
|
8503
|
+
</article>
|
|
8504
|
+
`;
|
|
8505
|
+
}
|
|
8506
|
+
function renderDetailsSection(title, innerHtml, open = false) {
|
|
8507
|
+
return `
|
|
8508
|
+
<details class="details"${open ? " open" : ""}>
|
|
8509
|
+
<summary>${escapeHtml(title)}</summary>
|
|
8510
|
+
<div class="panel-inner">${innerHtml}</div>
|
|
8511
|
+
</details>
|
|
8512
|
+
`;
|
|
8513
|
+
}
|
|
8514
|
+
function renderDiff(diff) {
|
|
8515
|
+
const lines = diff.split(`
|
|
8516
|
+
`).map((line) => {
|
|
8517
|
+
const className = line.startsWith("+") ? "diff-line-add" : line.startsWith("-") ? "diff-line-del" : "";
|
|
8518
|
+
return `<div class="${className}"><pre>${escapeHtml(line || " ")}</pre></div>`;
|
|
8519
|
+
}).join("");
|
|
8520
|
+
return `<div class="code">${lines}</div>`;
|
|
8521
|
+
}
|
|
8522
|
+
function escapeHtml(value) {
|
|
8523
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8524
|
+
}
|
|
8525
|
+
function formatTimestamp(value) {
|
|
8526
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
8527
|
+
return "Unknown time";
|
|
8528
|
+
}
|
|
8529
|
+
try {
|
|
8530
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
8531
|
+
dateStyle: "medium",
|
|
8532
|
+
timeStyle: "short"
|
|
8533
|
+
}).format(value);
|
|
8534
|
+
} catch {
|
|
8535
|
+
return new Date(value).toISOString();
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
7933
8538
|
|
|
7934
8539
|
// ../../apps/desktop/src/core/pairing/runtime/bridge/server.ts
|
|
7935
8540
|
import { readdirSync as readdirSync3, readFileSync as readFileSync9, realpathSync, statSync as statSync3 } from "fs";
|
|
@@ -12151,8 +12756,107 @@ async function shutdownClaudeStreamJsonAgent(options, shutdownOptions = {}) {
|
|
|
12151
12756
|
|
|
12152
12757
|
// ../runtime/src/codex-app-server.ts
|
|
12153
12758
|
import { spawn as spawn3 } from "child_process";
|
|
12154
|
-
import {
|
|
12759
|
+
import { constants as constants3 } from "fs";
|
|
12760
|
+
import { access as access3, appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
12155
12761
|
import { delimiter as delimiter3, join as join13 } from "path";
|
|
12762
|
+
function normalizeCodexModelValue(value) {
|
|
12763
|
+
const trimmed = value?.trim();
|
|
12764
|
+
return trimmed ? trimmed : null;
|
|
12765
|
+
}
|
|
12766
|
+
function encodeCodexModelConfig(model) {
|
|
12767
|
+
return `model=${JSON.stringify(model)}`;
|
|
12768
|
+
}
|
|
12769
|
+
function parseCodexModelConfig(value) {
|
|
12770
|
+
const trimmed = value?.trim();
|
|
12771
|
+
if (!trimmed) {
|
|
12772
|
+
return null;
|
|
12773
|
+
}
|
|
12774
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
12775
|
+
if (separatorIndex <= 0) {
|
|
12776
|
+
return null;
|
|
12777
|
+
}
|
|
12778
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
12779
|
+
if (key !== "model") {
|
|
12780
|
+
return null;
|
|
12781
|
+
}
|
|
12782
|
+
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
|
12783
|
+
if (!rawValue) {
|
|
12784
|
+
return null;
|
|
12785
|
+
}
|
|
12786
|
+
if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
12787
|
+
return rawValue.slice(1, -1) || null;
|
|
12788
|
+
}
|
|
12789
|
+
return rawValue;
|
|
12790
|
+
}
|
|
12791
|
+
function normalizeCodexAppServerLaunchArgs(launchArgs) {
|
|
12792
|
+
const args = Array.isArray(launchArgs) ? launchArgs.map((entry) => String(entry).trim()).filter(Boolean) : [];
|
|
12793
|
+
const normalized = [];
|
|
12794
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
12795
|
+
const current = args[index] ?? "";
|
|
12796
|
+
if (current === "--model" || current === "-m") {
|
|
12797
|
+
const model = normalizeCodexModelValue(args[index + 1]);
|
|
12798
|
+
if (model) {
|
|
12799
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
12800
|
+
index += 1;
|
|
12801
|
+
continue;
|
|
12802
|
+
}
|
|
12803
|
+
normalized.push(current);
|
|
12804
|
+
continue;
|
|
12805
|
+
}
|
|
12806
|
+
if (current.startsWith("--model=")) {
|
|
12807
|
+
const model = normalizeCodexModelValue(current.slice("--model=".length));
|
|
12808
|
+
if (model) {
|
|
12809
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
12810
|
+
continue;
|
|
12811
|
+
}
|
|
12812
|
+
}
|
|
12813
|
+
if (current.startsWith("-m=")) {
|
|
12814
|
+
const model = normalizeCodexModelValue(current.slice(3));
|
|
12815
|
+
if (model) {
|
|
12816
|
+
normalized.push("-c", encodeCodexModelConfig(model));
|
|
12817
|
+
continue;
|
|
12818
|
+
}
|
|
12819
|
+
}
|
|
12820
|
+
if (current === "-c" || current === "--config") {
|
|
12821
|
+
const next = args[index + 1];
|
|
12822
|
+
if (typeof next === "string") {
|
|
12823
|
+
const model = parseCodexModelConfig(next);
|
|
12824
|
+
normalized.push(current === "--config" ? "--config" : "-c", model ? encodeCodexModelConfig(model) : next);
|
|
12825
|
+
index += 1;
|
|
12826
|
+
continue;
|
|
12827
|
+
}
|
|
12828
|
+
}
|
|
12829
|
+
if (current.startsWith("--config=")) {
|
|
12830
|
+
const value = current.slice("--config=".length);
|
|
12831
|
+
const model = parseCodexModelConfig(value);
|
|
12832
|
+
normalized.push(model ? `--config=${encodeCodexModelConfig(model)}` : current);
|
|
12833
|
+
continue;
|
|
12834
|
+
}
|
|
12835
|
+
normalized.push(current);
|
|
12836
|
+
}
|
|
12837
|
+
return normalized;
|
|
12838
|
+
}
|
|
12839
|
+
function readCodexAppServerModelFromLaunchArgs(launchArgs) {
|
|
12840
|
+
const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
|
|
12841
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
12842
|
+
const current = normalized[index] ?? "";
|
|
12843
|
+
if (current === "-c" || current === "--config") {
|
|
12844
|
+
const model = parseCodexModelConfig(normalized[index + 1]);
|
|
12845
|
+
if (model) {
|
|
12846
|
+
return model;
|
|
12847
|
+
}
|
|
12848
|
+
index += 1;
|
|
12849
|
+
continue;
|
|
12850
|
+
}
|
|
12851
|
+
if (current.startsWith("--config=")) {
|
|
12852
|
+
const model = parseCodexModelConfig(current.slice("--config=".length));
|
|
12853
|
+
if (model) {
|
|
12854
|
+
return model;
|
|
12855
|
+
}
|
|
12856
|
+
}
|
|
12857
|
+
}
|
|
12858
|
+
return null;
|
|
12859
|
+
}
|
|
12156
12860
|
function sessionKey2(options) {
|
|
12157
12861
|
return `${options.agentName}:${options.sessionId}`;
|
|
12158
12862
|
}
|
|
@@ -12261,6 +12965,13 @@ function isMissingCodexRolloutError2(error) {
|
|
|
12261
12965
|
const message = errorMessage3(error).toLowerCase();
|
|
12262
12966
|
return message.includes("no rollout found for thread id");
|
|
12263
12967
|
}
|
|
12968
|
+
function resolveCodexCompletionGraceMs() {
|
|
12969
|
+
const parsed = Number.parseInt(process.env.OPENSCOUT_CODEX_COMPLETION_GRACE_MS ?? "", 10);
|
|
12970
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
12971
|
+
return parsed;
|
|
12972
|
+
}
|
|
12973
|
+
return 60000;
|
|
12974
|
+
}
|
|
12264
12975
|
|
|
12265
12976
|
class CodexAppServerSession {
|
|
12266
12977
|
options;
|
|
@@ -12450,7 +13161,7 @@ class CodexAppServerSession {
|
|
|
12450
13161
|
systemPrompt: options.systemPrompt,
|
|
12451
13162
|
threadId: options.threadId ?? null,
|
|
12452
13163
|
requireExistingThread: options.requireExistingThread === true,
|
|
12453
|
-
launchArgs:
|
|
13164
|
+
launchArgs: normalizeCodexAppServerLaunchArgs(options.launchArgs)
|
|
12454
13165
|
});
|
|
12455
13166
|
}
|
|
12456
13167
|
async ensureStarted() {
|
|
@@ -12472,6 +13183,7 @@ class CodexAppServerSession {
|
|
|
12472
13183
|
await mkdir5(this.options.logsDirectory, { recursive: true });
|
|
12473
13184
|
await writeFile5(join13(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
|
|
12474
13185
|
const codexExecutable = await resolveCodexExecutable2();
|
|
13186
|
+
const launchArgs = normalizeCodexAppServerLaunchArgs(this.options.launchArgs);
|
|
12475
13187
|
const env = buildManagedAgentEnvironment({
|
|
12476
13188
|
agentName: this.options.agentName,
|
|
12477
13189
|
currentDirectory: this.options.cwd,
|
|
@@ -12482,7 +13194,8 @@ class CodexAppServerSession {
|
|
|
12482
13194
|
...buildScoutMcpCodexLaunchArgs({
|
|
12483
13195
|
currentDirectory: this.options.cwd,
|
|
12484
13196
|
env
|
|
12485
|
-
})
|
|
13197
|
+
}),
|
|
13198
|
+
...launchArgs
|
|
12486
13199
|
], {
|
|
12487
13200
|
cwd: this.options.cwd,
|
|
12488
13201
|
env,
|
|
@@ -12580,6 +13293,8 @@ class CodexAppServerSession {
|
|
|
12580
13293
|
turnId: "",
|
|
12581
13294
|
startedAt: Date.now(),
|
|
12582
13295
|
timer: null,
|
|
13296
|
+
graceTimer: null,
|
|
13297
|
+
timedOutAt: null,
|
|
12583
13298
|
messageOrder: [],
|
|
12584
13299
|
messageByItemId: new Map,
|
|
12585
13300
|
resolve: resolve4,
|
|
@@ -12587,20 +13302,43 @@ class CodexAppServerSession {
|
|
|
12587
13302
|
watchers: []
|
|
12588
13303
|
};
|
|
12589
13304
|
turn.timer = setTimeout(() => {
|
|
12590
|
-
this.
|
|
12591
|
-
return;
|
|
12592
|
-
});
|
|
12593
|
-
if (this.activeTurn === turn) {
|
|
12594
|
-
this.activeTurn = null;
|
|
12595
|
-
}
|
|
12596
|
-
for (const watcher of this.drainTurnWatchers(turn)) {
|
|
12597
|
-
watcher.reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
|
|
12598
|
-
}
|
|
12599
|
-
reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
|
|
13305
|
+
this.scheduleTurnTimeout(turn, timeoutMs);
|
|
12600
13306
|
}, timeoutMs);
|
|
12601
13307
|
this.activeTurn = turn;
|
|
12602
13308
|
return turn;
|
|
12603
13309
|
}
|
|
13310
|
+
buildTurnTimeoutError(timeoutMs) {
|
|
13311
|
+
return new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`);
|
|
13312
|
+
}
|
|
13313
|
+
scheduleTurnTimeout(turn, timeoutMs) {
|
|
13314
|
+
if (turn.timedOutAt !== null) {
|
|
13315
|
+
return;
|
|
13316
|
+
}
|
|
13317
|
+
turn.timedOutAt = Date.now();
|
|
13318
|
+
this.interrupt().catch(() => {
|
|
13319
|
+
return;
|
|
13320
|
+
});
|
|
13321
|
+
const graceMs = resolveCodexCompletionGraceMs();
|
|
13322
|
+
if (graceMs <= 0) {
|
|
13323
|
+
this.rejectTimedOutTurn(turn, timeoutMs);
|
|
13324
|
+
return;
|
|
13325
|
+
}
|
|
13326
|
+
turn.graceTimer = setTimeout(() => {
|
|
13327
|
+
this.rejectTimedOutTurn(turn, timeoutMs);
|
|
13328
|
+
}, graceMs);
|
|
13329
|
+
}
|
|
13330
|
+
rejectTimedOutTurn(turn, timeoutMs) {
|
|
13331
|
+
if (this.activeTurn !== turn) {
|
|
13332
|
+
this.clearActiveTurn(turn);
|
|
13333
|
+
return;
|
|
13334
|
+
}
|
|
13335
|
+
this.clearActiveTurn(turn);
|
|
13336
|
+
const error = this.buildTurnTimeoutError(timeoutMs);
|
|
13337
|
+
for (const watcher of this.drainTurnWatchers(turn)) {
|
|
13338
|
+
watcher.reject(error);
|
|
13339
|
+
}
|
|
13340
|
+
turn.reject(error);
|
|
13341
|
+
}
|
|
12604
13342
|
addTurnWatcher(turn, resolve4, reject, timeoutMs) {
|
|
12605
13343
|
const watcher = {
|
|
12606
13344
|
resolve: resolve4,
|
|
@@ -12634,6 +13372,9 @@ class CodexAppServerSession {
|
|
|
12634
13372
|
if (turn.timer) {
|
|
12635
13373
|
clearTimeout(turn.timer);
|
|
12636
13374
|
}
|
|
13375
|
+
if (turn.graceTimer) {
|
|
13376
|
+
clearTimeout(turn.graceTimer);
|
|
13377
|
+
}
|
|
12637
13378
|
if (this.activeTurn === turn) {
|
|
12638
13379
|
this.activeTurn = null;
|
|
12639
13380
|
}
|
|
@@ -13833,6 +14574,128 @@ function normalizeLocalAgentCapabilities(value) {
|
|
|
13833
14574
|
function normalizeLocalAgentLaunchArgs(value) {
|
|
13834
14575
|
return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
|
|
13835
14576
|
}
|
|
14577
|
+
function normalizeRequestedModel(value) {
|
|
14578
|
+
const trimmed = value?.trim();
|
|
14579
|
+
return trimmed ? trimmed : undefined;
|
|
14580
|
+
}
|
|
14581
|
+
function readClaudeLaunchModel(launchArgs) {
|
|
14582
|
+
for (let index = 0;index < launchArgs.length; index += 1) {
|
|
14583
|
+
const current = launchArgs[index] ?? "";
|
|
14584
|
+
if (current === "--model") {
|
|
14585
|
+
const next = launchArgs[index + 1]?.trim();
|
|
14586
|
+
return next || undefined;
|
|
14587
|
+
}
|
|
14588
|
+
if (current.startsWith("--model=")) {
|
|
14589
|
+
const next = current.slice("--model=".length).trim();
|
|
14590
|
+
return next || undefined;
|
|
14591
|
+
}
|
|
14592
|
+
}
|
|
14593
|
+
return;
|
|
14594
|
+
}
|
|
14595
|
+
function normalizeLaunchArgsForHarness(harness, value) {
|
|
14596
|
+
const normalized = normalizeLocalAgentLaunchArgs(value);
|
|
14597
|
+
if (harness === "codex") {
|
|
14598
|
+
return normalizeCodexAppServerLaunchArgs(normalized);
|
|
14599
|
+
}
|
|
14600
|
+
return normalized;
|
|
14601
|
+
}
|
|
14602
|
+
function readLaunchModelForHarness(harness, launchArgs) {
|
|
14603
|
+
if (harness === "codex") {
|
|
14604
|
+
return readCodexAppServerModelFromLaunchArgs(launchArgs) ?? undefined;
|
|
14605
|
+
}
|
|
14606
|
+
if (harness === "claude") {
|
|
14607
|
+
return readClaudeLaunchModel(launchArgs ?? []);
|
|
14608
|
+
}
|
|
14609
|
+
return;
|
|
14610
|
+
}
|
|
14611
|
+
function stripLaunchModelForHarness(harness, launchArgs) {
|
|
14612
|
+
if (harness === "codex") {
|
|
14613
|
+
const next = [];
|
|
14614
|
+
const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
|
|
14615
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
14616
|
+
const current = normalized[index] ?? "";
|
|
14617
|
+
if (current === "-c" || current === "--config") {
|
|
14618
|
+
const value = normalized[index + 1];
|
|
14619
|
+
if (readCodexAppServerModelFromLaunchArgs(value ? [current, value] : [current])) {
|
|
14620
|
+
index += 1;
|
|
14621
|
+
continue;
|
|
14622
|
+
}
|
|
14623
|
+
next.push(current);
|
|
14624
|
+
if (value) {
|
|
14625
|
+
next.push(value);
|
|
14626
|
+
index += 1;
|
|
14627
|
+
}
|
|
14628
|
+
continue;
|
|
14629
|
+
}
|
|
14630
|
+
if (current.startsWith("--config=")) {
|
|
14631
|
+
if (readCodexAppServerModelFromLaunchArgs([current])) {
|
|
14632
|
+
continue;
|
|
14633
|
+
}
|
|
14634
|
+
}
|
|
14635
|
+
next.push(current);
|
|
14636
|
+
}
|
|
14637
|
+
return next;
|
|
14638
|
+
}
|
|
14639
|
+
if (harness === "claude") {
|
|
14640
|
+
const next = [];
|
|
14641
|
+
const normalized = normalizeLocalAgentLaunchArgs(launchArgs);
|
|
14642
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
14643
|
+
const current = normalized[index] ?? "";
|
|
14644
|
+
if (current === "--model") {
|
|
14645
|
+
index += 1;
|
|
14646
|
+
continue;
|
|
14647
|
+
}
|
|
14648
|
+
if (current.startsWith("--model=")) {
|
|
14649
|
+
continue;
|
|
14650
|
+
}
|
|
14651
|
+
next.push(current);
|
|
14652
|
+
}
|
|
14653
|
+
return next;
|
|
14654
|
+
}
|
|
14655
|
+
return normalizeLocalAgentLaunchArgs(launchArgs);
|
|
14656
|
+
}
|
|
14657
|
+
function buildLaunchArgsForRequestedModel(harness, model) {
|
|
14658
|
+
if (harness === "codex") {
|
|
14659
|
+
return normalizeCodexAppServerLaunchArgs(["--model", model]);
|
|
14660
|
+
}
|
|
14661
|
+
if (harness === "claude") {
|
|
14662
|
+
return ["--model", model];
|
|
14663
|
+
}
|
|
14664
|
+
return [];
|
|
14665
|
+
}
|
|
14666
|
+
function applyRequestedModelToLaunchArgs(harness, launchArgs, model) {
|
|
14667
|
+
const normalized = normalizeLaunchArgsForHarness(harness, launchArgs);
|
|
14668
|
+
const requestedModel = normalizeRequestedModel(model);
|
|
14669
|
+
if (!requestedModel) {
|
|
14670
|
+
return normalized;
|
|
14671
|
+
}
|
|
14672
|
+
return [
|
|
14673
|
+
...stripLaunchModelForHarness(harness, normalized),
|
|
14674
|
+
...buildLaunchArgsForRequestedModel(harness, requestedModel)
|
|
14675
|
+
];
|
|
14676
|
+
}
|
|
14677
|
+
function defaultHarnessForOverride(override, fallback = "claude") {
|
|
14678
|
+
return normalizeManagedHarness2(override.defaultHarness ?? override.runtime?.harness, fallback);
|
|
14679
|
+
}
|
|
14680
|
+
function launchArgsForOverrideHarness(override, harness) {
|
|
14681
|
+
const profileLaunchArgs = override.harnessProfiles?.[harness]?.launchArgs;
|
|
14682
|
+
if (profileLaunchArgs) {
|
|
14683
|
+
return normalizeLaunchArgsForHarness(harness, profileLaunchArgs);
|
|
14684
|
+
}
|
|
14685
|
+
if (harness === defaultHarnessForOverride(override, harness)) {
|
|
14686
|
+
return normalizeLaunchArgsForHarness(harness, override.launchArgs);
|
|
14687
|
+
}
|
|
14688
|
+
return [];
|
|
14689
|
+
}
|
|
14690
|
+
function overrideHarnessProfile(override, harness) {
|
|
14691
|
+
const profile = override.harnessProfiles?.[harness];
|
|
14692
|
+
return {
|
|
14693
|
+
cwd: normalizeProjectPath(profile?.cwd || override.runtime?.cwd || override.projectRoot || process.cwd()),
|
|
14694
|
+
transport: normalizeLocalAgentTransport(profile?.transport ?? override.runtime?.transport, harness),
|
|
14695
|
+
sessionId: normalizeTmuxSessionName2(profile?.sessionId, `${override.agentId}-${harness}`),
|
|
14696
|
+
launchArgs: launchArgsForOverrideHarness(override, harness)
|
|
14697
|
+
};
|
|
14698
|
+
}
|
|
13836
14699
|
function normalizeManagedHarness2(value, fallback) {
|
|
13837
14700
|
return value === "codex" ? "codex" : value === "claude" ? "claude" : fallback;
|
|
13838
14701
|
}
|
|
@@ -13848,7 +14711,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
13848
14711
|
cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
|
|
13849
14712
|
transport: normalizeLocalAgentTransport(profile.transport, harness),
|
|
13850
14713
|
sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
|
|
13851
|
-
launchArgs:
|
|
14714
|
+
launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
|
|
13852
14715
|
};
|
|
13853
14716
|
}
|
|
13854
14717
|
const runtimeHarness = normalizeManagedHarness2(record.harness, defaultHarness);
|
|
@@ -13857,7 +14720,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
13857
14720
|
cwd: normalizeProjectPath(record.cwd || process.cwd()),
|
|
13858
14721
|
transport: normalizeLocalAgentTransport(record.transport, runtimeHarness),
|
|
13859
14722
|
sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${runtimeHarness}`),
|
|
13860
|
-
launchArgs:
|
|
14723
|
+
launchArgs: normalizeLaunchArgsForHarness(runtimeHarness, record.launchArgs)
|
|
13861
14724
|
};
|
|
13862
14725
|
}
|
|
13863
14726
|
if (!nextProfiles[defaultHarness]) {
|
|
@@ -13865,7 +14728,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
13865
14728
|
cwd: normalizeProjectPath(record.cwd || process.cwd()),
|
|
13866
14729
|
transport: normalizeLocalAgentTransport(record.transport, defaultHarness),
|
|
13867
14730
|
sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${defaultHarness}`),
|
|
13868
|
-
launchArgs:
|
|
14731
|
+
launchArgs: normalizeLaunchArgsForHarness(defaultHarness, record.launchArgs)
|
|
13869
14732
|
};
|
|
13870
14733
|
}
|
|
13871
14734
|
for (const harness of ["claude", "codex"]) {
|
|
@@ -13877,7 +14740,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
|
|
|
13877
14740
|
cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
|
|
13878
14741
|
transport: normalizeLocalAgentTransport(profile.transport, harness),
|
|
13879
14742
|
sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
|
|
13880
|
-
launchArgs:
|
|
14743
|
+
launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
|
|
13881
14744
|
};
|
|
13882
14745
|
}
|
|
13883
14746
|
return nextProfiles;
|
|
@@ -13901,14 +14764,14 @@ function recordForHarness(record, harnessOverride) {
|
|
|
13901
14764
|
tmuxSession: fallbackSessionId,
|
|
13902
14765
|
cwd: fallbackCwd,
|
|
13903
14766
|
transport: fallbackTransport,
|
|
13904
|
-
launchArgs:
|
|
14767
|
+
launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs),
|
|
13905
14768
|
harnessProfiles: {
|
|
13906
14769
|
...normalized.harnessProfiles,
|
|
13907
14770
|
[selectedHarness]: {
|
|
13908
14771
|
cwd: fallbackCwd,
|
|
13909
14772
|
transport: fallbackTransport,
|
|
13910
14773
|
sessionId: fallbackSessionId,
|
|
13911
|
-
launchArgs:
|
|
14774
|
+
launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs)
|
|
13912
14775
|
}
|
|
13913
14776
|
}
|
|
13914
14777
|
};
|
|
@@ -13950,7 +14813,7 @@ function normalizeLocalAgentRecord(agentId, record) {
|
|
|
13950
14813
|
harnessProfiles,
|
|
13951
14814
|
transport: activeProfile?.transport ?? normalizeLocalAgentTransport(record.transport, harness),
|
|
13952
14815
|
capabilities: normalizeLocalAgentCapabilities(record.capabilities),
|
|
13953
|
-
launchArgs: activeProfile?.launchArgs ??
|
|
14816
|
+
launchArgs: activeProfile?.launchArgs ?? normalizeLaunchArgsForHarness(harness, record.launchArgs)
|
|
13954
14817
|
};
|
|
13955
14818
|
}
|
|
13956
14819
|
function localAgentStatusFromRecord(agentId, record, source) {
|
|
@@ -14019,7 +14882,7 @@ function relayAgentOverrideFromLocalAgentRecord(agentId, record, existing) {
|
|
|
14019
14882
|
source: existing?.source && existing.source !== "inferred" ? existing.source : "manual",
|
|
14020
14883
|
startedAt: normalizedRecord.startedAt,
|
|
14021
14884
|
systemPrompt: normalizedRecord.systemPrompt,
|
|
14022
|
-
launchArgs:
|
|
14885
|
+
launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs),
|
|
14023
14886
|
capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
|
|
14024
14887
|
defaultHarness: normalizeManagedHarness2(normalizedRecord.defaultHarness, "claude"),
|
|
14025
14888
|
harnessProfiles: normalizedRecord.harnessProfiles,
|
|
@@ -14040,7 +14903,7 @@ function buildCodexAgentSessionOptions(agentName, record, systemPrompt) {
|
|
|
14040
14903
|
systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "codex_app_server" }),
|
|
14041
14904
|
runtimeDirectory: relayAgentRuntimeDirectory(agentName),
|
|
14042
14905
|
logsDirectory: relayAgentLogsDirectory(agentName),
|
|
14043
|
-
launchArgs:
|
|
14906
|
+
launchArgs: normalizeLaunchArgsForHarness("codex", record.launchArgs)
|
|
14044
14907
|
};
|
|
14045
14908
|
}
|
|
14046
14909
|
function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
|
|
@@ -14051,7 +14914,7 @@ function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
|
|
|
14051
14914
|
systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "claude_stream_json" }),
|
|
14052
14915
|
runtimeDirectory: relayAgentRuntimeDirectory(agentName),
|
|
14053
14916
|
logsDirectory: relayAgentLogsDirectory(agentName),
|
|
14054
|
-
launchArgs:
|
|
14917
|
+
launchArgs: normalizeLaunchArgsForHarness("claude", record.launchArgs)
|
|
14055
14918
|
};
|
|
14056
14919
|
}
|
|
14057
14920
|
function isLocalAgentRecordOnline(agentName, record) {
|
|
@@ -14085,7 +14948,7 @@ function buildLocalAgentBootstrapPrompt(_harness, _systemPrompt, initialMessage)
|
|
|
14085
14948
|
return initialMessage;
|
|
14086
14949
|
}
|
|
14087
14950
|
function buildLocalAgentLaunchCommand(agentName, record, projectPath, promptFile, workerScript) {
|
|
14088
|
-
const extraArgs = shellQuoteArguments(
|
|
14951
|
+
const extraArgs = shellQuoteArguments(normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs));
|
|
14089
14952
|
if (normalizeLocalAgentHarness(record.harness) === "codex") {
|
|
14090
14953
|
return `exec bash ${JSON.stringify(workerScript ?? join15(relayAgentRuntimeDirectory(agentName), "codex-worker.sh"))}`;
|
|
14091
14954
|
}
|
|
@@ -14351,6 +15214,7 @@ async function startLocalAgent(input) {
|
|
|
14351
15214
|
const effectiveHarness = normalizeManagedHarness2(preferredHarness ?? configDefaultHarness, "claude");
|
|
14352
15215
|
const transport = normalizeLocalAgentTransport(undefined, effectiveHarness);
|
|
14353
15216
|
const sessionId = normalizeTmuxSessionName2(undefined, `${instance.id}-${effectiveHarness}`);
|
|
15217
|
+
const launchArgs = applyRequestedModelToLaunchArgs(effectiveHarness, [], input.model);
|
|
14354
15218
|
const existingForInstance = overrides[instance.id];
|
|
14355
15219
|
if (existingForInstance && normalizeProjectPath(existingForInstance.projectRoot) !== projectRoot) {
|
|
14356
15220
|
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.`);
|
|
@@ -14364,13 +15228,14 @@ async function startLocalAgent(input) {
|
|
|
14364
15228
|
projectConfigPath: coldProjectConfigPath,
|
|
14365
15229
|
source: "manual",
|
|
14366
15230
|
startedAt: nowSeconds2(),
|
|
15231
|
+
launchArgs,
|
|
14367
15232
|
defaultHarness: effectiveHarness,
|
|
14368
15233
|
harnessProfiles: {
|
|
14369
15234
|
[effectiveHarness]: {
|
|
14370
15235
|
cwd: effectiveCwd ?? projectRoot,
|
|
14371
15236
|
transport,
|
|
14372
15237
|
sessionId,
|
|
14373
|
-
launchArgs
|
|
15238
|
+
launchArgs
|
|
14374
15239
|
}
|
|
14375
15240
|
},
|
|
14376
15241
|
runtime: {
|
|
@@ -14391,6 +15256,9 @@ async function startLocalAgent(input) {
|
|
|
14391
15256
|
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.`);
|
|
14392
15257
|
}
|
|
14393
15258
|
const resolvedHarness = preferredHarness ?? matchingOverride.runtime?.harness;
|
|
15259
|
+
const nextHarness = normalizeManagedHarness2(resolvedHarness, "claude");
|
|
15260
|
+
const nextDefaultHarness = preferredHarness ? normalizeManagedHarness2(preferredHarness, "claude") : defaultHarnessForOverride(matchingOverride, "claude");
|
|
15261
|
+
const nextLaunchArgs = applyRequestedModelToLaunchArgs(nextHarness, launchArgsForOverrideHarness(matchingOverride, nextHarness), input.model);
|
|
14394
15262
|
overrides[instance.id] = {
|
|
14395
15263
|
agentId: instance.id,
|
|
14396
15264
|
definitionId: requestedDefinitionId,
|
|
@@ -14401,10 +15269,16 @@ async function startLocalAgent(input) {
|
|
|
14401
15269
|
source: "manual",
|
|
14402
15270
|
startedAt: matchingOverride.startedAt ?? nowSeconds2(),
|
|
14403
15271
|
systemPrompt: matchingOverride.systemPrompt,
|
|
14404
|
-
launchArgs: matchingOverride.launchArgs,
|
|
15272
|
+
launchArgs: nextHarness === nextDefaultHarness ? nextLaunchArgs : matchingOverride.launchArgs,
|
|
14405
15273
|
capabilities: matchingOverride.capabilities,
|
|
14406
|
-
defaultHarness:
|
|
14407
|
-
harnessProfiles:
|
|
15274
|
+
defaultHarness: nextDefaultHarness,
|
|
15275
|
+
harnessProfiles: {
|
|
15276
|
+
...matchingOverride.harnessProfiles ?? {},
|
|
15277
|
+
[nextHarness]: {
|
|
15278
|
+
...overrideHarnessProfile(matchingOverride, nextHarness),
|
|
15279
|
+
launchArgs: nextLaunchArgs
|
|
15280
|
+
}
|
|
15281
|
+
},
|
|
14408
15282
|
runtime: {
|
|
14409
15283
|
cwd: effectiveCwd ?? matchingOverride.runtime?.cwd ?? matchingProjectRoot,
|
|
14410
15284
|
harness: resolvedHarness,
|
|
@@ -14416,6 +15290,22 @@ async function startLocalAgent(input) {
|
|
|
14416
15290
|
await writeRelayAgentOverrides(overrides);
|
|
14417
15291
|
targetAgentId = instance.id;
|
|
14418
15292
|
} else {
|
|
15293
|
+
if (input.model) {
|
|
15294
|
+
const existingHarness = normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness ?? matchingOverride.runtime?.harness, "claude");
|
|
15295
|
+
const nextLaunchArgs = applyRequestedModelToLaunchArgs(existingHarness, launchArgsForOverrideHarness(matchingOverride, existingHarness), input.model);
|
|
15296
|
+
overrides[matchingAgentId] = {
|
|
15297
|
+
...matchingOverride,
|
|
15298
|
+
launchArgs: existingHarness === defaultHarnessForOverride(matchingOverride, existingHarness) ? nextLaunchArgs : matchingOverride.launchArgs,
|
|
15299
|
+
harnessProfiles: {
|
|
15300
|
+
...matchingOverride.harnessProfiles ?? {},
|
|
15301
|
+
[existingHarness]: {
|
|
15302
|
+
...overrideHarnessProfile(matchingOverride, existingHarness),
|
|
15303
|
+
launchArgs: nextLaunchArgs
|
|
15304
|
+
}
|
|
15305
|
+
}
|
|
15306
|
+
};
|
|
15307
|
+
await writeRelayAgentOverrides(overrides);
|
|
15308
|
+
}
|
|
14419
15309
|
targetAgentId = matchingAgentId;
|
|
14420
15310
|
}
|
|
14421
15311
|
await ensureLocalAgentBindingOnline(targetAgentId, process.env.OPENSCOUT_NODE_ID ?? "local", {
|
|
@@ -14511,6 +15401,7 @@ function readPersistedClaudeSessionId(agentName) {
|
|
|
14511
15401
|
}
|
|
14512
15402
|
function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
14513
15403
|
const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
|
|
15404
|
+
const configuredModel = readLaunchModelForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs);
|
|
14514
15405
|
const definitionId = normalizedRecord.definitionId ?? agentId;
|
|
14515
15406
|
const displayName = titleCaseLocalAgentName(definitionId);
|
|
14516
15407
|
const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
|
|
@@ -14535,7 +15426,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
14535
15426
|
defaultSelector: instance.defaultSelector,
|
|
14536
15427
|
nodeQualifier: instance.nodeQualifier,
|
|
14537
15428
|
workspaceQualifier: instance.workspaceQualifier,
|
|
14538
|
-
branch: instance.branch
|
|
15429
|
+
branch: instance.branch,
|
|
15430
|
+
...configuredModel ? { model: configuredModel } : {}
|
|
14539
15431
|
}
|
|
14540
15432
|
},
|
|
14541
15433
|
agent: {
|
|
@@ -14562,7 +15454,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
14562
15454
|
defaultSelector: instance.defaultSelector,
|
|
14563
15455
|
nodeQualifier: instance.nodeQualifier,
|
|
14564
15456
|
workspaceQualifier: instance.workspaceQualifier,
|
|
14565
|
-
branch: instance.branch
|
|
15457
|
+
branch: instance.branch,
|
|
15458
|
+
...configuredModel ? { model: configuredModel } : {}
|
|
14566
15459
|
},
|
|
14567
15460
|
agentClass: "general",
|
|
14568
15461
|
capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
|
|
@@ -14595,6 +15488,7 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
|
|
|
14595
15488
|
nodeQualifier: instance.nodeQualifier,
|
|
14596
15489
|
workspaceQualifier: instance.workspaceQualifier,
|
|
14597
15490
|
branch: instance.branch,
|
|
15491
|
+
...configuredModel ? { model: configuredModel } : {},
|
|
14598
15492
|
...externalSessionId ? { externalSessionId } : {}
|
|
14599
15493
|
}
|
|
14600
15494
|
}
|
|
@@ -15971,6 +16865,20 @@ function readSyncStatus(bridge, sessionId) {
|
|
|
15971
16865
|
oldestBufferedSeq: bridge.oldestBufferedSeq(sessionId)
|
|
15972
16866
|
};
|
|
15973
16867
|
}
|
|
16868
|
+
function resolveSyncSessionId(bridge, preferredSessionId) {
|
|
16869
|
+
if (preferredSessionId) {
|
|
16870
|
+
return preferredSessionId;
|
|
16871
|
+
}
|
|
16872
|
+
let latestSessionId = null;
|
|
16873
|
+
let latestActivityAt = Number.NEGATIVE_INFINITY;
|
|
16874
|
+
for (const session of bridge.getSessionSummaries()) {
|
|
16875
|
+
if (session.lastActivityAt > latestActivityAt) {
|
|
16876
|
+
latestActivityAt = session.lastActivityAt;
|
|
16877
|
+
latestSessionId = session.sessionId;
|
|
16878
|
+
}
|
|
16879
|
+
}
|
|
16880
|
+
return latestSessionId;
|
|
16881
|
+
}
|
|
15974
16882
|
function sessionRegistryErrorResponse(reqId, error) {
|
|
15975
16883
|
if (!isSessionRegistryError(error)) {
|
|
15976
16884
|
return null;
|
|
@@ -16040,23 +16948,36 @@ async function handleRPCInner(bridge, req, deviceId) {
|
|
|
16040
16948
|
return { id: req.id, result: { ok: true } };
|
|
16041
16949
|
}
|
|
16042
16950
|
case "sync/replay": {
|
|
16043
|
-
const p = req.params;
|
|
16044
|
-
if (
|
|
16045
|
-
return { id: req.id, error: { code: -32602, message: "
|
|
16951
|
+
const p = req.params ?? {};
|
|
16952
|
+
if (typeof p.lastSeq !== "number") {
|
|
16953
|
+
return { id: req.id, error: { code: -32602, message: "lastSeq is required" } };
|
|
16954
|
+
}
|
|
16955
|
+
const sessionId = resolveSyncSessionId(bridge, p.sessionId);
|
|
16956
|
+
if (!sessionId) {
|
|
16957
|
+
return { id: req.id, result: { events: [] } };
|
|
16046
16958
|
}
|
|
16047
|
-
const events2 = replaySyncEvents(bridge,
|
|
16959
|
+
const events2 = replaySyncEvents(bridge, sessionId, p.lastSeq);
|
|
16048
16960
|
return { id: req.id, result: { events: events2 } };
|
|
16049
16961
|
}
|
|
16050
16962
|
case "sync/status": {
|
|
16051
16963
|
const p = req.params ?? {};
|
|
16052
|
-
|
|
16053
|
-
|
|
16964
|
+
const sessionCount = bridge.listSessions().length;
|
|
16965
|
+
const sessionId = resolveSyncSessionId(bridge, p.sessionId);
|
|
16966
|
+
if (!sessionId) {
|
|
16967
|
+
return {
|
|
16968
|
+
id: req.id,
|
|
16969
|
+
result: {
|
|
16970
|
+
currentSeq: 0,
|
|
16971
|
+
oldestBufferedSeq: 0,
|
|
16972
|
+
sessionCount
|
|
16973
|
+
}
|
|
16974
|
+
};
|
|
16054
16975
|
}
|
|
16055
16976
|
return {
|
|
16056
16977
|
id: req.id,
|
|
16057
16978
|
result: {
|
|
16058
|
-
...readSyncStatus(bridge,
|
|
16059
|
-
sessionCount
|
|
16979
|
+
...readSyncStatus(bridge, sessionId),
|
|
16980
|
+
sessionCount
|
|
16060
16981
|
}
|
|
16061
16982
|
};
|
|
16062
16983
|
}
|
|
@@ -34044,6 +34965,64 @@ var mobileRouter = t.router({
|
|
|
34044
34965
|
limit: typeof input.limit === "number" ? input.limit : null
|
|
34045
34966
|
}, resolveMobileCurrentDirectory2());
|
|
34046
34967
|
}),
|
|
34968
|
+
webHandoff: procedure.input(exports_external.object({
|
|
34969
|
+
kind: exports_external.enum(["session", "file_change"]),
|
|
34970
|
+
sessionId: exports_external.string(),
|
|
34971
|
+
turnId: exports_external.string().optional(),
|
|
34972
|
+
blockId: exports_external.string().optional()
|
|
34973
|
+
})).mutation(({ input, ctx }) => {
|
|
34974
|
+
if (!ctx.deviceId) {
|
|
34975
|
+
throw new TRPCError({
|
|
34976
|
+
code: "UNAUTHORIZED",
|
|
34977
|
+
message: "Secure web handoff requires a paired mobile device"
|
|
34978
|
+
});
|
|
34979
|
+
}
|
|
34980
|
+
const snapshot = ctx.bridge.getSessionSnapshot(input.sessionId);
|
|
34981
|
+
if (!snapshot) {
|
|
34982
|
+
throw new TRPCError({
|
|
34983
|
+
code: "NOT_FOUND",
|
|
34984
|
+
message: `No session: ${input.sessionId}`
|
|
34985
|
+
});
|
|
34986
|
+
}
|
|
34987
|
+
let scope;
|
|
34988
|
+
let title = snapshot.session.name || snapshot.session.id;
|
|
34989
|
+
if (input.kind === "file_change") {
|
|
34990
|
+
if (!input.turnId || !input.blockId) {
|
|
34991
|
+
throw new TRPCError({
|
|
34992
|
+
code: "BAD_REQUEST",
|
|
34993
|
+
message: "turnId and blockId are required for file_change handoffs"
|
|
34994
|
+
});
|
|
34995
|
+
}
|
|
34996
|
+
const turn = snapshot.turns.find((candidate) => candidate.id === input.turnId);
|
|
34997
|
+
const block = turn?.blocks.find((candidate) => candidate.block.id === input.blockId)?.block;
|
|
34998
|
+
if (!turn || !block || block.type !== "action" || block.action.kind !== "file_change") {
|
|
34999
|
+
throw new TRPCError({
|
|
35000
|
+
code: "NOT_FOUND",
|
|
35001
|
+
message: "File change block not found"
|
|
35002
|
+
});
|
|
35003
|
+
}
|
|
35004
|
+
scope = {
|
|
35005
|
+
kind: "file_change",
|
|
35006
|
+
sessionId: input.sessionId,
|
|
35007
|
+
turnId: input.turnId,
|
|
35008
|
+
blockId: input.blockId
|
|
35009
|
+
};
|
|
35010
|
+
title = block.action.path || title;
|
|
35011
|
+
} else {
|
|
35012
|
+
scope = {
|
|
35013
|
+
kind: "session",
|
|
35014
|
+
sessionId: input.sessionId
|
|
35015
|
+
};
|
|
35016
|
+
}
|
|
35017
|
+
const issued = issueWebHandoff(scope, ctx.deviceId);
|
|
35018
|
+
return {
|
|
35019
|
+
kind: input.kind,
|
|
35020
|
+
path: pathForWebHandoffScope(scope),
|
|
35021
|
+
token: issued.token,
|
|
35022
|
+
expiresAt: issued.expiresAt,
|
|
35023
|
+
title
|
|
35024
|
+
};
|
|
35025
|
+
}),
|
|
34047
35026
|
createSession: procedure.input(exports_external.object({
|
|
34048
35027
|
workspaceId: exports_external.string(),
|
|
34049
35028
|
harness: exports_external.string().optional(),
|
|
@@ -34257,15 +35236,42 @@ function readSyncStatus2(bridge, sessionId) {
|
|
|
34257
35236
|
oldestBufferedSeq: bridge.oldestBufferedSeq(sessionId)
|
|
34258
35237
|
};
|
|
34259
35238
|
}
|
|
35239
|
+
function resolveSyncSessionId2(bridge, preferredSessionId) {
|
|
35240
|
+
if (preferredSessionId) {
|
|
35241
|
+
return preferredSessionId;
|
|
35242
|
+
}
|
|
35243
|
+
let latestSessionId = null;
|
|
35244
|
+
let latestActivityAt = Number.NEGATIVE_INFINITY;
|
|
35245
|
+
for (const session of bridge.getSessionSummaries()) {
|
|
35246
|
+
if (session.lastActivityAt > latestActivityAt) {
|
|
35247
|
+
latestActivityAt = session.lastActivityAt;
|
|
35248
|
+
latestSessionId = session.sessionId;
|
|
35249
|
+
}
|
|
35250
|
+
}
|
|
35251
|
+
return latestSessionId;
|
|
35252
|
+
}
|
|
34260
35253
|
var syncRouter = t.router({
|
|
34261
|
-
replay: procedure.input(exports_external.object({ lastSeq: exports_external.number(), sessionId: exports_external.string() })).query(({ input, ctx }) => {
|
|
34262
|
-
const
|
|
35254
|
+
replay: procedure.input(exports_external.object({ lastSeq: exports_external.number(), sessionId: exports_external.string().optional() })).query(({ input, ctx }) => {
|
|
35255
|
+
const sessionId = resolveSyncSessionId2(ctx.bridge, input.sessionId);
|
|
35256
|
+
if (!sessionId) {
|
|
35257
|
+
return { events: [] };
|
|
35258
|
+
}
|
|
35259
|
+
const events2 = replaySyncEvents2(ctx.bridge, sessionId, input.lastSeq);
|
|
34263
35260
|
return { events: events2 };
|
|
34264
35261
|
}),
|
|
34265
|
-
status: procedure.input(exports_external.object({ sessionId: exports_external.string() })).query(({ input, ctx }) => {
|
|
35262
|
+
status: procedure.input(exports_external.object({ sessionId: exports_external.string().optional() }).optional()).query(({ input, ctx }) => {
|
|
35263
|
+
const sessionCount = ctx.bridge.listSessions().length;
|
|
35264
|
+
const sessionId = resolveSyncSessionId2(ctx.bridge, input?.sessionId);
|
|
35265
|
+
if (!sessionId) {
|
|
35266
|
+
return {
|
|
35267
|
+
currentSeq: 0,
|
|
35268
|
+
oldestBufferedSeq: 0,
|
|
35269
|
+
sessionCount
|
|
35270
|
+
};
|
|
35271
|
+
}
|
|
34266
35272
|
return {
|
|
34267
|
-
...readSyncStatus2(ctx.bridge,
|
|
34268
|
-
sessionCount
|
|
35273
|
+
...readSyncStatus2(ctx.bridge, sessionId),
|
|
35274
|
+
sessionCount
|
|
34269
35275
|
};
|
|
34270
35276
|
})
|
|
34271
35277
|
});
|
|
@@ -38767,7 +39773,7 @@ async function startPairingRuntime(options) {
|
|
|
38767
39773
|
secure: config2.secure,
|
|
38768
39774
|
identity: config2.secure ? identity2 : undefined
|
|
38769
39775
|
});
|
|
38770
|
-
const fileServer = startFileServer({ port: config2.port + 2 });
|
|
39776
|
+
const fileServer = startFileServer({ port: config2.port + 2, bridge });
|
|
38771
39777
|
const relayUrl = options?.relayUrl?.trim() || config2.relay || null;
|
|
38772
39778
|
if (!relayUrl) {
|
|
38773
39779
|
fileServer.stop();
|