@threadbase-sh/streamer 1.58.0 → 1.58.2
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/cli.cjs +296 -129
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +215 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +257 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -907,6 +907,21 @@ var import_child_process = require("child_process");
|
|
|
907
907
|
var import_fs3 = require("fs");
|
|
908
908
|
var import_os2 = require("os");
|
|
909
909
|
var import_path3 = require("path");
|
|
910
|
+
|
|
911
|
+
// src/providers.ts
|
|
912
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
913
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
914
|
+
function isProviderName(value) {
|
|
915
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
916
|
+
}
|
|
917
|
+
function coerceProviderForRunner(value) {
|
|
918
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
|
|
919
|
+
}
|
|
920
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
921
|
+
return availabilityResumable;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/platform.ts
|
|
910
925
|
var isWindows = (0, import_os2.platform)() === "win32";
|
|
911
926
|
var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
|
|
912
927
|
function isWindowsExecutablePath(path) {
|
|
@@ -1036,18 +1051,38 @@ function resolveCodexExe() {
|
|
|
1036
1051
|
_codexExe = "codex";
|
|
1037
1052
|
return _codexExe;
|
|
1038
1053
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1054
|
+
function isExecutableFile(path) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (!(0, import_fs3.statSync)(path).isFile()) return false;
|
|
1057
|
+
(0, import_fs3.accessSync)(path, import_fs3.constants.X_OK);
|
|
1058
|
+
return true;
|
|
1059
|
+
} catch {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1045
1062
|
}
|
|
1046
|
-
function
|
|
1047
|
-
return
|
|
1063
|
+
function locateExecutable(exe) {
|
|
1064
|
+
if (/[\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;
|
|
1065
|
+
const names = isWindows ? [
|
|
1066
|
+
...isWindowsExecutablePath(exe) ? [exe] : [],
|
|
1067
|
+
...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`)
|
|
1068
|
+
] : [exe];
|
|
1069
|
+
for (const dir of (process.env.PATH ?? "").split(import_path3.delimiter)) {
|
|
1070
|
+
if (!dir) continue;
|
|
1071
|
+
for (const name of names) {
|
|
1072
|
+
const candidate = (0, import_path3.join)(dir, name);
|
|
1073
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1048
1077
|
}
|
|
1049
|
-
function
|
|
1050
|
-
|
|
1078
|
+
function locateProviderExe(provider) {
|
|
1079
|
+
const isCodex = provider === CODEX_CLI_PROVIDER;
|
|
1080
|
+
const found = locateExecutable(isCodex ? resolveCodexExe() : resolveClaudeExe());
|
|
1081
|
+
if (found === null) {
|
|
1082
|
+
if (isCodex) clearCodexExeCache();
|
|
1083
|
+
else clearClaudeExeCache();
|
|
1084
|
+
}
|
|
1085
|
+
return found;
|
|
1051
1086
|
}
|
|
1052
1087
|
|
|
1053
1088
|
// src/pty-shared.ts
|
|
@@ -1305,11 +1340,19 @@ var CodexPtyRunner = class {
|
|
|
1305
1340
|
onStatusChange;
|
|
1306
1341
|
onPhaseChange;
|
|
1307
1342
|
onReady;
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1343
|
+
// Every Codex prompt the client can answer — startup gates (directory trust,
|
|
1344
|
+
// hooks review), command approvals, and the rate-limit model picker — is
|
|
1345
|
+
// broadcast through this one channel; null dismisses the card once the prompt
|
|
1346
|
+
// leaves the screen.
|
|
1347
|
+
//
|
|
1348
|
+
// Deliberately NOT onLiveQuestion/onLiveQuestionGone, which is Claude's
|
|
1349
|
+
// AskUserQuestion transport. Both channels land on the same mobile
|
|
1350
|
+
// QuestionCard, and the permission one is the correct fit for Codex: its menus
|
|
1351
|
+
// are answered by the option's real on-screen number (parseCodexNumberedOptions
|
|
1352
|
+
// emits `answerKeys: "2\r"`), which is exactly what `permissionIndices` carries
|
|
1353
|
+
// and what AskUserQuestion's down-arrow-count model cannot express. Wiring the
|
|
1354
|
+
// question channel as well would be a second path to the same card.
|
|
1310
1355
|
onPermissionChange;
|
|
1311
|
-
onLiveQuestion;
|
|
1312
|
-
onLiveQuestionGone;
|
|
1313
1356
|
onUserMessage;
|
|
1314
1357
|
log;
|
|
1315
1358
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
@@ -1359,8 +1402,6 @@ var CodexPtyRunner = class {
|
|
|
1359
1402
|
this.onPhaseChange = options.onPhaseChange;
|
|
1360
1403
|
this.onReady = options.onReady;
|
|
1361
1404
|
this.onPermissionChange = options.onPermissionChange;
|
|
1362
|
-
this.onLiveQuestion = options.onLiveQuestion;
|
|
1363
|
-
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
1364
1405
|
this.onUserMessage = options.onUserMessage;
|
|
1365
1406
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
1366
1407
|
}
|
|
@@ -3668,11 +3709,13 @@ var LiveSessionManager = class {
|
|
|
3668
3709
|
async start(sessionId, options) {
|
|
3669
3710
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3670
3711
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3712
|
+
this.assertProviderInstalled(provider);
|
|
3671
3713
|
return runner.start(sessionId, options);
|
|
3672
3714
|
}
|
|
3673
3715
|
async startFresh(options) {
|
|
3674
3716
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3675
3717
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3718
|
+
this.assertProviderInstalled(provider);
|
|
3676
3719
|
return runner.startFresh(options);
|
|
3677
3720
|
}
|
|
3678
3721
|
/**
|
|
@@ -3691,6 +3734,7 @@ var LiveSessionManager = class {
|
|
|
3691
3734
|
err.statusCode = 501;
|
|
3692
3735
|
throw err;
|
|
3693
3736
|
}
|
|
3737
|
+
this.assertProviderInstalled(provider);
|
|
3694
3738
|
return runner.startFork(options);
|
|
3695
3739
|
}
|
|
3696
3740
|
sendInput(sessionId, input) {
|
|
@@ -3772,6 +3816,30 @@ var LiveSessionManager = class {
|
|
|
3772
3816
|
}
|
|
3773
3817
|
throw new Error(`Session not found: ${sessionId}`);
|
|
3774
3818
|
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Refuse before spawning when the provider's CLI is not on this machine.
|
|
3821
|
+
*
|
|
3822
|
+
* Without this the spawn "succeeds": on POSIX execvp fails inside the forked
|
|
3823
|
+
* child, so a session appears, exits ~12ms later with code 1 and no output,
|
|
3824
|
+
* and the caller is told only that it "exited before becoming ready" — or,
|
|
3825
|
+
* on the Claude resume path, is told nothing at all, since that path answers
|
|
3826
|
+
* 200 before the process has had a chance to die. Every start route funnels
|
|
3827
|
+
* through here, so one check covers start, resume, adopt and fork.
|
|
3828
|
+
*
|
|
3829
|
+
* 503, not 500: the request was well-formed and the fault is this machine's
|
|
3830
|
+
* environment. `code` is what mobile branches on (it reads `errBody.code`),
|
|
3831
|
+
* and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
|
|
3832
|
+
*/
|
|
3833
|
+
assertProviderInstalled(provider) {
|
|
3834
|
+
if (locateProviderExe(provider) !== null) return;
|
|
3835
|
+
const command = provider === CODEX_CLI_PROVIDER ? "codex" : "claude";
|
|
3836
|
+
const err = new Error(
|
|
3837
|
+
`The ${command} command was not found on this server. Install the ${provider} CLI, or make sure it is on the PATH the streamer runs with.`
|
|
3838
|
+
);
|
|
3839
|
+
err.statusCode = 503;
|
|
3840
|
+
err.code = "PROVIDER_NOT_INSTALLED";
|
|
3841
|
+
throw err;
|
|
3842
|
+
}
|
|
3775
3843
|
assertSupportedProvider(provider, projectPath) {
|
|
3776
3844
|
if (this.remoteRunner) return this.remoteRunner;
|
|
3777
3845
|
const runner = this.runners.get(provider);
|
|
@@ -4454,7 +4522,9 @@ var corsMiddleware = (configValue) => {
|
|
|
4454
4522
|
// src/api/middleware/error.middleware.ts
|
|
4455
4523
|
var errorMiddleware = (err, c) => {
|
|
4456
4524
|
const message = err instanceof Error ? err.message : "Internal server error";
|
|
4457
|
-
|
|
4525
|
+
const { statusCode, code } = err;
|
|
4526
|
+
const status = typeof statusCode === "number" && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
|
|
4527
|
+
return c.json(typeof code === "string" ? { error: message, code } : { error: message }, status);
|
|
4458
4528
|
};
|
|
4459
4529
|
|
|
4460
4530
|
// src/api/routes/backup.routes.ts
|
|
@@ -5009,24 +5079,26 @@ function computeBootToken() {
|
|
|
5009
5079
|
}
|
|
5010
5080
|
|
|
5011
5081
|
// src/api/routes/diagnostics.routes.ts
|
|
5012
|
-
function providerCheck(name
|
|
5082
|
+
function providerCheck(name) {
|
|
5013
5083
|
try {
|
|
5014
|
-
const exe =
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5084
|
+
const exe = locateProviderExe(name);
|
|
5085
|
+
if (exe !== null) {
|
|
5086
|
+
return {
|
|
5087
|
+
id: `provider:${name}`,
|
|
5088
|
+
status: "ok",
|
|
5089
|
+
summary: `${name} CLI is installed.`,
|
|
5090
|
+
remediation: "NONE",
|
|
5091
|
+
detail: { location: redactPath(exe) }
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5022
5094
|
} catch {
|
|
5023
|
-
return {
|
|
5024
|
-
id: `provider:${name}`,
|
|
5025
|
-
status: "failed",
|
|
5026
|
-
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5027
|
-
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5028
|
-
};
|
|
5029
5095
|
}
|
|
5096
|
+
return {
|
|
5097
|
+
id: `provider:${name}`,
|
|
5098
|
+
status: "failed",
|
|
5099
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5100
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5101
|
+
};
|
|
5030
5102
|
}
|
|
5031
5103
|
var createDiagnosticsRoutes = (deps) => {
|
|
5032
5104
|
const app = new import_hono8.Hono();
|
|
@@ -5039,8 +5111,8 @@ var createDiagnosticsRoutes = (deps) => {
|
|
|
5039
5111
|
remediation: "NONE",
|
|
5040
5112
|
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
5041
5113
|
});
|
|
5042
|
-
checks.push(providerCheck(
|
|
5043
|
-
checks.push(providerCheck(
|
|
5114
|
+
checks.push(providerCheck(CLAUDE_CODE_PROVIDER));
|
|
5115
|
+
checks.push(providerCheck(CODEX_CLI_PROVIDER));
|
|
5044
5116
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
5045
5117
|
checks.push(
|
|
5046
5118
|
cacheAlert ? {
|
|
@@ -6112,7 +6184,10 @@ function parseVersionOutput(output) {
|
|
|
6112
6184
|
const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
6113
6185
|
return match ? match[0] : null;
|
|
6114
6186
|
}
|
|
6187
|
+
var versionByExe = /* @__PURE__ */ new Map();
|
|
6115
6188
|
function runVersion(exe) {
|
|
6189
|
+
const cached3 = versionByExe.get(exe);
|
|
6190
|
+
if (cached3 !== void 0) return Promise.resolve(cached3);
|
|
6116
6191
|
const viaShell = isWindows && /\.(?:cmd|bat)$/i.test(exe);
|
|
6117
6192
|
const file = viaShell ? `"${exe}"` : exe;
|
|
6118
6193
|
return new Promise((resolve2) => {
|
|
@@ -6122,7 +6197,9 @@ function runVersion(exe) {
|
|
|
6122
6197
|
{ timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
|
|
6123
6198
|
(err, stdout, stderr) => {
|
|
6124
6199
|
if (err && !stdout && !stderr) return resolve2(null);
|
|
6125
|
-
|
|
6200
|
+
const version = parseVersionOutput(`${stdout}${stderr}`);
|
|
6201
|
+
if (version !== null) versionByExe.set(exe, version);
|
|
6202
|
+
resolve2(version);
|
|
6126
6203
|
}
|
|
6127
6204
|
);
|
|
6128
6205
|
});
|
|
@@ -6166,13 +6243,16 @@ function compareSemver(a, b) {
|
|
|
6166
6243
|
if (pb.pre === null) return -1;
|
|
6167
6244
|
return pa.pre < pb.pre ? -1 : 1;
|
|
6168
6245
|
}
|
|
6169
|
-
async function providerHealth(name,
|
|
6246
|
+
async function providerHealth(name, locateExe = () => locateProviderExe(name), detect = runVersion) {
|
|
6170
6247
|
const verifiedAgainst = VERIFIED_AGAINST[name];
|
|
6171
6248
|
const capabilities = capabilitiesFor(name);
|
|
6172
|
-
let exe;
|
|
6249
|
+
let exe = null;
|
|
6173
6250
|
try {
|
|
6174
|
-
exe =
|
|
6251
|
+
exe = locateExe();
|
|
6175
6252
|
} catch {
|
|
6253
|
+
exe = null;
|
|
6254
|
+
}
|
|
6255
|
+
if (exe === null) {
|
|
6176
6256
|
return {
|
|
6177
6257
|
name,
|
|
6178
6258
|
available: false,
|
|
@@ -6209,8 +6289,8 @@ var createProviderRoutes = () => {
|
|
|
6209
6289
|
const app = new import_hono14.Hono();
|
|
6210
6290
|
app.get("/", async (c) => {
|
|
6211
6291
|
const providers = await Promise.all([
|
|
6212
|
-
providerHealth(CLAUDE_CODE_PROVIDER
|
|
6213
|
-
providerHealth(CODEX_CLI_PROVIDER
|
|
6292
|
+
providerHealth(CLAUDE_CODE_PROVIDER),
|
|
6293
|
+
providerHealth(CODEX_CLI_PROVIDER)
|
|
6214
6294
|
]);
|
|
6215
6295
|
return c.json({ providers });
|
|
6216
6296
|
});
|
|
@@ -10681,7 +10761,10 @@ var SessionHandlers = class {
|
|
|
10681
10761
|
});
|
|
10682
10762
|
this.sessionStore.addManaged(session);
|
|
10683
10763
|
this.registryBoot.recordSessionSpawn(session);
|
|
10684
|
-
const { outcome } = await this.deps.waitForStartupOutcome(
|
|
10764
|
+
const { outcome, session: settled } = await this.deps.waitForStartupOutcome(
|
|
10765
|
+
session.id,
|
|
10766
|
+
START_READY_TIMEOUT_MS
|
|
10767
|
+
);
|
|
10685
10768
|
const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());
|
|
10686
10769
|
if (outcome === "ready" && current) {
|
|
10687
10770
|
json(res, 200, { session: current });
|
|
@@ -10689,7 +10772,7 @@ var SessionHandlers = class {
|
|
|
10689
10772
|
json(res, 502, {
|
|
10690
10773
|
id: session.id,
|
|
10691
10774
|
status: "idle",
|
|
10692
|
-
error:
|
|
10775
|
+
error: settled?.failureReason ?? "Session exited before becoming ready"
|
|
10693
10776
|
});
|
|
10694
10777
|
} else {
|
|
10695
10778
|
json(res, 202, { id: session.id, status: "pending" });
|
|
@@ -10703,11 +10786,16 @@ var SessionHandlers = class {
|
|
|
10703
10786
|
} catch (err) {
|
|
10704
10787
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
10705
10788
|
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
10789
|
+
const code = err.code;
|
|
10706
10790
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
10707
10791
|
event: "session.start_failed",
|
|
10708
10792
|
error: message
|
|
10709
10793
|
});
|
|
10710
|
-
json(
|
|
10794
|
+
json(
|
|
10795
|
+
res,
|
|
10796
|
+
statusCode,
|
|
10797
|
+
typeof code === "string" ? { error: message, code } : { error: message }
|
|
10798
|
+
);
|
|
10711
10799
|
}
|
|
10712
10800
|
}
|
|
10713
10801
|
async handleSetSessionName(sessionId, req, res) {
|
|
@@ -11391,7 +11479,37 @@ var PairTokenStore = class {
|
|
|
11391
11479
|
expiresInSeconds: Math.floor(this.ttlMs / 1e3)
|
|
11392
11480
|
};
|
|
11393
11481
|
}
|
|
11482
|
+
/**
|
|
11483
|
+
* Whether `consume` would succeed right now, WITHOUT spending the token.
|
|
11484
|
+
*
|
|
11485
|
+
* Exists so a caller can reject a bad token before doing any work, and still
|
|
11486
|
+
* spend the token only once the work has succeeded. A pair token is
|
|
11487
|
+
* single-use and lives 180 seconds, so spending it on a request that then
|
|
11488
|
+
* fails costs the user a whole new QR — and, worse, makes their retry
|
|
11489
|
+
* indistinguishable from an attacker replaying a photographed code, which is
|
|
11490
|
+
* the one signal `design.md` §2.6 designates as replay detection.
|
|
11491
|
+
*
|
|
11492
|
+
* Advisory, not a reservation: it takes no lock and holds nothing. The
|
|
11493
|
+
* authoritative answer is still `consume`'s.
|
|
11494
|
+
*/
|
|
11495
|
+
verify(token) {
|
|
11496
|
+
const result = this.check(token);
|
|
11497
|
+
return result.ok ? { ok: true } : result;
|
|
11498
|
+
}
|
|
11394
11499
|
consume(token) {
|
|
11500
|
+
const result = this.check(token);
|
|
11501
|
+
if (!result.ok) return result;
|
|
11502
|
+
result.record.used = true;
|
|
11503
|
+
return { ok: true };
|
|
11504
|
+
}
|
|
11505
|
+
/**
|
|
11506
|
+
* The shared predicate behind `verify` and `consume`.
|
|
11507
|
+
*
|
|
11508
|
+
* One implementation on purpose: two copies of "is this token usable" is two
|
|
11509
|
+
* places for the expiry or single-use rule to drift, and a drift in this
|
|
11510
|
+
* direction fails open.
|
|
11511
|
+
*/
|
|
11512
|
+
check(token) {
|
|
11395
11513
|
const record2 = this.current;
|
|
11396
11514
|
if (!record2 || record2.token !== token) return { ok: false, reason: "unknown" };
|
|
11397
11515
|
if (Date.now() > record2.expiresAt) {
|
|
@@ -11399,8 +11517,7 @@ var PairTokenStore = class {
|
|
|
11399
11517
|
return { ok: false, reason: "expired" };
|
|
11400
11518
|
}
|
|
11401
11519
|
if (record2.used) return { ok: false, reason: "used" };
|
|
11402
|
-
|
|
11403
|
-
return { ok: true };
|
|
11520
|
+
return { ok: true, record: record2 };
|
|
11404
11521
|
}
|
|
11405
11522
|
peek() {
|
|
11406
11523
|
return this.current;
|
|
@@ -12299,7 +12416,15 @@ function createLiveSessionOptions(deps) {
|
|
|
12299
12416
|
// grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
|
|
12300
12417
|
// genuine process exit ("process-exit"), and reports both as
|
|
12301
12418
|
// `lifecycle: "completed"`. See managedToResponse in session-store.ts.
|
|
12302
|
-
...session.statusSource != null && { statusSource: session.statusSource }
|
|
12419
|
+
...session.statusSource != null && { statusSource: session.statusSource },
|
|
12420
|
+
// Why a session died, not just that it did. Without this the store's
|
|
12421
|
+
// copy has no failureReason, so managedToResponse falls through to
|
|
12422
|
+
// `lifecycle: "completed"` (see session-store.ts) and a session that
|
|
12423
|
+
// never started — missing CLI, missing project dir — is reported to
|
|
12424
|
+
// every client as one that finished normally. Guarded like its
|
|
12425
|
+
// neighbours: a later transition must not blank a recorded failure.
|
|
12426
|
+
...session.failureReason != null && { failureReason: session.failureReason },
|
|
12427
|
+
...session.failureCode != null && { failureCode: session.failureCode }
|
|
12303
12428
|
});
|
|
12304
12429
|
deps.managedSessionsRepo()?.recordStatus(
|
|
12305
12430
|
session.id,
|
|
@@ -15754,6 +15879,41 @@ var StreamerServer = class {
|
|
|
15754
15879
|
json(res, 503, body);
|
|
15755
15880
|
return true;
|
|
15756
15881
|
}
|
|
15882
|
+
/**
|
|
15883
|
+
* Say which provider CLIs this machine can actually launch.
|
|
15884
|
+
*
|
|
15885
|
+
* The operator cannot discover this case unaided: under launchd/Task
|
|
15886
|
+
* Scheduler the service inherits a stripped PATH, so a CLI that works
|
|
15887
|
+
* perfectly in their terminal is invisible to the service, and every session
|
|
15888
|
+
* start dies milliseconds in. `/api/diagnostics` answers it too, but only for
|
|
15889
|
+
* someone who already suspects it.
|
|
15890
|
+
*
|
|
15891
|
+
* Availability only, never a version — `--version` costs a process spawn per
|
|
15892
|
+
* provider (85ms for claude here) and belongs on the first request that wants
|
|
15893
|
+
* it, not on boot.
|
|
15894
|
+
*
|
|
15895
|
+
* Called AFTER the port is bound, which is not cosmetic. This is the first
|
|
15896
|
+
* caller of the exe resolvers in the process, so the memo is cold by
|
|
15897
|
+
* definition and each provider pays one synchronous `which` / `where.exe`
|
|
15898
|
+
* (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
|
|
15899
|
+
* Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
|
|
15900
|
+
* event loop, and Task Scheduler's stripped PATH is exactly where a miss
|
|
15901
|
+
* pays the full timeout — so the worst case is ~6s of two blocking lookups.
|
|
15902
|
+
* After `listen()` that delays the first requests on a box that cannot start
|
|
15903
|
+
* a session anyway; before it, it would have delayed binding the port.
|
|
15904
|
+
*/
|
|
15905
|
+
logProviderAvailability() {
|
|
15906
|
+
for (const provider of [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER]) {
|
|
15907
|
+
if (locateProviderExe(provider)) {
|
|
15908
|
+
this.log.info(`Provider ${provider}: found`, { event: "config.provider", provider });
|
|
15909
|
+
} else {
|
|
15910
|
+
this.log.warn(`Provider ${provider}: not found on PATH \u2014 sessions cannot start`, {
|
|
15911
|
+
event: "config.provider_missing",
|
|
15912
|
+
provider
|
|
15913
|
+
});
|
|
15914
|
+
}
|
|
15915
|
+
}
|
|
15916
|
+
}
|
|
15757
15917
|
async listen(port, opts) {
|
|
15758
15918
|
if (this.featureFlags.ptyHost) {
|
|
15759
15919
|
try {
|
|
@@ -15814,6 +15974,7 @@ var StreamerServer = class {
|
|
|
15814
15974
|
event: "server.listening",
|
|
15815
15975
|
...this.host !== void 0 && { host: this.host }
|
|
15816
15976
|
});
|
|
15977
|
+
this.logProviderAvailability();
|
|
15817
15978
|
try {
|
|
15818
15979
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15819
15980
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
@@ -16190,9 +16351,9 @@ var StreamerServer = class {
|
|
|
16190
16351
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16191
16352
|
return;
|
|
16192
16353
|
}
|
|
16193
|
-
const
|
|
16194
|
-
if (!
|
|
16195
|
-
json(res, 401, { error: `Pair token ${
|
|
16354
|
+
const precheck = this.pairTokens.verify(token);
|
|
16355
|
+
if (!precheck.ok) {
|
|
16356
|
+
json(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
16196
16357
|
return;
|
|
16197
16358
|
}
|
|
16198
16359
|
let sealed;
|
|
@@ -16203,6 +16364,11 @@ var StreamerServer = class {
|
|
|
16203
16364
|
json(res, 400, { error: message });
|
|
16204
16365
|
return;
|
|
16205
16366
|
}
|
|
16367
|
+
const result = this.pairTokens.consume(token);
|
|
16368
|
+
if (!result.ok) {
|
|
16369
|
+
json(res, 401, { error: `Pair token ${result.reason}` });
|
|
16370
|
+
return;
|
|
16371
|
+
}
|
|
16206
16372
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
16207
16373
|
this.log.info(`[pair] token exchanged from ${ip} at ${ts}`, {
|
|
16208
16374
|
event: "pair.token_exchanged",
|