@threadbase-sh/streamer 1.58.1 → 1.58.3
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 +260 -123
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +179 -43
- 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 +221 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -851,9 +851,24 @@ import { basename } from "path";
|
|
|
851
851
|
|
|
852
852
|
// src/platform.ts
|
|
853
853
|
import { execFileSync } from "child_process";
|
|
854
|
-
import { existsSync } from "fs";
|
|
854
|
+
import { accessSync, constants, existsSync, statSync } from "fs";
|
|
855
855
|
import { homedir as homedir2, platform } from "os";
|
|
856
|
-
import { join as join4 } from "path";
|
|
856
|
+
import { delimiter, join as join4 } from "path";
|
|
857
|
+
|
|
858
|
+
// src/providers.ts
|
|
859
|
+
var CLAUDE_CODE_PROVIDER = "claude-code";
|
|
860
|
+
var CODEX_CLI_PROVIDER = "codex-cli";
|
|
861
|
+
function isProviderName(value) {
|
|
862
|
+
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
863
|
+
}
|
|
864
|
+
function coerceProviderForRunner(value) {
|
|
865
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
|
|
866
|
+
}
|
|
867
|
+
function isProviderResumable(_provider, availabilityResumable) {
|
|
868
|
+
return availabilityResumable;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/platform.ts
|
|
857
872
|
var isWindows = platform() === "win32";
|
|
858
873
|
var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
|
|
859
874
|
function isWindowsExecutablePath(path) {
|
|
@@ -983,18 +998,38 @@ function resolveCodexExe() {
|
|
|
983
998
|
_codexExe = "codex";
|
|
984
999
|
return _codexExe;
|
|
985
1000
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1001
|
+
function isExecutableFile(path) {
|
|
1002
|
+
try {
|
|
1003
|
+
if (!statSync(path).isFile()) return false;
|
|
1004
|
+
accessSync(path, constants.X_OK);
|
|
1005
|
+
return true;
|
|
1006
|
+
} catch {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
992
1009
|
}
|
|
993
|
-
function
|
|
994
|
-
return
|
|
1010
|
+
function locateExecutable(exe) {
|
|
1011
|
+
if (/[\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;
|
|
1012
|
+
const names = isWindows ? [
|
|
1013
|
+
...isWindowsExecutablePath(exe) ? [exe] : [],
|
|
1014
|
+
...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`)
|
|
1015
|
+
] : [exe];
|
|
1016
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
1017
|
+
if (!dir) continue;
|
|
1018
|
+
for (const name of names) {
|
|
1019
|
+
const candidate = join4(dir, name);
|
|
1020
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
995
1024
|
}
|
|
996
|
-
function
|
|
997
|
-
|
|
1025
|
+
function locateProviderExe(provider) {
|
|
1026
|
+
const isCodex = provider === CODEX_CLI_PROVIDER;
|
|
1027
|
+
const found = locateExecutable(isCodex ? resolveCodexExe() : resolveClaudeExe());
|
|
1028
|
+
if (found === null) {
|
|
1029
|
+
if (isCodex) clearCodexExeCache();
|
|
1030
|
+
else clearClaudeExeCache();
|
|
1031
|
+
}
|
|
1032
|
+
return found;
|
|
998
1033
|
}
|
|
999
1034
|
|
|
1000
1035
|
// src/pty-shared.ts
|
|
@@ -3621,11 +3656,13 @@ var LiveSessionManager = class {
|
|
|
3621
3656
|
async start(sessionId, options) {
|
|
3622
3657
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3623
3658
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3659
|
+
this.assertProviderInstalled(provider);
|
|
3624
3660
|
return runner.start(sessionId, options);
|
|
3625
3661
|
}
|
|
3626
3662
|
async startFresh(options) {
|
|
3627
3663
|
const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
|
|
3628
3664
|
const runner = this.assertSupportedProvider(provider, options.projectPath);
|
|
3665
|
+
this.assertProviderInstalled(provider);
|
|
3629
3666
|
return runner.startFresh(options);
|
|
3630
3667
|
}
|
|
3631
3668
|
/**
|
|
@@ -3644,6 +3681,7 @@ var LiveSessionManager = class {
|
|
|
3644
3681
|
err.statusCode = 501;
|
|
3645
3682
|
throw err;
|
|
3646
3683
|
}
|
|
3684
|
+
this.assertProviderInstalled(provider);
|
|
3647
3685
|
return runner.startFork(options);
|
|
3648
3686
|
}
|
|
3649
3687
|
sendInput(sessionId, input) {
|
|
@@ -3725,6 +3763,30 @@ var LiveSessionManager = class {
|
|
|
3725
3763
|
}
|
|
3726
3764
|
throw new Error(`Session not found: ${sessionId}`);
|
|
3727
3765
|
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Refuse before spawning when the provider's CLI is not on this machine.
|
|
3768
|
+
*
|
|
3769
|
+
* Without this the spawn "succeeds": on POSIX execvp fails inside the forked
|
|
3770
|
+
* child, so a session appears, exits ~12ms later with code 1 and no output,
|
|
3771
|
+
* and the caller is told only that it "exited before becoming ready" — or,
|
|
3772
|
+
* on the Claude resume path, is told nothing at all, since that path answers
|
|
3773
|
+
* 200 before the process has had a chance to die. Every start route funnels
|
|
3774
|
+
* through here, so one check covers start, resume, adopt and fork.
|
|
3775
|
+
*
|
|
3776
|
+
* 503, not 500: the request was well-formed and the fault is this machine's
|
|
3777
|
+
* environment. `code` is what mobile branches on (it reads `errBody.code`),
|
|
3778
|
+
* and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
|
|
3779
|
+
*/
|
|
3780
|
+
assertProviderInstalled(provider) {
|
|
3781
|
+
if (locateProviderExe(provider) !== null) return;
|
|
3782
|
+
const command = provider === CODEX_CLI_PROVIDER ? "codex" : "claude";
|
|
3783
|
+
const err = new Error(
|
|
3784
|
+
`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.`
|
|
3785
|
+
);
|
|
3786
|
+
err.statusCode = 503;
|
|
3787
|
+
err.code = "PROVIDER_NOT_INSTALLED";
|
|
3788
|
+
throw err;
|
|
3789
|
+
}
|
|
3728
3790
|
assertSupportedProvider(provider, projectPath) {
|
|
3729
3791
|
if (this.remoteRunner) return this.remoteRunner;
|
|
3730
3792
|
const runner = this.runners.get(provider);
|
|
@@ -4407,7 +4469,9 @@ var corsMiddleware = (configValue) => {
|
|
|
4407
4469
|
// src/api/middleware/error.middleware.ts
|
|
4408
4470
|
var errorMiddleware = (err, c) => {
|
|
4409
4471
|
const message = err instanceof Error ? err.message : "Internal server error";
|
|
4410
|
-
|
|
4472
|
+
const { statusCode, code } = err;
|
|
4473
|
+
const status = typeof statusCode === "number" && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
|
|
4474
|
+
return c.json(typeof code === "string" ? { error: message, code } : { error: message }, status);
|
|
4411
4475
|
};
|
|
4412
4476
|
|
|
4413
4477
|
// src/api/routes/backup.routes.ts
|
|
@@ -4962,24 +5026,26 @@ function computeBootToken() {
|
|
|
4962
5026
|
}
|
|
4963
5027
|
|
|
4964
5028
|
// src/api/routes/diagnostics.routes.ts
|
|
4965
|
-
function providerCheck(name
|
|
5029
|
+
function providerCheck(name) {
|
|
4966
5030
|
try {
|
|
4967
|
-
const exe =
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5031
|
+
const exe = locateProviderExe(name);
|
|
5032
|
+
if (exe !== null) {
|
|
5033
|
+
return {
|
|
5034
|
+
id: `provider:${name}`,
|
|
5035
|
+
status: "ok",
|
|
5036
|
+
summary: `${name} CLI is installed.`,
|
|
5037
|
+
remediation: "NONE",
|
|
5038
|
+
detail: { location: redactPath(exe) }
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
4975
5041
|
} catch {
|
|
4976
|
-
return {
|
|
4977
|
-
id: `provider:${name}`,
|
|
4978
|
-
status: "failed",
|
|
4979
|
-
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4980
|
-
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4981
|
-
};
|
|
4982
5042
|
}
|
|
5043
|
+
return {
|
|
5044
|
+
id: `provider:${name}`,
|
|
5045
|
+
status: "failed",
|
|
5046
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
5047
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
5048
|
+
};
|
|
4983
5049
|
}
|
|
4984
5050
|
var createDiagnosticsRoutes = (deps) => {
|
|
4985
5051
|
const app = new Hono8();
|
|
@@ -4992,8 +5058,8 @@ var createDiagnosticsRoutes = (deps) => {
|
|
|
4992
5058
|
remediation: "NONE",
|
|
4993
5059
|
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4994
5060
|
});
|
|
4995
|
-
checks.push(providerCheck(
|
|
4996
|
-
checks.push(providerCheck(
|
|
5061
|
+
checks.push(providerCheck(CLAUDE_CODE_PROVIDER));
|
|
5062
|
+
checks.push(providerCheck(CODEX_CLI_PROVIDER));
|
|
4997
5063
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4998
5064
|
checks.push(
|
|
4999
5065
|
cacheAlert ? {
|
|
@@ -5098,7 +5164,7 @@ var createHealthRoutes = (deps) => {
|
|
|
5098
5164
|
};
|
|
5099
5165
|
|
|
5100
5166
|
// src/api/routes/logs.routes.ts
|
|
5101
|
-
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
|
|
5167
|
+
import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
|
|
5102
5168
|
import { join as join8 } from "path";
|
|
5103
5169
|
import { Hono as Hono10 } from "hono";
|
|
5104
5170
|
|
|
@@ -5118,7 +5184,7 @@ function resolveLogPath(source) {
|
|
|
5118
5184
|
function pickDefaultSource() {
|
|
5119
5185
|
for (const source of ["stdout", "stderr", "dev"]) {
|
|
5120
5186
|
const p = resolveLogPath(source);
|
|
5121
|
-
if (existsSync5(p) &&
|
|
5187
|
+
if (existsSync5(p) && statSync2(p).size > 0) return source;
|
|
5122
5188
|
}
|
|
5123
5189
|
return "stdout";
|
|
5124
5190
|
}
|
|
@@ -5176,7 +5242,7 @@ function createLogsRoutes() {
|
|
|
5176
5242
|
});
|
|
5177
5243
|
}
|
|
5178
5244
|
const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
|
|
5179
|
-
const stats =
|
|
5245
|
+
const stats = statSync2(logPath);
|
|
5180
5246
|
return c.json({
|
|
5181
5247
|
logs: lines,
|
|
5182
5248
|
offset,
|
|
@@ -5206,7 +5272,7 @@ function createLogsRoutes() {
|
|
|
5206
5272
|
if (!existsSync5(logPath)) {
|
|
5207
5273
|
return { source, exists: false, total: 0, fileSize: 0 };
|
|
5208
5274
|
}
|
|
5209
|
-
const stats =
|
|
5275
|
+
const stats = statSync2(logPath);
|
|
5210
5276
|
return {
|
|
5211
5277
|
source,
|
|
5212
5278
|
exists: true,
|
|
@@ -5584,7 +5650,7 @@ function publicKeyOf(privateKey) {
|
|
|
5584
5650
|
|
|
5585
5651
|
// src/services/push/apnsClient.ts
|
|
5586
5652
|
import { createSign } from "crypto";
|
|
5587
|
-
import { connect, constants } from "http2";
|
|
5653
|
+
import { connect, constants as constants2 } from "http2";
|
|
5588
5654
|
var log = getLogger("apns");
|
|
5589
5655
|
var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
|
|
5590
5656
|
var APNS_MAX_PAYLOAD_BYTES = 4096;
|
|
@@ -5691,27 +5757,27 @@ var ApnsClient = class {
|
|
|
5691
5757
|
}
|
|
5692
5758
|
const session = this.getSession();
|
|
5693
5759
|
const headers = {
|
|
5694
|
-
[
|
|
5695
|
-
[
|
|
5696
|
-
[
|
|
5760
|
+
[constants2.HTTP2_HEADER_METHOD]: "POST",
|
|
5761
|
+
[constants2.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
|
|
5762
|
+
[constants2.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
|
|
5697
5763
|
"apns-push-type": "liveactivity",
|
|
5698
5764
|
"apns-topic": this.topic,
|
|
5699
5765
|
"apns-priority": String(args.priority ?? 10),
|
|
5700
5766
|
...args.expirationSeconds != null && {
|
|
5701
5767
|
"apns-expiration": String(args.expirationSeconds)
|
|
5702
5768
|
},
|
|
5703
|
-
[
|
|
5704
|
-
[
|
|
5769
|
+
[constants2.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
|
|
5770
|
+
[constants2.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
|
|
5705
5771
|
};
|
|
5706
5772
|
return new Promise((resolve2, reject) => {
|
|
5707
5773
|
const req = session.request(headers);
|
|
5708
5774
|
req.setTimeout(args.timeoutMs ?? 1e4, () => {
|
|
5709
|
-
req.close(
|
|
5775
|
+
req.close(constants2.NGHTTP2_CANCEL);
|
|
5710
5776
|
resolve2({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
|
|
5711
5777
|
});
|
|
5712
5778
|
let status = 0;
|
|
5713
5779
|
req.on("response", (resHeaders) => {
|
|
5714
|
-
status = Number(resHeaders[
|
|
5780
|
+
status = Number(resHeaders[constants2.HTTP2_HEADER_STATUS] ?? 0);
|
|
5715
5781
|
});
|
|
5716
5782
|
const chunks = [];
|
|
5717
5783
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -6065,7 +6131,10 @@ function parseVersionOutput(output) {
|
|
|
6065
6131
|
const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
|
|
6066
6132
|
return match ? match[0] : null;
|
|
6067
6133
|
}
|
|
6134
|
+
var versionByExe = /* @__PURE__ */ new Map();
|
|
6068
6135
|
function runVersion(exe) {
|
|
6136
|
+
const cached3 = versionByExe.get(exe);
|
|
6137
|
+
if (cached3 !== void 0) return Promise.resolve(cached3);
|
|
6069
6138
|
const viaShell = isWindows && /\.(?:cmd|bat)$/i.test(exe);
|
|
6070
6139
|
const file = viaShell ? `"${exe}"` : exe;
|
|
6071
6140
|
return new Promise((resolve2) => {
|
|
@@ -6075,7 +6144,9 @@ function runVersion(exe) {
|
|
|
6075
6144
|
{ timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
|
|
6076
6145
|
(err, stdout, stderr) => {
|
|
6077
6146
|
if (err && !stdout && !stderr) return resolve2(null);
|
|
6078
|
-
|
|
6147
|
+
const version = parseVersionOutput(`${stdout}${stderr}`);
|
|
6148
|
+
if (version !== null) versionByExe.set(exe, version);
|
|
6149
|
+
resolve2(version);
|
|
6079
6150
|
}
|
|
6080
6151
|
);
|
|
6081
6152
|
});
|
|
@@ -6119,13 +6190,16 @@ function compareSemver(a, b) {
|
|
|
6119
6190
|
if (pb.pre === null) return -1;
|
|
6120
6191
|
return pa.pre < pb.pre ? -1 : 1;
|
|
6121
6192
|
}
|
|
6122
|
-
async function providerHealth(name,
|
|
6193
|
+
async function providerHealth(name, locateExe = () => locateProviderExe(name), detect = runVersion) {
|
|
6123
6194
|
const verifiedAgainst = VERIFIED_AGAINST[name];
|
|
6124
6195
|
const capabilities = capabilitiesFor(name);
|
|
6125
|
-
let exe;
|
|
6196
|
+
let exe = null;
|
|
6126
6197
|
try {
|
|
6127
|
-
exe =
|
|
6198
|
+
exe = locateExe();
|
|
6128
6199
|
} catch {
|
|
6200
|
+
exe = null;
|
|
6201
|
+
}
|
|
6202
|
+
if (exe === null) {
|
|
6129
6203
|
return {
|
|
6130
6204
|
name,
|
|
6131
6205
|
available: false,
|
|
@@ -6162,8 +6236,8 @@ var createProviderRoutes = () => {
|
|
|
6162
6236
|
const app = new Hono14();
|
|
6163
6237
|
app.get("/", async (c) => {
|
|
6164
6238
|
const providers = await Promise.all([
|
|
6165
|
-
providerHealth(CLAUDE_CODE_PROVIDER
|
|
6166
|
-
providerHealth(CODEX_CLI_PROVIDER
|
|
6239
|
+
providerHealth(CLAUDE_CODE_PROVIDER),
|
|
6240
|
+
providerHealth(CODEX_CLI_PROVIDER)
|
|
6167
6241
|
]);
|
|
6168
6242
|
return c.json({ providers });
|
|
6169
6243
|
});
|
|
@@ -6394,7 +6468,7 @@ import {
|
|
|
6394
6468
|
parseJsonlLine
|
|
6395
6469
|
} from "@threadbase-sh/scanner";
|
|
6396
6470
|
import Database from "better-sqlite3";
|
|
6397
|
-
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as
|
|
6471
|
+
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as statSync4 } from "fs";
|
|
6398
6472
|
import { open as openAsync } from "fs/promises";
|
|
6399
6473
|
import { dirname as dirname8 } from "path";
|
|
6400
6474
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
@@ -6512,7 +6586,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
6512
6586
|
}
|
|
6513
6587
|
|
|
6514
6588
|
// src/services/conversations/isAgentConversation.ts
|
|
6515
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as
|
|
6589
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync3 } from "fs";
|
|
6516
6590
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
6517
6591
|
var CHUNK_BYTES = 64 * 1024;
|
|
6518
6592
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -6539,7 +6613,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
6539
6613
|
return false;
|
|
6540
6614
|
}
|
|
6541
6615
|
try {
|
|
6542
|
-
const fileSize =
|
|
6616
|
+
const fileSize = statSync3(filePath).size;
|
|
6543
6617
|
if (fileSize === 0) {
|
|
6544
6618
|
fileDecisionCache.set(key, false);
|
|
6545
6619
|
return false;
|
|
@@ -7177,7 +7251,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7177
7251
|
if (!fileState) return null;
|
|
7178
7252
|
let stat3;
|
|
7179
7253
|
try {
|
|
7180
|
-
stat3 =
|
|
7254
|
+
stat3 = statSync4(filePath);
|
|
7181
7255
|
} catch {
|
|
7182
7256
|
return null;
|
|
7183
7257
|
}
|
|
@@ -7236,7 +7310,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7236
7310
|
isAgentFileCached(filePath) {
|
|
7237
7311
|
let s;
|
|
7238
7312
|
try {
|
|
7239
|
-
s =
|
|
7313
|
+
s = statSync4(filePath);
|
|
7240
7314
|
} catch {
|
|
7241
7315
|
return false;
|
|
7242
7316
|
}
|
|
@@ -7447,7 +7521,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7447
7521
|
let mtimeMs = null;
|
|
7448
7522
|
let fileSize = null;
|
|
7449
7523
|
try {
|
|
7450
|
-
const s =
|
|
7524
|
+
const s = statSync4(m.filePath);
|
|
7451
7525
|
mtimeMs = s.mtimeMs;
|
|
7452
7526
|
fileSize = s.size;
|
|
7453
7527
|
} catch {
|
|
@@ -7507,7 +7581,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
7507
7581
|
let fileSize;
|
|
7508
7582
|
let fd;
|
|
7509
7583
|
try {
|
|
7510
|
-
fileSize =
|
|
7584
|
+
fileSize = statSync4(filePath).size;
|
|
7511
7585
|
fd = openSync3(filePath, "r");
|
|
7512
7586
|
} catch {
|
|
7513
7587
|
return false;
|
|
@@ -9503,7 +9577,7 @@ async function findRolloutOwner(rolloutPath, options = {}) {
|
|
|
9503
9577
|
}
|
|
9504
9578
|
|
|
9505
9579
|
// src/services/sessions/conversationBusy.ts
|
|
9506
|
-
import { statSync as
|
|
9580
|
+
import { statSync as statSync5 } from "fs";
|
|
9507
9581
|
|
|
9508
9582
|
// src/utils/canonicalizeProjectPath.ts
|
|
9509
9583
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -9527,7 +9601,7 @@ function conversationBusy(input) {
|
|
|
9527
9601
|
let lastActivityMs = null;
|
|
9528
9602
|
if (input.jsonlPath) {
|
|
9529
9603
|
try {
|
|
9530
|
-
const mtimeMs =
|
|
9604
|
+
const mtimeMs = statSync5(input.jsonlPath).mtimeMs;
|
|
9531
9605
|
const age = now - mtimeMs;
|
|
9532
9606
|
lastActivityMs = Math.max(0, age);
|
|
9533
9607
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -10642,7 +10716,10 @@ var SessionHandlers = class {
|
|
|
10642
10716
|
});
|
|
10643
10717
|
this.sessionStore.addManaged(session);
|
|
10644
10718
|
this.registryBoot.recordSessionSpawn(session);
|
|
10645
|
-
const { outcome } = await this.deps.waitForStartupOutcome(
|
|
10719
|
+
const { outcome, session: settled } = await this.deps.waitForStartupOutcome(
|
|
10720
|
+
session.id,
|
|
10721
|
+
START_READY_TIMEOUT_MS
|
|
10722
|
+
);
|
|
10646
10723
|
const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());
|
|
10647
10724
|
if (outcome === "ready" && current) {
|
|
10648
10725
|
json(res, 200, { session: current });
|
|
@@ -10650,7 +10727,7 @@ var SessionHandlers = class {
|
|
|
10650
10727
|
json(res, 502, {
|
|
10651
10728
|
id: session.id,
|
|
10652
10729
|
status: "idle",
|
|
10653
|
-
error:
|
|
10730
|
+
error: settled?.failureReason ?? "Session exited before becoming ready"
|
|
10654
10731
|
});
|
|
10655
10732
|
} else {
|
|
10656
10733
|
json(res, 202, { id: session.id, status: "pending" });
|
|
@@ -10664,11 +10741,16 @@ var SessionHandlers = class {
|
|
|
10664
10741
|
} catch (err) {
|
|
10665
10742
|
const message = err instanceof Error ? err.message : "Failed to start session";
|
|
10666
10743
|
const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
|
|
10744
|
+
const code = err.code;
|
|
10667
10745
|
this.log.error(`[start] failed to start session: ${message}`, {
|
|
10668
10746
|
event: "session.start_failed",
|
|
10669
10747
|
error: message
|
|
10670
10748
|
});
|
|
10671
|
-
json(
|
|
10749
|
+
json(
|
|
10750
|
+
res,
|
|
10751
|
+
statusCode,
|
|
10752
|
+
typeof code === "string" ? { error: message, code } : { error: message }
|
|
10753
|
+
);
|
|
10672
10754
|
}
|
|
10673
10755
|
}
|
|
10674
10756
|
async handleSetSessionName(sessionId, req, res) {
|
|
@@ -11158,7 +11240,7 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
11158
11240
|
};
|
|
11159
11241
|
|
|
11160
11242
|
// src/external-tails.ts
|
|
11161
|
-
import { statSync as
|
|
11243
|
+
import { statSync as statSync6 } from "fs";
|
|
11162
11244
|
var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
|
|
11163
11245
|
var EXTERNAL_TAIL_MAX = 32;
|
|
11164
11246
|
var EXTERNAL_TAIL_IDLE_MS = 3e5;
|
|
@@ -11191,7 +11273,7 @@ var ExternalTailManager = class {
|
|
|
11191
11273
|
if (this.isManagedTailPath(key)) return;
|
|
11192
11274
|
let mtimeMs;
|
|
11193
11275
|
try {
|
|
11194
|
-
mtimeMs =
|
|
11276
|
+
mtimeMs = statSync6(filePath).mtimeMs;
|
|
11195
11277
|
} catch {
|
|
11196
11278
|
return;
|
|
11197
11279
|
}
|
|
@@ -11353,7 +11435,7 @@ var PairTokenStore = class {
|
|
|
11353
11435
|
};
|
|
11354
11436
|
}
|
|
11355
11437
|
/**
|
|
11356
|
-
*
|
|
11438
|
+
* What `consume` would answer right now, WITHOUT spending the token.
|
|
11357
11439
|
*
|
|
11358
11440
|
* Exists so a caller can reject a bad token before doing any work, and still
|
|
11359
11441
|
* spend the token only once the work has succeeded. A pair token is
|
|
@@ -11362,10 +11444,14 @@ var PairTokenStore = class {
|
|
|
11362
11444
|
* indistinguishable from an attacker replaying a photographed code, which is
|
|
11363
11445
|
* the one signal `design.md` §2.6 designates as replay detection.
|
|
11364
11446
|
*
|
|
11365
|
-
*
|
|
11366
|
-
*
|
|
11447
|
+
* **Named for its relationship to `consume`, deliberately.** It reserves
|
|
11448
|
+
* nothing and takes no lock, so two callers can both be told `{ ok: true }`
|
|
11449
|
+
* for the same token. A name like `verify` would read as a gate that had
|
|
11450
|
+
* decided something, and inviting a caller to act on this alone is precisely
|
|
11451
|
+
* the misuse the ordering it enables exists to prevent. A reader who sees
|
|
11452
|
+
* `wouldConsume` asks where the `consume` is, which is the right question.
|
|
11367
11453
|
*/
|
|
11368
|
-
|
|
11454
|
+
wouldConsume(token) {
|
|
11369
11455
|
const result = this.check(token);
|
|
11370
11456
|
return result.ok ? { ok: true } : result;
|
|
11371
11457
|
}
|
|
@@ -11496,7 +11582,7 @@ function spawnDetachedHost(socketPath, entryPoint) {
|
|
|
11496
11582
|
import {
|
|
11497
11583
|
ConversationScanner
|
|
11498
11584
|
} from "@threadbase-sh/scanner";
|
|
11499
|
-
import { statSync as
|
|
11585
|
+
import { statSync as statSync8 } from "fs";
|
|
11500
11586
|
import { homedir as homedir10 } from "os";
|
|
11501
11587
|
import { join as join20 } from "path";
|
|
11502
11588
|
|
|
@@ -11597,14 +11683,14 @@ function refreshConversationCache(deps) {
|
|
|
11597
11683
|
}
|
|
11598
11684
|
|
|
11599
11685
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
11600
|
-
import { readdirSync as readdirSync4, statSync as
|
|
11686
|
+
import { readdirSync as readdirSync4, statSync as statSync7 } from "fs";
|
|
11601
11687
|
import { homedir as homedir9 } from "os";
|
|
11602
11688
|
import { join as join19 } from "path";
|
|
11603
11689
|
var DEFAULT_PROJECTS_DIR = join19(homedir9(), ".claude", "projects");
|
|
11604
11690
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
11605
11691
|
let maxMs;
|
|
11606
11692
|
try {
|
|
11607
|
-
maxMs =
|
|
11693
|
+
maxMs = statSync7(projectsDir).mtimeMs;
|
|
11608
11694
|
} catch {
|
|
11609
11695
|
return null;
|
|
11610
11696
|
}
|
|
@@ -11612,7 +11698,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
11612
11698
|
for (const ent of readdirSync4(projectsDir, { withFileTypes: true })) {
|
|
11613
11699
|
if (!ent.isDirectory()) continue;
|
|
11614
11700
|
try {
|
|
11615
|
-
const childMs =
|
|
11701
|
+
const childMs = statSync7(join19(projectsDir, ent.name)).mtimeMs;
|
|
11616
11702
|
if (childMs > maxMs) maxMs = childMs;
|
|
11617
11703
|
} catch {
|
|
11618
11704
|
}
|
|
@@ -11824,7 +11910,7 @@ var ScannerManager = class {
|
|
|
11824
11910
|
if (!conv.filePath) return false;
|
|
11825
11911
|
let mtimeMs = null;
|
|
11826
11912
|
try {
|
|
11827
|
-
mtimeMs =
|
|
11913
|
+
mtimeMs = statSync8(conv.filePath).mtimeMs;
|
|
11828
11914
|
} catch {
|
|
11829
11915
|
return false;
|
|
11830
11916
|
}
|
|
@@ -12063,10 +12149,10 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
12063
12149
|
}
|
|
12064
12150
|
|
|
12065
12151
|
// src/server-wiring.ts
|
|
12066
|
-
import { statSync as
|
|
12152
|
+
import { statSync as statSync10 } from "fs";
|
|
12067
12153
|
|
|
12068
12154
|
// src/handlers/handleListProjects.ts
|
|
12069
|
-
import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as
|
|
12155
|
+
import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as statSync9 } from "fs";
|
|
12070
12156
|
import { homedir as homedir11 } from "os";
|
|
12071
12157
|
import { join as join21 } from "path";
|
|
12072
12158
|
var HEAD_BYTES = 64 * 1024;
|
|
@@ -12112,7 +12198,7 @@ function handleListProjects(url, res) {
|
|
|
12112
12198
|
const fullPath = join21(projectsDir, dirName);
|
|
12113
12199
|
let mtime = 0;
|
|
12114
12200
|
try {
|
|
12115
|
-
mtime =
|
|
12201
|
+
mtime = statSync9(fullPath).mtimeMs;
|
|
12116
12202
|
} catch {
|
|
12117
12203
|
}
|
|
12118
12204
|
return { dirName: String(dirName), mtime };
|
|
@@ -12146,7 +12232,7 @@ function createConversationWatcherEvents(deps) {
|
|
|
12146
12232
|
const seqs = cache.extendMessageIndex(
|
|
12147
12233
|
filePath,
|
|
12148
12234
|
spans,
|
|
12149
|
-
|
|
12235
|
+
statSync10(filePath),
|
|
12150
12236
|
readFrom,
|
|
12151
12237
|
endOffset
|
|
12152
12238
|
);
|
|
@@ -12193,7 +12279,7 @@ function createConversationWatcherEvents(deps) {
|
|
|
12193
12279
|
},
|
|
12194
12280
|
onConversationChanged: (filePath) => {
|
|
12195
12281
|
try {
|
|
12196
|
-
|
|
12282
|
+
statSync10(filePath);
|
|
12197
12283
|
} catch {
|
|
12198
12284
|
deps.externalTailManager().handleJsonlDeleted(filePath);
|
|
12199
12285
|
return;
|
|
@@ -12291,7 +12377,15 @@ function createLiveSessionOptions(deps) {
|
|
|
12291
12377
|
// grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
|
|
12292
12378
|
// genuine process exit ("process-exit"), and reports both as
|
|
12293
12379
|
// `lifecycle: "completed"`. See managedToResponse in session-store.ts.
|
|
12294
|
-
...session.statusSource != null && { statusSource: session.statusSource }
|
|
12380
|
+
...session.statusSource != null && { statusSource: session.statusSource },
|
|
12381
|
+
// Why a session died, not just that it did. Without this the store's
|
|
12382
|
+
// copy has no failureReason, so managedToResponse falls through to
|
|
12383
|
+
// `lifecycle: "completed"` (see session-store.ts) and a session that
|
|
12384
|
+
// never started — missing CLI, missing project dir — is reported to
|
|
12385
|
+
// every client as one that finished normally. Guarded like its
|
|
12386
|
+
// neighbours: a later transition must not blank a recorded failure.
|
|
12387
|
+
...session.failureReason != null && { failureReason: session.failureReason },
|
|
12388
|
+
...session.failureCode != null && { failureCode: session.failureCode }
|
|
12295
12389
|
});
|
|
12296
12390
|
deps.managedSessionsRepo()?.recordStatus(
|
|
12297
12391
|
session.id,
|
|
@@ -12546,7 +12640,7 @@ function saveAlertState(state) {
|
|
|
12546
12640
|
}
|
|
12547
12641
|
|
|
12548
12642
|
// src/services/cache-integrity/backup.ts
|
|
12549
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as
|
|
12643
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as statSync11, unlinkSync } from "fs";
|
|
12550
12644
|
import { join as join23 } from "path";
|
|
12551
12645
|
var DEFAULT_RETAIN = 3;
|
|
12552
12646
|
function retainCount() {
|
|
@@ -12565,7 +12659,7 @@ async function backupCacheDb(db, cacheDir) {
|
|
|
12565
12659
|
const retain = retainCount();
|
|
12566
12660
|
const backups = readdirSync6(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
12567
12661
|
const full = join23(backupsDir, f);
|
|
12568
|
-
return { full, mtime:
|
|
12662
|
+
return { full, mtime: statSync11(full).mtimeMs };
|
|
12569
12663
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
12570
12664
|
for (const stale of backups.slice(retain)) {
|
|
12571
12665
|
if (existsSync11(stale.full)) unlinkSync(stale.full);
|
|
@@ -12831,7 +12925,7 @@ var CacheIntegrityMonitor = class {
|
|
|
12831
12925
|
|
|
12832
12926
|
// src/services/conversations/conversationWatcher.ts
|
|
12833
12927
|
import chokidar from "chokidar";
|
|
12834
|
-
import { statSync as
|
|
12928
|
+
import { statSync as statSync12 } from "fs";
|
|
12835
12929
|
import { open, stat as stat2 } from "fs/promises";
|
|
12836
12930
|
var ConversationWatcher = class {
|
|
12837
12931
|
files = /* @__PURE__ */ new Map();
|
|
@@ -12857,7 +12951,7 @@ var ConversationWatcher = class {
|
|
|
12857
12951
|
if (this.files.has(key)) return;
|
|
12858
12952
|
let offset;
|
|
12859
12953
|
try {
|
|
12860
|
-
offset =
|
|
12954
|
+
offset = statSync12(filePath).size;
|
|
12861
12955
|
} catch {
|
|
12862
12956
|
offset = 0;
|
|
12863
12957
|
}
|
|
@@ -14532,7 +14626,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14532
14626
|
}
|
|
14533
14627
|
|
|
14534
14628
|
// src/session-watchers.ts
|
|
14535
|
-
import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as
|
|
14629
|
+
import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
|
|
14536
14630
|
import { homedir as homedir13 } from "os";
|
|
14537
14631
|
import { basename as basename6, join as join24 } from "path";
|
|
14538
14632
|
var SessionWatchers = class {
|
|
@@ -14639,7 +14733,7 @@ var SessionWatchers = class {
|
|
|
14639
14733
|
if (!resolvedFilePath && existsSync15(projectsDir)) {
|
|
14640
14734
|
try {
|
|
14641
14735
|
const now = Date.now();
|
|
14642
|
-
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime:
|
|
14736
|
+
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync13(join24(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14643
14737
|
({ f }) => basename6(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join24(projectsDir, f)) === sessionId
|
|
14644
14738
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
14645
14739
|
if (match) resolvedFilePath = join24(projectsDir, match.f);
|
|
@@ -14736,7 +14830,7 @@ var SessionWatchers = class {
|
|
|
14736
14830
|
continue;
|
|
14737
14831
|
}
|
|
14738
14832
|
const nowMs = Date.now();
|
|
14739
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
14833
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync13(join24(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
14740
14834
|
for (const { f } of recentCandidates) {
|
|
14741
14835
|
const candidatePath = join24(sessionsDir, f);
|
|
14742
14836
|
const match = matchesProjectPath(candidatePath);
|
|
@@ -15746,6 +15840,41 @@ var StreamerServer = class {
|
|
|
15746
15840
|
json(res, 503, body);
|
|
15747
15841
|
return true;
|
|
15748
15842
|
}
|
|
15843
|
+
/**
|
|
15844
|
+
* Say which provider CLIs this machine can actually launch.
|
|
15845
|
+
*
|
|
15846
|
+
* The operator cannot discover this case unaided: under launchd/Task
|
|
15847
|
+
* Scheduler the service inherits a stripped PATH, so a CLI that works
|
|
15848
|
+
* perfectly in their terminal is invisible to the service, and every session
|
|
15849
|
+
* start dies milliseconds in. `/api/diagnostics` answers it too, but only for
|
|
15850
|
+
* someone who already suspects it.
|
|
15851
|
+
*
|
|
15852
|
+
* Availability only, never a version — `--version` costs a process spawn per
|
|
15853
|
+
* provider (85ms for claude here) and belongs on the first request that wants
|
|
15854
|
+
* it, not on boot.
|
|
15855
|
+
*
|
|
15856
|
+
* Called AFTER the port is bound, which is not cosmetic. This is the first
|
|
15857
|
+
* caller of the exe resolvers in the process, so the memo is cold by
|
|
15858
|
+
* definition and each provider pays one synchronous `which` / `where.exe`
|
|
15859
|
+
* (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
|
|
15860
|
+
* Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
|
|
15861
|
+
* event loop, and Task Scheduler's stripped PATH is exactly where a miss
|
|
15862
|
+
* pays the full timeout — so the worst case is ~6s of two blocking lookups.
|
|
15863
|
+
* After `listen()` that delays the first requests on a box that cannot start
|
|
15864
|
+
* a session anyway; before it, it would have delayed binding the port.
|
|
15865
|
+
*/
|
|
15866
|
+
logProviderAvailability() {
|
|
15867
|
+
for (const provider of [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER]) {
|
|
15868
|
+
if (locateProviderExe(provider)) {
|
|
15869
|
+
this.log.info(`Provider ${provider}: found`, { event: "config.provider", provider });
|
|
15870
|
+
} else {
|
|
15871
|
+
this.log.warn(`Provider ${provider}: not found on PATH \u2014 sessions cannot start`, {
|
|
15872
|
+
event: "config.provider_missing",
|
|
15873
|
+
provider
|
|
15874
|
+
});
|
|
15875
|
+
}
|
|
15876
|
+
}
|
|
15877
|
+
}
|
|
15749
15878
|
async listen(port, opts) {
|
|
15750
15879
|
if (this.featureFlags.ptyHost) {
|
|
15751
15880
|
try {
|
|
@@ -15806,6 +15935,7 @@ var StreamerServer = class {
|
|
|
15806
15935
|
event: "server.listening",
|
|
15807
15936
|
...this.host !== void 0 && { host: this.host }
|
|
15808
15937
|
});
|
|
15938
|
+
this.logProviderAvailability();
|
|
15809
15939
|
try {
|
|
15810
15940
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15811
15941
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
@@ -16182,8 +16312,14 @@ var StreamerServer = class {
|
|
|
16182
16312
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16183
16313
|
return;
|
|
16184
16314
|
}
|
|
16185
|
-
const precheck = this.pairTokens.
|
|
16315
|
+
const precheck = this.pairTokens.wouldConsume(token);
|
|
16186
16316
|
if (!precheck.ok) {
|
|
16317
|
+
if (precheck.reason === "used") {
|
|
16318
|
+
this.log.warn(
|
|
16319
|
+
"[pair] a pair token was replayed. If you did not just pair a device, check the paired-devices list and revoke anything you do not recognise.",
|
|
16320
|
+
{ event: "pair.token_replayed", ip }
|
|
16321
|
+
);
|
|
16322
|
+
}
|
|
16187
16323
|
json(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
16188
16324
|
return;
|
|
16189
16325
|
}
|