@threadbase-sh/streamer 1.52.4 → 1.52.6
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 +786 -472
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +260 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -6
- package/dist/index.d.ts +66 -6
- package/dist/index.js +257 -64
- package/dist/index.js.map +1 -1
- package/dist/migrations/011_create_devices.sql +15 -0
- package/dist/runtime-migrations/003_create_devices.sql +52 -0
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -3608,7 +3608,7 @@ var require_thread_stream = __commonJS({
|
|
|
3608
3608
|
var { version: version2 } = require_package();
|
|
3609
3609
|
var { EventEmitter: EventEmitter5 } = require("events");
|
|
3610
3610
|
var { Worker } = require("worker_threads");
|
|
3611
|
-
var { join:
|
|
3611
|
+
var { join: join35 } = require("path");
|
|
3612
3612
|
var { pathToFileURL } = require("url");
|
|
3613
3613
|
var { wait } = require_wait();
|
|
3614
3614
|
var {
|
|
@@ -3659,7 +3659,7 @@ var require_thread_stream = __commonJS({
|
|
|
3659
3659
|
function createWorker(stream, opts) {
|
|
3660
3660
|
const { filename, workerData } = opts;
|
|
3661
3661
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
3662
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
3662
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join35(__dirname, "lib", "worker.js");
|
|
3663
3663
|
const worker = new Worker(toExecute, {
|
|
3664
3664
|
...opts.workerOpts,
|
|
3665
3665
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -4127,7 +4127,7 @@ var require_transport = __commonJS({
|
|
|
4127
4127
|
var { createRequire } = require("module");
|
|
4128
4128
|
var { existsSync: existsSync21 } = require("fs");
|
|
4129
4129
|
var getCallers = require_caller();
|
|
4130
|
-
var { join:
|
|
4130
|
+
var { join: join35, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
|
|
4131
4131
|
var { fileURLToPath: fileURLToPath3 } = require("url");
|
|
4132
4132
|
var sleep = require_atomic_sleep();
|
|
4133
4133
|
var onExit = require_on_exit_leak_free();
|
|
@@ -4280,7 +4280,7 @@ var require_transport = __commonJS({
|
|
|
4280
4280
|
throw new Error("only one of target or targets can be specified");
|
|
4281
4281
|
}
|
|
4282
4282
|
if (targets) {
|
|
4283
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
4283
|
+
target = bundlerOverrides["pino-worker"] || join35(__dirname, "worker.js");
|
|
4284
4284
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
4285
4285
|
return {
|
|
4286
4286
|
...dest,
|
|
@@ -4298,7 +4298,7 @@ var require_transport = __commonJS({
|
|
|
4298
4298
|
});
|
|
4299
4299
|
});
|
|
4300
4300
|
} else if (pipeline2) {
|
|
4301
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
4301
|
+
target = bundlerOverrides["pino-worker"] || join35(__dirname, "worker.js");
|
|
4302
4302
|
options.pipelines = [pipeline2.map((dest) => {
|
|
4303
4303
|
return {
|
|
4304
4304
|
...dest,
|
|
@@ -4321,7 +4321,7 @@ var require_transport = __commonJS({
|
|
|
4321
4321
|
return origin;
|
|
4322
4322
|
}
|
|
4323
4323
|
if (origin === "pino/file") {
|
|
4324
|
-
return
|
|
4324
|
+
return join35(__dirname, "..", "file.js");
|
|
4325
4325
|
}
|
|
4326
4326
|
let fixTarget2;
|
|
4327
4327
|
for (const filePath of callers) {
|
|
@@ -5302,7 +5302,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
5302
5302
|
return circularValue;
|
|
5303
5303
|
}
|
|
5304
5304
|
let res = "";
|
|
5305
|
-
let
|
|
5305
|
+
let join35 = ",";
|
|
5306
5306
|
const originalIndentation = indentation;
|
|
5307
5307
|
if (Array.isArray(value)) {
|
|
5308
5308
|
if (value.length === 0) {
|
|
@@ -5316,7 +5316,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
5316
5316
|
indentation += spacer;
|
|
5317
5317
|
res += `
|
|
5318
5318
|
${indentation}`;
|
|
5319
|
-
|
|
5319
|
+
join35 = `,
|
|
5320
5320
|
${indentation}`;
|
|
5321
5321
|
}
|
|
5322
5322
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -5324,13 +5324,13 @@ ${indentation}`;
|
|
|
5324
5324
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
5325
5325
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
5326
5326
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
5327
|
-
res +=
|
|
5327
|
+
res += join35;
|
|
5328
5328
|
}
|
|
5329
5329
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
5330
5330
|
res += tmp !== void 0 ? tmp : "null";
|
|
5331
5331
|
if (value.length - 1 > maximumBreadth) {
|
|
5332
5332
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
5333
|
-
res += `${
|
|
5333
|
+
res += `${join35}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
5334
5334
|
}
|
|
5335
5335
|
if (spacer !== "") {
|
|
5336
5336
|
res += `
|
|
@@ -5351,7 +5351,7 @@ ${originalIndentation}`;
|
|
|
5351
5351
|
let separator = "";
|
|
5352
5352
|
if (spacer !== "") {
|
|
5353
5353
|
indentation += spacer;
|
|
5354
|
-
|
|
5354
|
+
join35 = `,
|
|
5355
5355
|
${indentation}`;
|
|
5356
5356
|
whitespace = " ";
|
|
5357
5357
|
}
|
|
@@ -5365,13 +5365,13 @@ ${indentation}`;
|
|
|
5365
5365
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
5366
5366
|
if (tmp !== void 0) {
|
|
5367
5367
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
5368
|
-
separator =
|
|
5368
|
+
separator = join35;
|
|
5369
5369
|
}
|
|
5370
5370
|
}
|
|
5371
5371
|
if (keyLength > maximumBreadth) {
|
|
5372
5372
|
const removedKeys = keyLength - maximumBreadth;
|
|
5373
5373
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
5374
|
-
separator =
|
|
5374
|
+
separator = join35;
|
|
5375
5375
|
}
|
|
5376
5376
|
if (spacer !== "" && separator.length > 1) {
|
|
5377
5377
|
res = `
|
|
@@ -5412,7 +5412,7 @@ ${originalIndentation}`;
|
|
|
5412
5412
|
}
|
|
5413
5413
|
const originalIndentation = indentation;
|
|
5414
5414
|
let res = "";
|
|
5415
|
-
let
|
|
5415
|
+
let join35 = ",";
|
|
5416
5416
|
if (Array.isArray(value)) {
|
|
5417
5417
|
if (value.length === 0) {
|
|
5418
5418
|
return "[]";
|
|
@@ -5425,7 +5425,7 @@ ${originalIndentation}`;
|
|
|
5425
5425
|
indentation += spacer;
|
|
5426
5426
|
res += `
|
|
5427
5427
|
${indentation}`;
|
|
5428
|
-
|
|
5428
|
+
join35 = `,
|
|
5429
5429
|
${indentation}`;
|
|
5430
5430
|
}
|
|
5431
5431
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -5433,13 +5433,13 @@ ${indentation}`;
|
|
|
5433
5433
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
5434
5434
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
5435
5435
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
5436
|
-
res +=
|
|
5436
|
+
res += join35;
|
|
5437
5437
|
}
|
|
5438
5438
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
5439
5439
|
res += tmp !== void 0 ? tmp : "null";
|
|
5440
5440
|
if (value.length - 1 > maximumBreadth) {
|
|
5441
5441
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
5442
|
-
res += `${
|
|
5442
|
+
res += `${join35}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
5443
5443
|
}
|
|
5444
5444
|
if (spacer !== "") {
|
|
5445
5445
|
res += `
|
|
@@ -5452,7 +5452,7 @@ ${originalIndentation}`;
|
|
|
5452
5452
|
let whitespace = "";
|
|
5453
5453
|
if (spacer !== "") {
|
|
5454
5454
|
indentation += spacer;
|
|
5455
|
-
|
|
5455
|
+
join35 = `,
|
|
5456
5456
|
${indentation}`;
|
|
5457
5457
|
whitespace = " ";
|
|
5458
5458
|
}
|
|
@@ -5461,7 +5461,7 @@ ${indentation}`;
|
|
|
5461
5461
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
5462
5462
|
if (tmp !== void 0) {
|
|
5463
5463
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
5464
|
-
separator =
|
|
5464
|
+
separator = join35;
|
|
5465
5465
|
}
|
|
5466
5466
|
}
|
|
5467
5467
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -5519,20 +5519,20 @@ ${originalIndentation}`;
|
|
|
5519
5519
|
indentation += spacer;
|
|
5520
5520
|
let res2 = `
|
|
5521
5521
|
${indentation}`;
|
|
5522
|
-
const
|
|
5522
|
+
const join36 = `,
|
|
5523
5523
|
${indentation}`;
|
|
5524
5524
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
5525
5525
|
let i = 0;
|
|
5526
5526
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
5527
5527
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
5528
5528
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
5529
|
-
res2 +=
|
|
5529
|
+
res2 += join36;
|
|
5530
5530
|
}
|
|
5531
5531
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
5532
5532
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
5533
5533
|
if (value.length - 1 > maximumBreadth) {
|
|
5534
5534
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
5535
|
-
res2 += `${
|
|
5535
|
+
res2 += `${join36}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
5536
5536
|
}
|
|
5537
5537
|
res2 += `
|
|
5538
5538
|
${originalIndentation}`;
|
|
@@ -5548,16 +5548,16 @@ ${originalIndentation}`;
|
|
|
5548
5548
|
return '"[Object]"';
|
|
5549
5549
|
}
|
|
5550
5550
|
indentation += spacer;
|
|
5551
|
-
const
|
|
5551
|
+
const join35 = `,
|
|
5552
5552
|
${indentation}`;
|
|
5553
5553
|
let res = "";
|
|
5554
5554
|
let separator = "";
|
|
5555
5555
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
5556
5556
|
if (isTypedArrayWithEntries(value)) {
|
|
5557
|
-
res += stringifyTypedArray(value,
|
|
5557
|
+
res += stringifyTypedArray(value, join35, maximumBreadth);
|
|
5558
5558
|
keys = keys.slice(value.length);
|
|
5559
5559
|
maximumPropertiesToStringify -= value.length;
|
|
5560
|
-
separator =
|
|
5560
|
+
separator = join35;
|
|
5561
5561
|
}
|
|
5562
5562
|
if (deterministic) {
|
|
5563
5563
|
keys = sort(keys, comparator);
|
|
@@ -5568,13 +5568,13 @@ ${indentation}`;
|
|
|
5568
5568
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
5569
5569
|
if (tmp !== void 0) {
|
|
5570
5570
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
5571
|
-
separator =
|
|
5571
|
+
separator = join35;
|
|
5572
5572
|
}
|
|
5573
5573
|
}
|
|
5574
5574
|
if (keyLength > maximumBreadth) {
|
|
5575
5575
|
const removedKeys = keyLength - maximumBreadth;
|
|
5576
5576
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
5577
|
-
separator =
|
|
5577
|
+
separator = join35;
|
|
5578
5578
|
}
|
|
5579
5579
|
if (separator !== "") {
|
|
5580
5580
|
res = `
|
|
@@ -6285,7 +6285,8 @@ function loadOrCreateApiKey() {
|
|
|
6285
6285
|
const key = generateApiKey();
|
|
6286
6286
|
(0, import_fs.mkdirSync)(configDir(), { recursive: true });
|
|
6287
6287
|
(0, import_fs.writeFileSync)(configFile(), `api_key: ${key}
|
|
6288
|
-
`, "utf-8");
|
|
6288
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
6289
|
+
(0, import_fs.chmodSync)(configFile(), 384);
|
|
6289
6290
|
return key;
|
|
6290
6291
|
}
|
|
6291
6292
|
function loadBrowseRoot() {
|
|
@@ -96870,6 +96871,252 @@ var require_lib3 = __commonJS({
|
|
|
96870
96871
|
}
|
|
96871
96872
|
});
|
|
96872
96873
|
|
|
96874
|
+
// src/services/security/capabilities.ts
|
|
96875
|
+
function isCapability(value) {
|
|
96876
|
+
return typeof value === "string" && CAPABILITIES.includes(value);
|
|
96877
|
+
}
|
|
96878
|
+
function capabilitiesForPreset(preset) {
|
|
96879
|
+
return preset === "read-only" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];
|
|
96880
|
+
}
|
|
96881
|
+
function legacyPrincipal() {
|
|
96882
|
+
return { kind: "legacy", capabilities: [...FULL_CAPABILITIES, "admin"] };
|
|
96883
|
+
}
|
|
96884
|
+
function hasCapability(principal, required2) {
|
|
96885
|
+
return principal.capabilities.includes(required2);
|
|
96886
|
+
}
|
|
96887
|
+
function requiredCapability(path2, method) {
|
|
96888
|
+
if (path2.startsWith("/api/sessions") && (method === "GET" || method === "HEAD")) {
|
|
96889
|
+
return "history:read";
|
|
96890
|
+
}
|
|
96891
|
+
let best = null;
|
|
96892
|
+
for (const [prefix, cap] of ROUTE_CAPABILITIES) {
|
|
96893
|
+
if (path2.startsWith(prefix) && (best === null || prefix.length > best.len)) {
|
|
96894
|
+
best = { len: prefix.length, cap };
|
|
96895
|
+
}
|
|
96896
|
+
}
|
|
96897
|
+
return best?.cap ?? null;
|
|
96898
|
+
}
|
|
96899
|
+
var CAPABILITIES, FULL_CAPABILITIES, READ_ONLY_CAPABILITIES, ROUTE_CAPABILITIES;
|
|
96900
|
+
var init_capabilities = __esm({
|
|
96901
|
+
"src/services/security/capabilities.ts"() {
|
|
96902
|
+
"use strict";
|
|
96903
|
+
CAPABILITIES = [
|
|
96904
|
+
"history:read",
|
|
96905
|
+
// read conversations, search
|
|
96906
|
+
"session:control",
|
|
96907
|
+
// start, resume, send input, interrupt
|
|
96908
|
+
"fs:browse",
|
|
96909
|
+
// browse the project tree
|
|
96910
|
+
"fs:upload",
|
|
96911
|
+
// upload files into a project
|
|
96912
|
+
"notifications",
|
|
96913
|
+
// register for push
|
|
96914
|
+
"admin"
|
|
96915
|
+
// rotate keys, manage devices
|
|
96916
|
+
];
|
|
96917
|
+
FULL_CAPABILITIES = [
|
|
96918
|
+
"history:read",
|
|
96919
|
+
"session:control",
|
|
96920
|
+
"fs:browse",
|
|
96921
|
+
"fs:upload",
|
|
96922
|
+
"notifications"
|
|
96923
|
+
];
|
|
96924
|
+
READ_ONLY_CAPABILITIES = ["history:read"];
|
|
96925
|
+
ROUTE_CAPABILITIES = [
|
|
96926
|
+
// Most specific first for readability; matching sorts by length anyway.
|
|
96927
|
+
["/api/sessions/", "session:control"],
|
|
96928
|
+
["/api/sessions", "history:read"],
|
|
96929
|
+
// listing sessions is a read
|
|
96930
|
+
["/api/conversations", "history:read"],
|
|
96931
|
+
["/api/projects", "history:read"],
|
|
96932
|
+
["/api/search", "history:read"],
|
|
96933
|
+
["/api/providers", "history:read"],
|
|
96934
|
+
["/api/browse", "fs:browse"],
|
|
96935
|
+
["/api/upload", "fs:upload"],
|
|
96936
|
+
["/api/push", "notifications"],
|
|
96937
|
+
["/api/devices", "admin"],
|
|
96938
|
+
["/api/config", "admin"],
|
|
96939
|
+
["/api/auth/rotate", "admin"],
|
|
96940
|
+
["/api/backup", "admin"],
|
|
96941
|
+
// Server identity and capability discovery. A read-only device must be able
|
|
96942
|
+
// to see WHICH server it is talking to and what it supports, or it cannot
|
|
96943
|
+
// render anything at all.
|
|
96944
|
+
["/api/info", "history:read"],
|
|
96945
|
+
["/api/profiles", "history:read"],
|
|
96946
|
+
["/api/diagnostics", "history:read"],
|
|
96947
|
+
["/api/cache/alert", "history:read"],
|
|
96948
|
+
// Client log shipping: any authenticated client may report its own errors.
|
|
96949
|
+
// Gating this behind a capability would silence diagnostics from exactly the
|
|
96950
|
+
// devices most likely to be misbehaving.
|
|
96951
|
+
["/api/__client-log", "history:read"],
|
|
96952
|
+
// Logs viewer is localhost-only and already bypasses this middleware; the
|
|
96953
|
+
// mapping exists so a remote request is classified rather than denied as
|
|
96954
|
+
// unclassified.
|
|
96955
|
+
["/api/logs", "admin"],
|
|
96956
|
+
// Pairing routes other than the public exchange (e.g. minting a token).
|
|
96957
|
+
["/api/pair", "admin"],
|
|
96958
|
+
// The live WebSocket. Subscribing is a read — terminal output, session
|
|
96959
|
+
// updates, conversation events. Control still flows through the HTTP input
|
|
96960
|
+
// routes, which carry their own capability check, so a read-only device can
|
|
96961
|
+
// watch a session stream without being able to drive it.
|
|
96962
|
+
["/ws", "history:read"],
|
|
96963
|
+
// Progress webhook (multi-agent). Authenticated by HMAC in the handler and
|
|
96964
|
+
// already skipped by the middleware; classified so a stray request is denied
|
|
96965
|
+
// by rule rather than as "unclassified".
|
|
96966
|
+
["/internal/sessions", "admin"]
|
|
96967
|
+
];
|
|
96968
|
+
}
|
|
96969
|
+
});
|
|
96970
|
+
|
|
96971
|
+
// src/db/repositories/devices.repository.ts
|
|
96972
|
+
var devices_repository_exports = {};
|
|
96973
|
+
__export(devices_repository_exports, {
|
|
96974
|
+
DevicesRepository: () => DevicesRepository,
|
|
96975
|
+
generateDeviceToken: () => generateDeviceToken,
|
|
96976
|
+
hashDeviceToken: () => hashDeviceToken,
|
|
96977
|
+
parseCapabilities: () => parseCapabilities,
|
|
96978
|
+
safeHashEquals: () => safeHashEquals,
|
|
96979
|
+
toDeviceView: () => toDeviceView
|
|
96980
|
+
});
|
|
96981
|
+
function generateDeviceToken() {
|
|
96982
|
+
return (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
96983
|
+
}
|
|
96984
|
+
function hashDeviceToken(token) {
|
|
96985
|
+
return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
|
|
96986
|
+
}
|
|
96987
|
+
function safeHashEquals(a, b2) {
|
|
96988
|
+
if (a.length !== b2.length) return false;
|
|
96989
|
+
return (0, import_crypto3.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b2, "utf8"));
|
|
96990
|
+
}
|
|
96991
|
+
function parseCapabilities(raw2) {
|
|
96992
|
+
try {
|
|
96993
|
+
const parsed = JSON.parse(raw2);
|
|
96994
|
+
if (!Array.isArray(parsed)) return [];
|
|
96995
|
+
return parsed.filter(isCapability);
|
|
96996
|
+
} catch {
|
|
96997
|
+
return [];
|
|
96998
|
+
}
|
|
96999
|
+
}
|
|
97000
|
+
function toDeviceView(row) {
|
|
97001
|
+
return {
|
|
97002
|
+
deviceId: row.device_id,
|
|
97003
|
+
name: row.name,
|
|
97004
|
+
capabilities: parseCapabilities(row.capabilities),
|
|
97005
|
+
createdAt: row.created_at,
|
|
97006
|
+
lastSeenAt: row.last_seen_at,
|
|
97007
|
+
revokedAt: row.revoked_at
|
|
97008
|
+
};
|
|
97009
|
+
}
|
|
97010
|
+
var import_crypto3, DevicesRepository;
|
|
97011
|
+
var init_devices_repository = __esm({
|
|
97012
|
+
"src/db/repositories/devices.repository.ts"() {
|
|
97013
|
+
"use strict";
|
|
97014
|
+
import_crypto3 = require("crypto");
|
|
97015
|
+
init_capabilities();
|
|
97016
|
+
DevicesRepository = class {
|
|
97017
|
+
insertStmt;
|
|
97018
|
+
byTokenHashStmt;
|
|
97019
|
+
byIdStmt;
|
|
97020
|
+
listStmt;
|
|
97021
|
+
revokeStmt;
|
|
97022
|
+
touchStmt;
|
|
97023
|
+
deleteStmt;
|
|
97024
|
+
deleteRevokedStmt;
|
|
97025
|
+
constructor(db) {
|
|
97026
|
+
this.insertStmt = db.prepare(`
|
|
97027
|
+
INSERT INTO devices (
|
|
97028
|
+
device_id, public_key, token_hash, name, capabilities, created_at
|
|
97029
|
+
) VALUES (
|
|
97030
|
+
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at
|
|
97031
|
+
)
|
|
97032
|
+
`);
|
|
97033
|
+
this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
|
|
97034
|
+
this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
|
|
97035
|
+
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
97036
|
+
this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
|
|
97037
|
+
this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
|
|
97038
|
+
this.deleteStmt = db.prepare("DELETE FROM devices WHERE device_id = ?");
|
|
97039
|
+
this.deleteRevokedStmt = db.prepare("DELETE FROM devices WHERE revoked_at IS NOT NULL");
|
|
97040
|
+
}
|
|
97041
|
+
/**
|
|
97042
|
+
* Record a newly paired device and mint its token.
|
|
97043
|
+
*
|
|
97044
|
+
* The raw token is returned to the caller and never stored — this is the only
|
|
97045
|
+
* moment it exists outside the client.
|
|
97046
|
+
*/
|
|
97047
|
+
register(args) {
|
|
97048
|
+
const deviceId = (0, import_crypto3.randomUUID)();
|
|
97049
|
+
const deviceToken = generateDeviceToken();
|
|
97050
|
+
const capabilities = capabilitiesForPreset(args.preset ?? "full");
|
|
97051
|
+
this.insertStmt.run({
|
|
97052
|
+
device_id: deviceId,
|
|
97053
|
+
public_key: args.publicKey,
|
|
97054
|
+
token_hash: hashDeviceToken(deviceToken),
|
|
97055
|
+
name: args.name ?? null,
|
|
97056
|
+
capabilities: JSON.stringify(capabilities),
|
|
97057
|
+
created_at: args.now ?? Date.now()
|
|
97058
|
+
});
|
|
97059
|
+
return { deviceId, deviceToken, capabilities };
|
|
97060
|
+
}
|
|
97061
|
+
/**
|
|
97062
|
+
* Resolve a presented token to a device, or null.
|
|
97063
|
+
*
|
|
97064
|
+
* Returns null for a revoked device, so revocation takes effect on the very
|
|
97065
|
+
* next request with no cache to go stale.
|
|
97066
|
+
*/
|
|
97067
|
+
authenticate(token) {
|
|
97068
|
+
const hash2 = hashDeviceToken(token);
|
|
97069
|
+
const row = this.byTokenHashStmt.get(hash2);
|
|
97070
|
+
if (!row) return null;
|
|
97071
|
+
if (!safeHashEquals(row.token_hash, hash2)) return null;
|
|
97072
|
+
if (row.revoked_at != null) return null;
|
|
97073
|
+
return row;
|
|
97074
|
+
}
|
|
97075
|
+
get(deviceId) {
|
|
97076
|
+
return this.byIdStmt.get(deviceId) ?? null;
|
|
97077
|
+
}
|
|
97078
|
+
/** All devices, including revoked ones — an audit surface needs the history. */
|
|
97079
|
+
list() {
|
|
97080
|
+
return this.listStmt.all().map(toDeviceView);
|
|
97081
|
+
}
|
|
97082
|
+
/** Revoke one device. Others are untouched — no key rotation, no collateral. */
|
|
97083
|
+
revoke(deviceId, now = Date.now()) {
|
|
97084
|
+
return this.revokeStmt.run(now, deviceId).changes > 0;
|
|
97085
|
+
}
|
|
97086
|
+
/**
|
|
97087
|
+
* Erase one device's record outright.
|
|
97088
|
+
*
|
|
97089
|
+
* Deliberately separate from `revoke`, which is a soft delete that keeps the
|
|
97090
|
+
* row so `list()` can show what happened. That audit trail is the right
|
|
97091
|
+
* default — but it meant a `devices` row, including the user-supplied `name`
|
|
97092
|
+
* ("Ronen's iPhone"), had no removal path at all once the registry moved to
|
|
97093
|
+
* runtime.db, which no command deletes. This is that path.
|
|
97094
|
+
*
|
|
97095
|
+
* Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
|
|
97096
|
+
* device whose token is still on a phone somewhere stops being *known* rather
|
|
97097
|
+
* than being *refused*. Revoke first, delete second, is the safe order, and
|
|
97098
|
+
* `deleteRevoked()` exists so that is the easy thing to do.
|
|
97099
|
+
*/
|
|
97100
|
+
delete(deviceId) {
|
|
97101
|
+
return this.deleteStmt.run(deviceId).changes > 0;
|
|
97102
|
+
}
|
|
97103
|
+
/**
|
|
97104
|
+
* Erase every already-revoked device. The bulk companion to `delete`, and the
|
|
97105
|
+
* one that is safe by construction: a revoked device is already refused, so
|
|
97106
|
+
* removing its row cannot restore access to anything.
|
|
97107
|
+
*
|
|
97108
|
+
* Returns the number of rows removed.
|
|
97109
|
+
*/
|
|
97110
|
+
deleteRevoked() {
|
|
97111
|
+
return this.deleteRevokedStmt.run().changes;
|
|
97112
|
+
}
|
|
97113
|
+
touch(deviceId, now = Date.now()) {
|
|
97114
|
+
this.touchStmt.run(now, deviceId);
|
|
97115
|
+
}
|
|
97116
|
+
};
|
|
97117
|
+
}
|
|
97118
|
+
});
|
|
97119
|
+
|
|
96873
97120
|
// src/platform.ts
|
|
96874
97121
|
function isWindowsExecutablePath(path2) {
|
|
96875
97122
|
const dot = path2.lastIndexOf(".");
|
|
@@ -102777,6 +103024,132 @@ var require_out4 = __commonJS({
|
|
|
102777
103024
|
}
|
|
102778
103025
|
});
|
|
102779
103026
|
|
|
103027
|
+
// src/db/query-timing.ts
|
|
103028
|
+
function deriveLabel(sql) {
|
|
103029
|
+
const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
|
|
103030
|
+
const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
|
|
103031
|
+
return `${verb}:${table}`;
|
|
103032
|
+
}
|
|
103033
|
+
function resolveSlowMs() {
|
|
103034
|
+
const raw2 = process.env.THREADBASE_DB_SLOW_QUERY_MS;
|
|
103035
|
+
if (raw2 === void 0 || raw2 === "") return DEFAULT_SLOW_QUERY_MS;
|
|
103036
|
+
const parsed = Number(raw2);
|
|
103037
|
+
return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
|
|
103038
|
+
}
|
|
103039
|
+
function record2(label, ms2, rows, slowMs) {
|
|
103040
|
+
if (slowMs > 0 && ms2 >= slowMs) {
|
|
103041
|
+
log2.warn(
|
|
103042
|
+
`[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
|
|
103043
|
+
{ event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
103044
|
+
"pino"
|
|
103045
|
+
);
|
|
103046
|
+
return;
|
|
103047
|
+
}
|
|
103048
|
+
if (log2.pino.isLevelEnabled("debug")) {
|
|
103049
|
+
log2.debug(
|
|
103050
|
+
`[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
|
|
103051
|
+
{ event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
103052
|
+
"pino"
|
|
103053
|
+
);
|
|
103054
|
+
}
|
|
103055
|
+
}
|
|
103056
|
+
function rowsOf(method, result) {
|
|
103057
|
+
if (method === "all") return Array.isArray(result) ? result.length : 0;
|
|
103058
|
+
if (method === "run") return result?.changes ?? 0;
|
|
103059
|
+
return result === void 0 ? 0 : 1;
|
|
103060
|
+
}
|
|
103061
|
+
function instrumentDatabase(db, options = {}) {
|
|
103062
|
+
const slowMs = options.slowMs ?? resolveSlowMs();
|
|
103063
|
+
const prepare = db.prepare.bind(db);
|
|
103064
|
+
db.prepare = ((sql) => {
|
|
103065
|
+
const stmt = prepare(sql);
|
|
103066
|
+
const box = { label: deriveLabel(sql) };
|
|
103067
|
+
Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
|
|
103068
|
+
for (const method of ["get", "all", "run"]) {
|
|
103069
|
+
const original = stmt[method].bind(stmt);
|
|
103070
|
+
Object.defineProperty(stmt, method, {
|
|
103071
|
+
configurable: true,
|
|
103072
|
+
writable: true,
|
|
103073
|
+
value: (...args) => {
|
|
103074
|
+
const started = performance.now();
|
|
103075
|
+
const result = original(...args);
|
|
103076
|
+
record2(box.label, performance.now() - started, rowsOf(method, result), slowMs);
|
|
103077
|
+
return result;
|
|
103078
|
+
}
|
|
103079
|
+
});
|
|
103080
|
+
}
|
|
103081
|
+
return stmt;
|
|
103082
|
+
});
|
|
103083
|
+
return db;
|
|
103084
|
+
}
|
|
103085
|
+
function labelStatements(statements) {
|
|
103086
|
+
for (const [name, stmt] of Object.entries(statements)) {
|
|
103087
|
+
const box = stmt?.[LABEL];
|
|
103088
|
+
if (box) box.label = name;
|
|
103089
|
+
}
|
|
103090
|
+
}
|
|
103091
|
+
var log2, DEFAULT_SLOW_QUERY_MS, LABEL;
|
|
103092
|
+
var init_query_timing = __esm({
|
|
103093
|
+
"src/db/query-timing.ts"() {
|
|
103094
|
+
"use strict";
|
|
103095
|
+
init_logger();
|
|
103096
|
+
log2 = getLogger("db");
|
|
103097
|
+
DEFAULT_SLOW_QUERY_MS = 35;
|
|
103098
|
+
LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
103099
|
+
}
|
|
103100
|
+
});
|
|
103101
|
+
|
|
103102
|
+
// src/db/sqlite-migrate.ts
|
|
103103
|
+
function getMigrationsDir() {
|
|
103104
|
+
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
103105
|
+
return (0, import_path14.dirname)((0, import_url6.fileURLToPath)(import_meta2.url));
|
|
103106
|
+
}
|
|
103107
|
+
return __dirname;
|
|
103108
|
+
}
|
|
103109
|
+
function resolveMigrationsDir(name = "migrations") {
|
|
103110
|
+
return (0, import_path14.join)(getMigrationsDir(), name);
|
|
103111
|
+
}
|
|
103112
|
+
function runSqliteMigrations(db, migrationsDir) {
|
|
103113
|
+
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
103114
|
+
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
103115
|
+
const files = (0, import_fs15.readdirSync)(dir).filter((f2) => f2.endsWith(".sql")).sort();
|
|
103116
|
+
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
103117
|
+
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
103118
|
+
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
103119
|
+
const applied = [];
|
|
103120
|
+
const skipped = [];
|
|
103121
|
+
for (const file2 of files) {
|
|
103122
|
+
if (appliedSet.has(file2)) {
|
|
103123
|
+
skipped.push(file2);
|
|
103124
|
+
continue;
|
|
103125
|
+
}
|
|
103126
|
+
const sql = (0, import_fs15.readFileSync)((0, import_path14.join)(dir, file2), "utf-8");
|
|
103127
|
+
const tx = db.transaction(() => {
|
|
103128
|
+
db.exec(sql);
|
|
103129
|
+
recordApplied.run(file2, (/* @__PURE__ */ new Date()).toISOString());
|
|
103130
|
+
});
|
|
103131
|
+
tx();
|
|
103132
|
+
applied.push(file2);
|
|
103133
|
+
}
|
|
103134
|
+
return { applied, skipped };
|
|
103135
|
+
}
|
|
103136
|
+
var import_fs15, import_path14, import_url6, import_meta2, SCHEMA_MIGRATIONS_SQL;
|
|
103137
|
+
var init_sqlite_migrate = __esm({
|
|
103138
|
+
"src/db/sqlite-migrate.ts"() {
|
|
103139
|
+
"use strict";
|
|
103140
|
+
import_fs15 = require("fs");
|
|
103141
|
+
import_path14 = require("path");
|
|
103142
|
+
import_url6 = require("url");
|
|
103143
|
+
import_meta2 = {};
|
|
103144
|
+
SCHEMA_MIGRATIONS_SQL = `
|
|
103145
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
103146
|
+
id TEXT PRIMARY KEY,
|
|
103147
|
+
applied_at TEXT NOT NULL
|
|
103148
|
+
);
|
|
103149
|
+
`;
|
|
103150
|
+
}
|
|
103151
|
+
});
|
|
103152
|
+
|
|
102780
103153
|
// src/services/questions/codexScreen.ts
|
|
102781
103154
|
function parseCodexNumberedOptions(lines) {
|
|
102782
103155
|
const options = [];
|
|
@@ -114874,6 +115247,113 @@ var init_esm2 = __esm({
|
|
|
114874
115247
|
}
|
|
114875
115248
|
});
|
|
114876
115249
|
|
|
115250
|
+
// src/db/runtime-store.ts
|
|
115251
|
+
var runtime_store_exports = {};
|
|
115252
|
+
__export(runtime_store_exports, {
|
|
115253
|
+
RuntimeStore: () => RuntimeStore,
|
|
115254
|
+
resolveRuntimeDbPath: () => resolveRuntimeDbPath
|
|
115255
|
+
});
|
|
115256
|
+
function resolveRuntimeDbPath(override) {
|
|
115257
|
+
return override ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path23.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path23.join)((0, import_os9.homedir)(), ".threadbase"), "runtime.db");
|
|
115258
|
+
}
|
|
115259
|
+
var import_better_sqlite33, import_os9, import_path23, RuntimeStore;
|
|
115260
|
+
var init_runtime_store = __esm({
|
|
115261
|
+
"src/db/runtime-store.ts"() {
|
|
115262
|
+
"use strict";
|
|
115263
|
+
import_better_sqlite33 = __toESM(require("better-sqlite3"), 1);
|
|
115264
|
+
import_os9 = require("os");
|
|
115265
|
+
import_path23 = require("path");
|
|
115266
|
+
init_query_timing();
|
|
115267
|
+
init_sqlite_migrate();
|
|
115268
|
+
RuntimeStore = class _RuntimeStore {
|
|
115269
|
+
constructor(db) {
|
|
115270
|
+
this.db = db;
|
|
115271
|
+
}
|
|
115272
|
+
db;
|
|
115273
|
+
static open(dbPath, migrationsDir) {
|
|
115274
|
+
const db = instrumentDatabase(new import_better_sqlite33.default(dbPath));
|
|
115275
|
+
db.pragma("journal_mode = WAL");
|
|
115276
|
+
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
115277
|
+
return new _RuntimeStore(db);
|
|
115278
|
+
}
|
|
115279
|
+
getDatabase() {
|
|
115280
|
+
return this.db;
|
|
115281
|
+
}
|
|
115282
|
+
/**
|
|
115283
|
+
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
115284
|
+
*
|
|
115285
|
+
* Returns the number of rows copied.
|
|
115286
|
+
*/
|
|
115287
|
+
importLegacyManagedSessions(source) {
|
|
115288
|
+
return this.importLegacyTable(source, "managed_sessions");
|
|
115289
|
+
}
|
|
115290
|
+
/**
|
|
115291
|
+
* One-time move of `devices` rows out of `cache.db`, where the registry used
|
|
115292
|
+
* to live (migration `011_create_devices.sql`).
|
|
115293
|
+
*
|
|
115294
|
+
* Losing this table invalidates every device token ever issued, and cache.db
|
|
115295
|
+
* is the file `tb-streamer cache clear` deletes and the integrity monitor
|
|
115296
|
+
* rebuilds — see `runtime-migrations/003_create_devices.sql`.
|
|
115297
|
+
*
|
|
115298
|
+
* Unlike `managed_sessions`, this one MOVES rather than copies: the source
|
|
115299
|
+
* rows are deleted once the copy is verified. A `devices` row carries a
|
|
115300
|
+
* user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
|
|
115301
|
+
* on disk indefinitely — in the one file the user is told to delete when
|
|
115302
|
+
* something goes wrong — is more retained personal data than the rollback
|
|
115303
|
+
* path is worth. Recovering from a rollback is re-scanning a pairing QR.
|
|
115304
|
+
*
|
|
115305
|
+
* The delete is conditional on the copy being complete: `INSERT OR IGNORE`
|
|
115306
|
+
* can silently skip a row, so the destination count must match what was read
|
|
115307
|
+
* before anything is removed. A mismatch keeps the source and reports
|
|
115308
|
+
* `purged: false` rather than throwing — the import itself still succeeded,
|
|
115309
|
+
* and keeping data is the safe direction to fail in.
|
|
115310
|
+
*/
|
|
115311
|
+
importLegacyDevices(source) {
|
|
115312
|
+
const copied = this.importLegacyTable(source, "devices");
|
|
115313
|
+
if (copied === 0) return { copied: 0, purged: false };
|
|
115314
|
+
const landed = this.db.prepare("SELECT COUNT(*) AS n FROM devices").get().n;
|
|
115315
|
+
if (landed !== copied) return { copied, purged: false };
|
|
115316
|
+
source.prepare("DELETE FROM devices").run();
|
|
115317
|
+
return { copied, purged: true };
|
|
115318
|
+
}
|
|
115319
|
+
/**
|
|
115320
|
+
* Copy a whole table out of a pre-split `cache.db` into this file.
|
|
115321
|
+
*
|
|
115322
|
+
* The copy itself is non-destructive — the source is left in place, so an
|
|
115323
|
+
* older streamer rolled back onto the same machine still finds its data.
|
|
115324
|
+
* `importLegacyDevices` deletes the source afterwards for its own reasons;
|
|
115325
|
+
* `managed_sessions` does not. Runs only when this file's table is empty, so
|
|
115326
|
+
* a second boot is a no-op rather than a re-copy that would resurrect rows
|
|
115327
|
+
* deleted since.
|
|
115328
|
+
*
|
|
115329
|
+
* The table name is interpolated into SQL, so it is typed as a closed union
|
|
115330
|
+
* rather than `string` — the set of tables that can ever be lifted is known
|
|
115331
|
+
* at compile time, and that is what keeps a caller from making this a hole.
|
|
115332
|
+
*/
|
|
115333
|
+
importLegacyTable(source, table) {
|
|
115334
|
+
const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
115335
|
+
if (existing.n > 0) return 0;
|
|
115336
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
115337
|
+
if (!hasTable) return 0;
|
|
115338
|
+
const rows = source.prepare(`SELECT * FROM ${table}`).all();
|
|
115339
|
+
if (rows.length === 0) return 0;
|
|
115340
|
+
const columns = Object.keys(rows[0]);
|
|
115341
|
+
const insert = this.db.prepare(
|
|
115342
|
+
`INSERT OR IGNORE INTO ${table} (${columns.join(", ")})
|
|
115343
|
+
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
115344
|
+
);
|
|
115345
|
+
this.db.transaction((batch) => {
|
|
115346
|
+
for (const row of batch) insert.run(row);
|
|
115347
|
+
})(rows);
|
|
115348
|
+
return rows.length;
|
|
115349
|
+
}
|
|
115350
|
+
close() {
|
|
115351
|
+
this.db.close();
|
|
115352
|
+
}
|
|
115353
|
+
};
|
|
115354
|
+
}
|
|
115355
|
+
});
|
|
115356
|
+
|
|
114877
115357
|
// node_modules/@xterm/headless/lib-headless/xterm-headless.js
|
|
114878
115358
|
var require_xterm_headless = __commonJS({
|
|
114879
115359
|
"node_modules/@xterm/headless/lib-headless/xterm-headless.js"(exports2) {
|
|
@@ -119964,8 +120444,8 @@ var init_pty_shared = __esm({
|
|
|
119964
120444
|
|
|
119965
120445
|
// src/services/questions/codexGateAnswers.ts
|
|
119966
120446
|
function gateAnswersPath() {
|
|
119967
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0,
|
|
119968
|
-
return (0,
|
|
120447
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path24.join)((0, import_os10.homedir)(), ".threadbase");
|
|
120448
|
+
return (0, import_path24.join)(dir, "gate-answers.json");
|
|
119969
120449
|
}
|
|
119970
120450
|
function loadGateAnswers() {
|
|
119971
120451
|
try {
|
|
@@ -119977,7 +120457,7 @@ function loadGateAnswers() {
|
|
|
119977
120457
|
}
|
|
119978
120458
|
function saveGateAnswer(key, value) {
|
|
119979
120459
|
const path2 = gateAnswersPath();
|
|
119980
|
-
(0, import_fs24.mkdirSync)((0,
|
|
120460
|
+
(0, import_fs24.mkdirSync)((0, import_path24.dirname)(path2), { recursive: true });
|
|
119981
120461
|
(0, import_fs24.writeFileSync)(path2, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
|
|
119982
120462
|
`);
|
|
119983
120463
|
}
|
|
@@ -119990,13 +120470,13 @@ function rememberedGateDigit(gate) {
|
|
|
119990
120470
|
}
|
|
119991
120471
|
return answers.codexTrustGate === "yes" ? "1" : null;
|
|
119992
120472
|
}
|
|
119993
|
-
var import_fs24,
|
|
120473
|
+
var import_fs24, import_os10, import_path24;
|
|
119994
120474
|
var init_codexGateAnswers = __esm({
|
|
119995
120475
|
"src/services/questions/codexGateAnswers.ts"() {
|
|
119996
120476
|
"use strict";
|
|
119997
120477
|
import_fs24 = require("fs");
|
|
119998
|
-
|
|
119999
|
-
|
|
120478
|
+
import_os10 = require("os");
|
|
120479
|
+
import_path24 = require("path");
|
|
120000
120480
|
}
|
|
120001
120481
|
});
|
|
120002
120482
|
|
|
@@ -120057,13 +120537,13 @@ function toPublicSession(s3) {
|
|
|
120057
120537
|
...s3.filePath != null && { filePath: s3.filePath }
|
|
120058
120538
|
};
|
|
120059
120539
|
}
|
|
120060
|
-
var import_crypto9, import_fs25,
|
|
120540
|
+
var import_crypto9, import_fs25, import_path25, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
|
|
120061
120541
|
var init_codex_pty_runner = __esm({
|
|
120062
120542
|
"src/codex-pty-runner.ts"() {
|
|
120063
120543
|
"use strict";
|
|
120064
120544
|
import_crypto9 = require("crypto");
|
|
120065
120545
|
import_fs25 = require("fs");
|
|
120066
|
-
|
|
120546
|
+
import_path25 = require("path");
|
|
120067
120547
|
init_logger();
|
|
120068
120548
|
init_platform();
|
|
120069
120549
|
init_providers();
|
|
@@ -120164,7 +120644,7 @@ var init_codex_pty_runner = __esm({
|
|
|
120164
120644
|
// resume, fresh and fork is argv.
|
|
120165
120645
|
async launch(sessionId, args, options) {
|
|
120166
120646
|
const nodePty = await loadPty();
|
|
120167
|
-
const projectName = options.projectName ?? (0,
|
|
120647
|
+
const projectName = options.projectName ?? (0, import_path25.basename)(options.projectPath);
|
|
120168
120648
|
let proc;
|
|
120169
120649
|
try {
|
|
120170
120650
|
proc = nodePty.spawn(resolveCodexExe(), args, {
|
|
@@ -121407,13 +121887,13 @@ function toPublicSession2(s3) {
|
|
|
121407
121887
|
...s3.firstMessageText != null && { firstMessageText: s3.firstMessageText }
|
|
121408
121888
|
};
|
|
121409
121889
|
}
|
|
121410
|
-
var import_crypto10, import_fs26,
|
|
121890
|
+
var import_crypto10, import_fs26, import_path26, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
|
|
121411
121891
|
var init_pty_manager = __esm({
|
|
121412
121892
|
"src/pty-manager.ts"() {
|
|
121413
121893
|
"use strict";
|
|
121414
121894
|
import_crypto10 = require("crypto");
|
|
121415
121895
|
import_fs26 = require("fs");
|
|
121416
|
-
|
|
121896
|
+
import_path26 = require("path");
|
|
121417
121897
|
init_claude_flags();
|
|
121418
121898
|
init_logger();
|
|
121419
121899
|
init_platform();
|
|
@@ -121535,7 +122015,7 @@ var init_pty_manager = __esm({
|
|
|
121535
122015
|
}
|
|
121536
122016
|
async doStart(sessionId, options) {
|
|
121537
122017
|
const nodePty = await loadPty();
|
|
121538
|
-
const projectName = options.projectName ?? (0,
|
|
122018
|
+
const projectName = options.projectName ?? (0, import_path26.basename)(options.projectPath);
|
|
121539
122019
|
const permissionMode = options.permissionMode ?? "acceptEdits";
|
|
121540
122020
|
const args = [
|
|
121541
122021
|
"--permission-mode",
|
|
@@ -121599,7 +122079,7 @@ var init_pty_manager = __esm({
|
|
|
121599
122079
|
async startFresh(options) {
|
|
121600
122080
|
const nodePty = await loadPty();
|
|
121601
122081
|
const sessionId = (0, import_crypto10.randomUUID)();
|
|
121602
|
-
const projectName = options.projectName ?? (0,
|
|
122082
|
+
const projectName = options.projectName ?? (0, import_path26.basename)(options.projectPath);
|
|
121603
122083
|
const permissionMode = options.permissionMode ?? "acceptEdits";
|
|
121604
122084
|
const args = [
|
|
121605
122085
|
"--permission-mode",
|
|
@@ -122205,11 +122685,11 @@ var init_pty_manager = __esm({
|
|
|
122205
122685
|
});
|
|
122206
122686
|
|
|
122207
122687
|
// src/live-session-manager.ts
|
|
122208
|
-
var
|
|
122688
|
+
var import_path27, LiveSessionManager;
|
|
122209
122689
|
var init_live_session_manager = __esm({
|
|
122210
122690
|
"src/live-session-manager.ts"() {
|
|
122211
122691
|
"use strict";
|
|
122212
|
-
|
|
122692
|
+
import_path27 = require("path");
|
|
122213
122693
|
init_codex_pty_runner();
|
|
122214
122694
|
init_providers();
|
|
122215
122695
|
init_remote_session_runner();
|
|
@@ -122352,7 +122832,7 @@ var init_live_session_manager = __esm({
|
|
|
122352
122832
|
const runner = this.runners.get(provider);
|
|
122353
122833
|
if (runner) return runner;
|
|
122354
122834
|
const err = new Error(
|
|
122355
|
-
`Live ${provider} sessions are not implemented yet for ${(0,
|
|
122835
|
+
`Live ${provider} sessions are not implemented yet for ${(0, import_path27.basename)(projectPath)}`
|
|
122356
122836
|
);
|
|
122357
122837
|
err.statusCode = 501;
|
|
122358
122838
|
throw err;
|
|
@@ -131471,8 +131951,8 @@ var import_events2 = require("events");
|
|
|
131471
131951
|
var import_fs38 = require("fs");
|
|
131472
131952
|
var import_promises16 = require("fs/promises");
|
|
131473
131953
|
var import_http = require("http");
|
|
131474
|
-
var
|
|
131475
|
-
var
|
|
131954
|
+
var import_os16 = require("os");
|
|
131955
|
+
var import_path34 = require("path");
|
|
131476
131956
|
|
|
131477
131957
|
// src/agent/agent-client.ts
|
|
131478
131958
|
var userInputSignal = { type: "signal", name: "userInput" };
|
|
@@ -133697,203 +134177,8 @@ init_logger();
|
|
|
133697
134177
|
|
|
133698
134178
|
// src/api/middleware/auth.middleware.ts
|
|
133699
134179
|
init_auth();
|
|
133700
|
-
|
|
133701
|
-
|
|
133702
|
-
var import_crypto3 = require("crypto");
|
|
133703
|
-
|
|
133704
|
-
// src/services/security/capabilities.ts
|
|
133705
|
-
var CAPABILITIES = [
|
|
133706
|
-
"history:read",
|
|
133707
|
-
// read conversations, search
|
|
133708
|
-
"session:control",
|
|
133709
|
-
// start, resume, send input, interrupt
|
|
133710
|
-
"fs:browse",
|
|
133711
|
-
// browse the project tree
|
|
133712
|
-
"fs:upload",
|
|
133713
|
-
// upload files into a project
|
|
133714
|
-
"notifications",
|
|
133715
|
-
// register for push
|
|
133716
|
-
"admin"
|
|
133717
|
-
// rotate keys, manage devices
|
|
133718
|
-
];
|
|
133719
|
-
function isCapability(value) {
|
|
133720
|
-
return typeof value === "string" && CAPABILITIES.includes(value);
|
|
133721
|
-
}
|
|
133722
|
-
var FULL_CAPABILITIES = [
|
|
133723
|
-
"history:read",
|
|
133724
|
-
"session:control",
|
|
133725
|
-
"fs:browse",
|
|
133726
|
-
"fs:upload",
|
|
133727
|
-
"notifications"
|
|
133728
|
-
];
|
|
133729
|
-
var READ_ONLY_CAPABILITIES = ["history:read"];
|
|
133730
|
-
function capabilitiesForPreset(preset) {
|
|
133731
|
-
return preset === "read-only" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];
|
|
133732
|
-
}
|
|
133733
|
-
function legacyPrincipal() {
|
|
133734
|
-
return { kind: "legacy", capabilities: [...FULL_CAPABILITIES, "admin"] };
|
|
133735
|
-
}
|
|
133736
|
-
function hasCapability(principal, required2) {
|
|
133737
|
-
return principal.capabilities.includes(required2);
|
|
133738
|
-
}
|
|
133739
|
-
var ROUTE_CAPABILITIES = [
|
|
133740
|
-
// Most specific first for readability; matching sorts by length anyway.
|
|
133741
|
-
["/api/sessions/", "session:control"],
|
|
133742
|
-
["/api/sessions", "history:read"],
|
|
133743
|
-
// listing sessions is a read
|
|
133744
|
-
["/api/conversations", "history:read"],
|
|
133745
|
-
["/api/projects", "history:read"],
|
|
133746
|
-
["/api/search", "history:read"],
|
|
133747
|
-
["/api/providers", "history:read"],
|
|
133748
|
-
["/api/browse", "fs:browse"],
|
|
133749
|
-
["/api/upload", "fs:upload"],
|
|
133750
|
-
["/api/push", "notifications"],
|
|
133751
|
-
["/api/devices", "admin"],
|
|
133752
|
-
["/api/config", "admin"],
|
|
133753
|
-
["/api/auth/rotate", "admin"],
|
|
133754
|
-
["/api/backup", "admin"],
|
|
133755
|
-
// Server identity and capability discovery. A read-only device must be able
|
|
133756
|
-
// to see WHICH server it is talking to and what it supports, or it cannot
|
|
133757
|
-
// render anything at all.
|
|
133758
|
-
["/api/info", "history:read"],
|
|
133759
|
-
["/api/profiles", "history:read"],
|
|
133760
|
-
["/api/diagnostics", "history:read"],
|
|
133761
|
-
["/api/cache/alert", "history:read"],
|
|
133762
|
-
// Client log shipping: any authenticated client may report its own errors.
|
|
133763
|
-
// Gating this behind a capability would silence diagnostics from exactly the
|
|
133764
|
-
// devices most likely to be misbehaving.
|
|
133765
|
-
["/api/__client-log", "history:read"],
|
|
133766
|
-
// Logs viewer is localhost-only and already bypasses this middleware; the
|
|
133767
|
-
// mapping exists so a remote request is classified rather than denied as
|
|
133768
|
-
// unclassified.
|
|
133769
|
-
["/api/logs", "admin"],
|
|
133770
|
-
// Pairing routes other than the public exchange (e.g. minting a token).
|
|
133771
|
-
["/api/pair", "admin"],
|
|
133772
|
-
// The live WebSocket. Subscribing is a read — terminal output, session
|
|
133773
|
-
// updates, conversation events. Control still flows through the HTTP input
|
|
133774
|
-
// routes, which carry their own capability check, so a read-only device can
|
|
133775
|
-
// watch a session stream without being able to drive it.
|
|
133776
|
-
["/ws", "history:read"],
|
|
133777
|
-
// Progress webhook (multi-agent). Authenticated by HMAC in the handler and
|
|
133778
|
-
// already skipped by the middleware; classified so a stray request is denied
|
|
133779
|
-
// by rule rather than as "unclassified".
|
|
133780
|
-
["/internal/sessions", "admin"]
|
|
133781
|
-
];
|
|
133782
|
-
function requiredCapability(path2, method) {
|
|
133783
|
-
if (path2.startsWith("/api/sessions") && (method === "GET" || method === "HEAD")) {
|
|
133784
|
-
return "history:read";
|
|
133785
|
-
}
|
|
133786
|
-
let best = null;
|
|
133787
|
-
for (const [prefix, cap] of ROUTE_CAPABILITIES) {
|
|
133788
|
-
if (path2.startsWith(prefix) && (best === null || prefix.length > best.len)) {
|
|
133789
|
-
best = { len: prefix.length, cap };
|
|
133790
|
-
}
|
|
133791
|
-
}
|
|
133792
|
-
return best?.cap ?? null;
|
|
133793
|
-
}
|
|
133794
|
-
|
|
133795
|
-
// src/db/repositories/devices.repository.ts
|
|
133796
|
-
function generateDeviceToken() {
|
|
133797
|
-
return (0, import_crypto3.randomBytes)(32).toString("base64url");
|
|
133798
|
-
}
|
|
133799
|
-
function hashDeviceToken(token) {
|
|
133800
|
-
return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
|
|
133801
|
-
}
|
|
133802
|
-
function safeHashEquals(a, b2) {
|
|
133803
|
-
if (a.length !== b2.length) return false;
|
|
133804
|
-
return (0, import_crypto3.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b2, "utf8"));
|
|
133805
|
-
}
|
|
133806
|
-
function parseCapabilities(raw2) {
|
|
133807
|
-
try {
|
|
133808
|
-
const parsed = JSON.parse(raw2);
|
|
133809
|
-
if (!Array.isArray(parsed)) return [];
|
|
133810
|
-
return parsed.filter(isCapability);
|
|
133811
|
-
} catch {
|
|
133812
|
-
return [];
|
|
133813
|
-
}
|
|
133814
|
-
}
|
|
133815
|
-
function toDeviceView(row) {
|
|
133816
|
-
return {
|
|
133817
|
-
deviceId: row.device_id,
|
|
133818
|
-
name: row.name,
|
|
133819
|
-
capabilities: parseCapabilities(row.capabilities),
|
|
133820
|
-
createdAt: row.created_at,
|
|
133821
|
-
lastSeenAt: row.last_seen_at,
|
|
133822
|
-
revokedAt: row.revoked_at
|
|
133823
|
-
};
|
|
133824
|
-
}
|
|
133825
|
-
var DevicesRepository = class {
|
|
133826
|
-
insertStmt;
|
|
133827
|
-
byTokenHashStmt;
|
|
133828
|
-
byIdStmt;
|
|
133829
|
-
listStmt;
|
|
133830
|
-
revokeStmt;
|
|
133831
|
-
touchStmt;
|
|
133832
|
-
constructor(db) {
|
|
133833
|
-
this.insertStmt = db.prepare(`
|
|
133834
|
-
INSERT INTO devices (
|
|
133835
|
-
device_id, public_key, token_hash, name, capabilities, created_at
|
|
133836
|
-
) VALUES (
|
|
133837
|
-
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at
|
|
133838
|
-
)
|
|
133839
|
-
`);
|
|
133840
|
-
this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
|
|
133841
|
-
this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
|
|
133842
|
-
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
133843
|
-
this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
|
|
133844
|
-
this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
|
|
133845
|
-
}
|
|
133846
|
-
/**
|
|
133847
|
-
* Record a newly paired device and mint its token.
|
|
133848
|
-
*
|
|
133849
|
-
* The raw token is returned to the caller and never stored — this is the only
|
|
133850
|
-
* moment it exists outside the client.
|
|
133851
|
-
*/
|
|
133852
|
-
register(args) {
|
|
133853
|
-
const deviceId = (0, import_crypto3.randomUUID)();
|
|
133854
|
-
const deviceToken = generateDeviceToken();
|
|
133855
|
-
const capabilities = capabilitiesForPreset(args.preset ?? "full");
|
|
133856
|
-
this.insertStmt.run({
|
|
133857
|
-
device_id: deviceId,
|
|
133858
|
-
public_key: args.publicKey,
|
|
133859
|
-
token_hash: hashDeviceToken(deviceToken),
|
|
133860
|
-
name: args.name ?? null,
|
|
133861
|
-
capabilities: JSON.stringify(capabilities),
|
|
133862
|
-
created_at: args.now ?? Date.now()
|
|
133863
|
-
});
|
|
133864
|
-
return { deviceId, deviceToken, capabilities };
|
|
133865
|
-
}
|
|
133866
|
-
/**
|
|
133867
|
-
* Resolve a presented token to a device, or null.
|
|
133868
|
-
*
|
|
133869
|
-
* Returns null for a revoked device, so revocation takes effect on the very
|
|
133870
|
-
* next request with no cache to go stale.
|
|
133871
|
-
*/
|
|
133872
|
-
authenticate(token) {
|
|
133873
|
-
const hash2 = hashDeviceToken(token);
|
|
133874
|
-
const row = this.byTokenHashStmt.get(hash2);
|
|
133875
|
-
if (!row) return null;
|
|
133876
|
-
if (!safeHashEquals(row.token_hash, hash2)) return null;
|
|
133877
|
-
if (row.revoked_at != null) return null;
|
|
133878
|
-
return row;
|
|
133879
|
-
}
|
|
133880
|
-
get(deviceId) {
|
|
133881
|
-
return this.byIdStmt.get(deviceId) ?? null;
|
|
133882
|
-
}
|
|
133883
|
-
/** All devices, including revoked ones — an audit surface needs the history. */
|
|
133884
|
-
list() {
|
|
133885
|
-
return this.listStmt.all().map(toDeviceView);
|
|
133886
|
-
}
|
|
133887
|
-
/** Revoke one device. Others are untouched — no key rotation, no collateral. */
|
|
133888
|
-
revoke(deviceId, now = Date.now()) {
|
|
133889
|
-
return this.revokeStmt.run(now, deviceId).changes > 0;
|
|
133890
|
-
}
|
|
133891
|
-
touch(deviceId, now = Date.now()) {
|
|
133892
|
-
this.touchStmt.run(now, deviceId);
|
|
133893
|
-
}
|
|
133894
|
-
};
|
|
133895
|
-
|
|
133896
|
-
// src/api/middleware/auth.middleware.ts
|
|
134180
|
+
init_devices_repository();
|
|
134181
|
+
init_capabilities();
|
|
133897
134182
|
function isLocalRequest(remoteAddr) {
|
|
133898
134183
|
const addr = remoteAddr ?? "";
|
|
133899
134184
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
@@ -134404,6 +134689,34 @@ var createDeviceRoutes = (deps) => {
|
|
|
134404
134689
|
repo.revoke(id);
|
|
134405
134690
|
return c.json({ ok: true, alreadyRevoked: false });
|
|
134406
134691
|
});
|
|
134692
|
+
app.delete("/:id", (c) => {
|
|
134693
|
+
const repo = deps.devicesRepo();
|
|
134694
|
+
if (!repo) {
|
|
134695
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
134696
|
+
}
|
|
134697
|
+
const id = c.req.param("id");
|
|
134698
|
+
const existing = repo.get(id);
|
|
134699
|
+
if (!existing) return c.json({ ok: true, alreadyDeleted: true });
|
|
134700
|
+
const force = c.req.query("force") === "1" || c.req.query("force") === "true";
|
|
134701
|
+
if (existing.revoked_at == null && !force) {
|
|
134702
|
+
return c.json(
|
|
134703
|
+
{
|
|
134704
|
+
error: "Revoke the device before deleting it, or pass ?force=1",
|
|
134705
|
+
code: "DEVICE_ACTIVE"
|
|
134706
|
+
},
|
|
134707
|
+
409
|
|
134708
|
+
);
|
|
134709
|
+
}
|
|
134710
|
+
repo.delete(id);
|
|
134711
|
+
return c.json({ ok: true, alreadyDeleted: false });
|
|
134712
|
+
});
|
|
134713
|
+
app.delete("/", (c) => {
|
|
134714
|
+
const repo = deps.devicesRepo();
|
|
134715
|
+
if (!repo) {
|
|
134716
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
134717
|
+
}
|
|
134718
|
+
return c.json({ ok: true, deleted: repo.deleteRevoked() });
|
|
134719
|
+
});
|
|
134407
134720
|
return app;
|
|
134408
134721
|
};
|
|
134409
134722
|
|
|
@@ -135278,6 +135591,13 @@ var createMiscRoutes = (deps) => {
|
|
|
135278
135591
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
135279
135592
|
// Hub's grouped views need before they can draw a tree.
|
|
135280
135593
|
projectSummary: true,
|
|
135594
|
+
// The paired-device registry lives in runtime.db, so it survives
|
|
135595
|
+
// `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.
|
|
135596
|
+
// A client may only prefer its scoped device token over the shared API
|
|
135597
|
+
// key when this is true: on an older server the registry is inside
|
|
135598
|
+
// cache.db, where a documented troubleshooting step deletes it and every
|
|
135599
|
+
// device token with it. Absent means "old server, assume not durable".
|
|
135600
|
+
devicesDurable: true,
|
|
135281
135601
|
// Delivery capability, not endpoint support: whether this server can
|
|
135282
135602
|
// actually send a push, so mobile can hide an affordance instead of
|
|
135283
135603
|
// registering tokens nothing will ever send to. Absent on older servers,
|
|
@@ -135817,7 +136137,13 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
135817
136137
|
const app = new Hono2();
|
|
135818
136138
|
app.get(
|
|
135819
136139
|
"/ws",
|
|
135820
|
-
|
|
136140
|
+
// The principal is read here, at the upgrade, and captured for the life of
|
|
136141
|
+
// the socket. authMiddleware sets it because /ws is classified
|
|
136142
|
+
// `history:read`, but it only ever reaches the HTTP request — without
|
|
136143
|
+
// capturing it the socket has no principal at all, so every frame after
|
|
136144
|
+
// the upgrade is unauthorized-by-omission.
|
|
136145
|
+
upgradeWebSocket((c) => {
|
|
136146
|
+
const principal = c.get("principal") ?? null;
|
|
135821
136147
|
let openWs = null;
|
|
135822
136148
|
return {
|
|
135823
136149
|
onOpen(_evt, ws2) {
|
|
@@ -135827,7 +136153,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
135827
136153
|
deps.handleWsOpen(raw2);
|
|
135828
136154
|
},
|
|
135829
136155
|
onMessage(evt, _ws) {
|
|
135830
|
-
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
136156
|
+
if (openWs) deps.handleWsMessage(openWs, evt.data, principal);
|
|
135831
136157
|
},
|
|
135832
136158
|
onClose(_evt, _ws) {
|
|
135833
136159
|
if (openWs) deps.handleWsClose(openWs);
|
|
@@ -143257,122 +143583,8 @@ var import_fs17 = require("fs");
|
|
|
143257
143583
|
var import_promises11 = require("fs/promises");
|
|
143258
143584
|
var import_path16 = require("path");
|
|
143259
143585
|
var import_promises12 = require("timers/promises");
|
|
143260
|
-
|
|
143261
|
-
|
|
143262
|
-
init_logger();
|
|
143263
|
-
var log2 = getLogger("db");
|
|
143264
|
-
var DEFAULT_SLOW_QUERY_MS = 35;
|
|
143265
|
-
var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
|
|
143266
|
-
function deriveLabel(sql) {
|
|
143267
|
-
const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
|
|
143268
|
-
const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
|
|
143269
|
-
return `${verb}:${table}`;
|
|
143270
|
-
}
|
|
143271
|
-
function resolveSlowMs() {
|
|
143272
|
-
const raw2 = process.env.THREADBASE_DB_SLOW_QUERY_MS;
|
|
143273
|
-
if (raw2 === void 0 || raw2 === "") return DEFAULT_SLOW_QUERY_MS;
|
|
143274
|
-
const parsed = Number(raw2);
|
|
143275
|
-
return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
|
|
143276
|
-
}
|
|
143277
|
-
function record2(label, ms2, rows, slowMs) {
|
|
143278
|
-
if (slowMs > 0 && ms2 >= slowMs) {
|
|
143279
|
-
log2.warn(
|
|
143280
|
-
`[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
|
|
143281
|
-
{ event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143282
|
-
"pino"
|
|
143283
|
-
);
|
|
143284
|
-
return;
|
|
143285
|
-
}
|
|
143286
|
-
if (log2.pino.isLevelEnabled("debug")) {
|
|
143287
|
-
log2.debug(
|
|
143288
|
-
`[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
|
|
143289
|
-
{ event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
|
|
143290
|
-
"pino"
|
|
143291
|
-
);
|
|
143292
|
-
}
|
|
143293
|
-
}
|
|
143294
|
-
function rowsOf(method, result) {
|
|
143295
|
-
if (method === "all") return Array.isArray(result) ? result.length : 0;
|
|
143296
|
-
if (method === "run") return result?.changes ?? 0;
|
|
143297
|
-
return result === void 0 ? 0 : 1;
|
|
143298
|
-
}
|
|
143299
|
-
function instrumentDatabase(db, options = {}) {
|
|
143300
|
-
const slowMs = options.slowMs ?? resolveSlowMs();
|
|
143301
|
-
const prepare = db.prepare.bind(db);
|
|
143302
|
-
db.prepare = ((sql) => {
|
|
143303
|
-
const stmt = prepare(sql);
|
|
143304
|
-
const box = { label: deriveLabel(sql) };
|
|
143305
|
-
Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
|
|
143306
|
-
for (const method of ["get", "all", "run"]) {
|
|
143307
|
-
const original = stmt[method].bind(stmt);
|
|
143308
|
-
Object.defineProperty(stmt, method, {
|
|
143309
|
-
configurable: true,
|
|
143310
|
-
writable: true,
|
|
143311
|
-
value: (...args) => {
|
|
143312
|
-
const started = performance.now();
|
|
143313
|
-
const result = original(...args);
|
|
143314
|
-
record2(box.label, performance.now() - started, rowsOf(method, result), slowMs);
|
|
143315
|
-
return result;
|
|
143316
|
-
}
|
|
143317
|
-
});
|
|
143318
|
-
}
|
|
143319
|
-
return stmt;
|
|
143320
|
-
});
|
|
143321
|
-
return db;
|
|
143322
|
-
}
|
|
143323
|
-
function labelStatements(statements) {
|
|
143324
|
-
for (const [name, stmt] of Object.entries(statements)) {
|
|
143325
|
-
const box = stmt?.[LABEL];
|
|
143326
|
-
if (box) box.label = name;
|
|
143327
|
-
}
|
|
143328
|
-
}
|
|
143329
|
-
|
|
143330
|
-
// src/db/sqlite-migrate.ts
|
|
143331
|
-
var import_fs15 = require("fs");
|
|
143332
|
-
var import_path14 = require("path");
|
|
143333
|
-
var import_url6 = require("url");
|
|
143334
|
-
var import_meta2 = {};
|
|
143335
|
-
function getMigrationsDir() {
|
|
143336
|
-
if (typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
143337
|
-
return (0, import_path14.dirname)((0, import_url6.fileURLToPath)(import_meta2.url));
|
|
143338
|
-
}
|
|
143339
|
-
return __dirname;
|
|
143340
|
-
}
|
|
143341
|
-
function resolveMigrationsDir(name = "migrations") {
|
|
143342
|
-
return (0, import_path14.join)(getMigrationsDir(), name);
|
|
143343
|
-
}
|
|
143344
|
-
var SCHEMA_MIGRATIONS_SQL = `
|
|
143345
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
143346
|
-
id TEXT PRIMARY KEY,
|
|
143347
|
-
applied_at TEXT NOT NULL
|
|
143348
|
-
);
|
|
143349
|
-
`;
|
|
143350
|
-
function runSqliteMigrations(db, migrationsDir) {
|
|
143351
|
-
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
143352
|
-
const dir = migrationsDir ?? resolveMigrationsDir();
|
|
143353
|
-
const files = (0, import_fs15.readdirSync)(dir).filter((f2) => f2.endsWith(".sql")).sort();
|
|
143354
|
-
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
143355
|
-
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
143356
|
-
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
143357
|
-
const applied = [];
|
|
143358
|
-
const skipped = [];
|
|
143359
|
-
for (const file2 of files) {
|
|
143360
|
-
if (appliedSet.has(file2)) {
|
|
143361
|
-
skipped.push(file2);
|
|
143362
|
-
continue;
|
|
143363
|
-
}
|
|
143364
|
-
const sql = (0, import_fs15.readFileSync)((0, import_path14.join)(dir, file2), "utf-8");
|
|
143365
|
-
const tx = db.transaction(() => {
|
|
143366
|
-
db.exec(sql);
|
|
143367
|
-
recordApplied.run(file2, (/* @__PURE__ */ new Date()).toISOString());
|
|
143368
|
-
});
|
|
143369
|
-
tx();
|
|
143370
|
-
applied.push(file2);
|
|
143371
|
-
}
|
|
143372
|
-
return { applied, skipped };
|
|
143373
|
-
}
|
|
143374
|
-
|
|
143375
|
-
// src/conversation-cache.ts
|
|
143586
|
+
init_query_timing();
|
|
143587
|
+
init_sqlite_migrate();
|
|
143376
143588
|
init_logger();
|
|
143377
143589
|
init_providers();
|
|
143378
143590
|
|
|
@@ -147942,6 +148154,9 @@ var ConversationsRepository = class {
|
|
|
147942
148154
|
}
|
|
147943
148155
|
};
|
|
147944
148156
|
|
|
148157
|
+
// src/server.ts
|
|
148158
|
+
init_devices_repository();
|
|
148159
|
+
|
|
147945
148160
|
// src/db/repositories/managed-sessions.repository.ts
|
|
147946
148161
|
var PROBE_SET_MAX = 200;
|
|
147947
148162
|
var DIAGNOSTICS_MAX = 200;
|
|
@@ -148257,53 +148472,8 @@ var SessionsRepository = class {
|
|
|
148257
148472
|
}
|
|
148258
148473
|
};
|
|
148259
148474
|
|
|
148260
|
-
// src/
|
|
148261
|
-
|
|
148262
|
-
var RuntimeStore = class _RuntimeStore {
|
|
148263
|
-
constructor(db) {
|
|
148264
|
-
this.db = db;
|
|
148265
|
-
}
|
|
148266
|
-
db;
|
|
148267
|
-
static open(dbPath, migrationsDir) {
|
|
148268
|
-
const db = instrumentDatabase(new import_better_sqlite33.default(dbPath));
|
|
148269
|
-
db.pragma("journal_mode = WAL");
|
|
148270
|
-
runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
|
|
148271
|
-
return new _RuntimeStore(db);
|
|
148272
|
-
}
|
|
148273
|
-
getDatabase() {
|
|
148274
|
-
return this.db;
|
|
148275
|
-
}
|
|
148276
|
-
/**
|
|
148277
|
-
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
148278
|
-
*
|
|
148279
|
-
* Non-destructive by design: the source table is left in place so an older
|
|
148280
|
-
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
148281
|
-
* only when this file's table is empty, so a second boot is a no-op rather
|
|
148282
|
-
* than a re-copy that would resurrect rows deleted since.
|
|
148283
|
-
*
|
|
148284
|
-
* Returns the number of rows copied.
|
|
148285
|
-
*/
|
|
148286
|
-
importLegacyManagedSessions(source) {
|
|
148287
|
-
const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
|
|
148288
|
-
if (existing.n > 0) return 0;
|
|
148289
|
-
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
|
|
148290
|
-
if (!hasTable) return 0;
|
|
148291
|
-
const rows = source.prepare("SELECT * FROM managed_sessions").all();
|
|
148292
|
-
if (rows.length === 0) return 0;
|
|
148293
|
-
const columns = Object.keys(rows[0]);
|
|
148294
|
-
const insert = this.db.prepare(
|
|
148295
|
-
`INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
|
|
148296
|
-
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
148297
|
-
);
|
|
148298
|
-
this.db.transaction((batch) => {
|
|
148299
|
-
for (const row of batch) insert.run(row);
|
|
148300
|
-
})(rows);
|
|
148301
|
-
return rows.length;
|
|
148302
|
-
}
|
|
148303
|
-
close() {
|
|
148304
|
-
this.db.close();
|
|
148305
|
-
}
|
|
148306
|
-
};
|
|
148475
|
+
// src/server.ts
|
|
148476
|
+
init_runtime_store();
|
|
148307
148477
|
|
|
148308
148478
|
// src/external-tails.ts
|
|
148309
148479
|
var import_fs23 = require("fs");
|
|
@@ -148576,8 +148746,8 @@ function spawnDetachedHost(socketPath, entryPoint) {
|
|
|
148576
148746
|
|
|
148577
148747
|
// src/scanner-manager.ts
|
|
148578
148748
|
var import_fs28 = require("fs");
|
|
148579
|
-
var
|
|
148580
|
-
var
|
|
148749
|
+
var import_os12 = require("os");
|
|
148750
|
+
var import_path29 = require("path");
|
|
148581
148751
|
init_logger();
|
|
148582
148752
|
init_providers();
|
|
148583
148753
|
|
|
@@ -148899,9 +149069,9 @@ function refreshConversationCache(deps) {
|
|
|
148899
149069
|
|
|
148900
149070
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
148901
149071
|
var import_fs27 = require("fs");
|
|
148902
|
-
var
|
|
148903
|
-
var
|
|
148904
|
-
var DEFAULT_PROJECTS_DIR = (0,
|
|
149072
|
+
var import_os11 = require("os");
|
|
149073
|
+
var import_path28 = require("path");
|
|
149074
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path28.join)((0, import_os11.homedir)(), ".claude", "projects");
|
|
148905
149075
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
148906
149076
|
let maxMs;
|
|
148907
149077
|
try {
|
|
@@ -148913,7 +149083,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
148913
149083
|
for (const ent of (0, import_fs27.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
148914
149084
|
if (!ent.isDirectory()) continue;
|
|
148915
149085
|
try {
|
|
148916
|
-
const childMs = (0, import_fs27.statSync)((0,
|
|
149086
|
+
const childMs = (0, import_fs27.statSync)((0, import_path28.join)(projectsDir, ent.name)).mtimeMs;
|
|
148917
149087
|
if (childMs > maxMs) maxMs = childMs;
|
|
148918
149088
|
} catch {
|
|
148919
149089
|
}
|
|
@@ -149088,9 +149258,9 @@ var ScannerManager = class {
|
|
|
149088
149258
|
projectsDirs() {
|
|
149089
149259
|
const profiles = this.deps.scanProfiles;
|
|
149090
149260
|
if (profiles && profiles.length > 0) {
|
|
149091
|
-
return profiles.filter((p2) => p2.enabled).map((p2) => (0,
|
|
149261
|
+
return profiles.filter((p2) => p2.enabled).map((p2) => (0, import_path29.join)(p2.configDir, "projects"));
|
|
149092
149262
|
}
|
|
149093
|
-
return [(0,
|
|
149263
|
+
return [(0, import_path29.join)((0, import_os12.homedir)(), ".claude", "projects")];
|
|
149094
149264
|
}
|
|
149095
149265
|
// ─── staleness ────────────────────────────────────────────────────
|
|
149096
149266
|
// Drain the stale set and disarm the flag together. The caller owns the
|
|
@@ -149371,8 +149541,8 @@ var import_fs30 = require("fs");
|
|
|
149371
149541
|
|
|
149372
149542
|
// src/handlers/handleListProjects.ts
|
|
149373
149543
|
var import_fs29 = require("fs");
|
|
149374
|
-
var
|
|
149375
|
-
var
|
|
149544
|
+
var import_os13 = require("os");
|
|
149545
|
+
var import_path30 = require("path");
|
|
149376
149546
|
var HEAD_BYTES = 64 * 1024;
|
|
149377
149547
|
var MAX_FILES_PROBED = 3;
|
|
149378
149548
|
function readRecordedCwd(dir) {
|
|
@@ -149385,7 +149555,7 @@ function readRecordedCwd(dir) {
|
|
|
149385
149555
|
for (const file2 of files.slice(0, MAX_FILES_PROBED)) {
|
|
149386
149556
|
let fd;
|
|
149387
149557
|
try {
|
|
149388
|
-
fd = (0, import_fs29.openSync)((0,
|
|
149558
|
+
fd = (0, import_fs29.openSync)((0, import_path30.join)(dir, file2), "r");
|
|
149389
149559
|
const buf = Buffer.alloc(HEAD_BYTES);
|
|
149390
149560
|
const bytes = (0, import_fs29.readSync)(fd, buf, 0, HEAD_BYTES, 0);
|
|
149391
149561
|
for (const line of buf.subarray(0, bytes).toString("utf8").split("\n")) {
|
|
@@ -149409,11 +149579,11 @@ function decodeProjectPath(dirName) {
|
|
|
149409
149579
|
function handleListProjects(url2, res) {
|
|
149410
149580
|
const limit = Math.max(1, parseInt(url2.searchParams.get("limit") ?? "50", 10) || 50);
|
|
149411
149581
|
const offset = Math.max(0, parseInt(url2.searchParams.get("offset") ?? "0", 10) || 0);
|
|
149412
|
-
const projectsDir = (0,
|
|
149582
|
+
const projectsDir = (0, import_path30.join)((0, import_os13.homedir)(), ".claude", "projects");
|
|
149413
149583
|
let entries;
|
|
149414
149584
|
try {
|
|
149415
149585
|
entries = (0, import_fs29.readdirSync)(projectsDir).map((dirName) => {
|
|
149416
|
-
const fullPath = (0,
|
|
149586
|
+
const fullPath = (0, import_path30.join)(projectsDir, dirName);
|
|
149417
149587
|
let mtime = 0;
|
|
149418
149588
|
try {
|
|
149419
149589
|
mtime = (0, import_fs29.statSync)(fullPath).mtimeMs;
|
|
@@ -149428,7 +149598,7 @@ function handleListProjects(url2, res) {
|
|
|
149428
149598
|
}
|
|
149429
149599
|
const total = entries.length;
|
|
149430
149600
|
const page = entries.slice(offset, offset + limit).map(({ dirName }) => {
|
|
149431
|
-
const path2 = readRecordedCwd((0,
|
|
149601
|
+
const path2 = readRecordedCwd((0, import_path30.join)(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
|
|
149432
149602
|
const name = path2.split(/[\\/]/).filter(Boolean).pop() ?? dirName;
|
|
149433
149603
|
return { name, path: path2, dirName };
|
|
149434
149604
|
});
|
|
@@ -149438,6 +149608,10 @@ function handleListProjects(url2, res) {
|
|
|
149438
149608
|
|
|
149439
149609
|
// src/server-wiring.ts
|
|
149440
149610
|
init_logger();
|
|
149611
|
+
init_capabilities();
|
|
149612
|
+
function wsAllows(principal, required2) {
|
|
149613
|
+
return principal === null || hasCapability(principal, required2);
|
|
149614
|
+
}
|
|
149441
149615
|
function createConversationWatcherEvents(deps) {
|
|
149442
149616
|
return {
|
|
149443
149617
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -149719,7 +149893,15 @@ function createApiDeps(deps) {
|
|
|
149719
149893
|
const alertMsg = deps.cacheMonitor()?.wsMessage();
|
|
149720
149894
|
if (alertMsg) deps.wsHub.unicast(ws2, alertMsg);
|
|
149721
149895
|
},
|
|
149722
|
-
handleWsMessage: async (ws2, raw2) => {
|
|
149896
|
+
handleWsMessage: async (ws2, raw2, principal) => {
|
|
149897
|
+
const deny = (type, required2) => {
|
|
149898
|
+
deps.log().warn(`[ws.capability_denied] ${type} requires ${required2}`, {
|
|
149899
|
+
event: "ws.capability_denied",
|
|
149900
|
+
type,
|
|
149901
|
+
required: required2,
|
|
149902
|
+
...principal?.deviceId ? { deviceId: principal.deviceId } : {}
|
|
149903
|
+
});
|
|
149904
|
+
};
|
|
149723
149905
|
try {
|
|
149724
149906
|
const msg = JSON.parse(String(raw2));
|
|
149725
149907
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
@@ -149729,6 +149911,10 @@ function createApiDeps(deps) {
|
|
|
149729
149911
|
deps.wsToClientId.set(ws2, msg.clientId);
|
|
149730
149912
|
}
|
|
149731
149913
|
if (msg.type === "subscribe_session" && typeof msg.sessionId === "string") {
|
|
149914
|
+
if (!wsAllows(principal, "history:read")) {
|
|
149915
|
+
deny(msg.type, "history:read");
|
|
149916
|
+
return;
|
|
149917
|
+
}
|
|
149732
149918
|
deps.addSessionSubscriber(msg.sessionId, ws2);
|
|
149733
149919
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
149734
149920
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
@@ -149777,6 +149963,10 @@ function createApiDeps(deps) {
|
|
|
149777
149963
|
}
|
|
149778
149964
|
}
|
|
149779
149965
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
149966
|
+
if (!wsAllows(principal, "session:control")) {
|
|
149967
|
+
deny(msg.type, "session:control");
|
|
149968
|
+
return;
|
|
149969
|
+
}
|
|
149780
149970
|
deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
|
|
149781
149971
|
}
|
|
149782
149972
|
} catch {
|
|
@@ -149804,11 +149994,11 @@ var import_fs33 = require("fs");
|
|
|
149804
149994
|
|
|
149805
149995
|
// src/services/cache-integrity/alertStore.ts
|
|
149806
149996
|
var import_fs31 = require("fs");
|
|
149807
|
-
var
|
|
149808
|
-
var
|
|
149997
|
+
var import_os14 = require("os");
|
|
149998
|
+
var import_path31 = require("path");
|
|
149809
149999
|
function alertStatePath() {
|
|
149810
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0,
|
|
149811
|
-
return (0,
|
|
150000
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path31.join)((0, import_os14.homedir)(), ".threadbase");
|
|
150001
|
+
return (0, import_path31.join)(dir, "cache-alert.json");
|
|
149812
150002
|
}
|
|
149813
150003
|
function loadAlertState() {
|
|
149814
150004
|
try {
|
|
@@ -149820,14 +150010,14 @@ function loadAlertState() {
|
|
|
149820
150010
|
}
|
|
149821
150011
|
function saveAlertState(state) {
|
|
149822
150012
|
const path2 = alertStatePath();
|
|
149823
|
-
(0, import_fs31.mkdirSync)((0,
|
|
150013
|
+
(0, import_fs31.mkdirSync)((0, import_path31.dirname)(path2), { recursive: true });
|
|
149824
150014
|
(0, import_fs31.writeFileSync)(path2, `${JSON.stringify(state, null, 2)}
|
|
149825
150015
|
`);
|
|
149826
150016
|
}
|
|
149827
150017
|
|
|
149828
150018
|
// src/services/cache-integrity/backup.ts
|
|
149829
150019
|
var import_fs32 = require("fs");
|
|
149830
|
-
var
|
|
150020
|
+
var import_path32 = require("path");
|
|
149831
150021
|
var DEFAULT_RETAIN = 3;
|
|
149832
150022
|
function retainCount() {
|
|
149833
150023
|
const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
|
|
@@ -149838,13 +150028,13 @@ function timestamp(d) {
|
|
|
149838
150028
|
return `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
|
|
149839
150029
|
}
|
|
149840
150030
|
async function backupCacheDb(db, cacheDir) {
|
|
149841
|
-
const backupsDir = (0,
|
|
150031
|
+
const backupsDir = (0, import_path32.join)(cacheDir, "backups");
|
|
149842
150032
|
(0, import_fs32.mkdirSync)(backupsDir, { recursive: true });
|
|
149843
|
-
const destPath = (0,
|
|
150033
|
+
const destPath = (0, import_path32.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
149844
150034
|
await db.backup(destPath);
|
|
149845
150035
|
const retain = retainCount();
|
|
149846
150036
|
const backups = (0, import_fs32.readdirSync)(backupsDir).filter((f2) => f2.startsWith("cache-") && f2.endsWith(".db")).map((f2) => {
|
|
149847
|
-
const full = (0,
|
|
150037
|
+
const full = (0, import_path32.join)(backupsDir, f2);
|
|
149848
150038
|
return { full, mtime: (0, import_fs32.statSync)(full).mtimeMs };
|
|
149849
150039
|
}).sort((a, b2) => b2.mtime - a.mtime);
|
|
149850
150040
|
for (const stale of backups.slice(retain)) {
|
|
@@ -151047,6 +151237,7 @@ function autoResumeSkipReason(row, opts) {
|
|
|
151047
151237
|
if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
|
|
151048
151238
|
if (!opts.projectExists(row.project_path)) return "project_missing";
|
|
151049
151239
|
if (resumeIdForRow(row) == null) return "resume_identity_missing";
|
|
151240
|
+
if (!opts.historyExists(row)) return "history_missing";
|
|
151050
151241
|
return null;
|
|
151051
151242
|
}
|
|
151052
151243
|
function planAutoResume(rows, opts) {
|
|
@@ -151308,7 +151499,34 @@ var SessionRegistryBoot = class {
|
|
|
151308
151499
|
/** Resume only the recent sessions the user explicitly allowed us to start at boot. */
|
|
151309
151500
|
async autoResumePreviousSessions(rows) {
|
|
151310
151501
|
if (!this.autoResumeOnBoot) return;
|
|
151311
|
-
const
|
|
151502
|
+
const now = Date.now();
|
|
151503
|
+
const baseOptions = { now, projectExists: import_fs36.existsSync, historyExists: () => true };
|
|
151504
|
+
const historyExists = /* @__PURE__ */ new Map();
|
|
151505
|
+
const preflights = /* @__PURE__ */ new Set();
|
|
151506
|
+
for (const row of rows) {
|
|
151507
|
+
if (autoResumeSkipReason(row, baseOptions) != null) {
|
|
151508
|
+
historyExists.set(row.session_id, false);
|
|
151509
|
+
continue;
|
|
151510
|
+
}
|
|
151511
|
+
while (preflights.size >= AUTO_RESUME_CONCURRENCY) {
|
|
151512
|
+
await Promise.race(preflights);
|
|
151513
|
+
}
|
|
151514
|
+
const preflight = (async () => {
|
|
151515
|
+
try {
|
|
151516
|
+
const target = await this.deps.resolveConversationTarget(row.session_id);
|
|
151517
|
+
historyExists.set(row.session_id, target.ok || target.reason !== "history_file_missing");
|
|
151518
|
+
} catch {
|
|
151519
|
+
historyExists.set(row.session_id, true);
|
|
151520
|
+
}
|
|
151521
|
+
})();
|
|
151522
|
+
preflights.add(preflight);
|
|
151523
|
+
void preflight.then(() => preflights.delete(preflight));
|
|
151524
|
+
}
|
|
151525
|
+
await Promise.all(preflights);
|
|
151526
|
+
const plan = planAutoResume(rows, {
|
|
151527
|
+
...baseOptions,
|
|
151528
|
+
historyExists: (row) => historyExists.get(row.session_id) ?? false
|
|
151529
|
+
});
|
|
151312
151530
|
const skippedBy = {};
|
|
151313
151531
|
for (const { row, reason } of plan.skipped) {
|
|
151314
151532
|
skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
|
|
@@ -151810,8 +152028,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
151810
152028
|
|
|
151811
152029
|
// src/session-watchers.ts
|
|
151812
152030
|
var import_fs37 = require("fs");
|
|
151813
|
-
var
|
|
151814
|
-
var
|
|
152031
|
+
var import_os15 = require("os");
|
|
152032
|
+
var import_path33 = require("path");
|
|
151815
152033
|
init_logger();
|
|
151816
152034
|
var SessionWatchers = class {
|
|
151817
152035
|
constructor(deps) {
|
|
@@ -151893,9 +152111,9 @@ var SessionWatchers = class {
|
|
|
151893
152111
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
151894
152112
|
watchForJsonl(sessionId, projectPath) {
|
|
151895
152113
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
151896
|
-
const projectsDir = (0,
|
|
152114
|
+
const projectsDir = (0, import_path33.join)((0, import_os15.homedir)(), ".claude", "projects", encoded);
|
|
151897
152115
|
const expectedFile = `${sessionId}.jsonl`;
|
|
151898
|
-
const filePath = (0,
|
|
152116
|
+
const filePath = (0, import_path33.join)(projectsDir, expectedFile);
|
|
151899
152117
|
const deadline = Date.now() + 12e4;
|
|
151900
152118
|
let watcher = null;
|
|
151901
152119
|
const cleanup = () => {
|
|
@@ -151917,10 +152135,10 @@ var SessionWatchers = class {
|
|
|
151917
152135
|
if (!resolvedFilePath && (0, import_fs37.existsSync)(projectsDir)) {
|
|
151918
152136
|
try {
|
|
151919
152137
|
const now = Date.now();
|
|
151920
|
-
const match2 = (0, import_fs37.readdirSync)(projectsDir).filter((f2) => f2.endsWith(".jsonl")).map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0,
|
|
151921
|
-
({ f: f2 }) => (0,
|
|
152138
|
+
const match2 = (0, import_fs37.readdirSync)(projectsDir).filter((f2) => f2.endsWith(".jsonl")).map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path33.join)(projectsDir, f2)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
152139
|
+
({ f: f2 }) => (0, import_path33.basename)(f2, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path33.join)(projectsDir, f2)) === sessionId
|
|
151922
152140
|
).sort((a, b2) => b2.mtime - a.mtime)[0];
|
|
151923
|
-
if (match2) resolvedFilePath = (0,
|
|
152141
|
+
if (match2) resolvedFilePath = (0, import_path33.join)(projectsDir, match2.f);
|
|
151924
152142
|
} catch {
|
|
151925
152143
|
}
|
|
151926
152144
|
}
|
|
@@ -151964,7 +152182,7 @@ var SessionWatchers = class {
|
|
|
151964
152182
|
watchForCodexRollout(sessionId, projectPath) {
|
|
151965
152183
|
const deadline = Date.now() + 12e4;
|
|
151966
152184
|
const now = /* @__PURE__ */ new Date();
|
|
151967
|
-
const dateDir = (0,
|
|
152185
|
+
const dateDir = (0, import_path33.join)(
|
|
151968
152186
|
String(now.getFullYear()),
|
|
151969
152187
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
151970
152188
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -152005,7 +152223,7 @@ var SessionWatchers = class {
|
|
|
152005
152223
|
this.deps.sessionStore.listManaged().filter((s3) => s3.id !== sessionId && s3.boundConversationId != null).map((s3) => s3.boundConversationId)
|
|
152006
152224
|
);
|
|
152007
152225
|
for (const root of this.deps.codexRoots) {
|
|
152008
|
-
const sessionsDir = (0,
|
|
152226
|
+
const sessionsDir = (0, import_path33.join)(root, dateDir);
|
|
152009
152227
|
if (!(0, import_fs37.existsSync)(sessionsDir)) continue;
|
|
152010
152228
|
let candidateFiles;
|
|
152011
152229
|
try {
|
|
@@ -152014,9 +152232,9 @@ var SessionWatchers = class {
|
|
|
152014
152232
|
continue;
|
|
152015
152233
|
}
|
|
152016
152234
|
const nowMs = Date.now();
|
|
152017
|
-
const recentCandidates = candidateFiles.map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0,
|
|
152235
|
+
const recentCandidates = candidateFiles.map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path33.join)(sessionsDir, f2)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b2) => b2.mtime - a.mtime);
|
|
152018
152236
|
for (const { f: f2 } of recentCandidates) {
|
|
152019
|
-
const candidatePath = (0,
|
|
152237
|
+
const candidatePath = (0, import_path33.join)(sessionsDir, f2);
|
|
152020
152238
|
const match2 = matchesProjectPath(candidatePath);
|
|
152021
152239
|
if (!match2) continue;
|
|
152022
152240
|
if (boundElsewhere.has(match2.id)) continue;
|
|
@@ -152420,7 +152638,7 @@ var StreamerServer = class {
|
|
|
152420
152638
|
this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
|
|
152421
152639
|
this.autoResumeOnBoot = config2.autoResumeOnBoot ?? false;
|
|
152422
152640
|
this.scanProfiles = config2.scanProfiles;
|
|
152423
|
-
this.codexRoots = config2.codexRoots ?? [(0,
|
|
152641
|
+
this.codexRoots = config2.codexRoots ?? [(0, import_path34.join)((0, import_os16.homedir)(), ".codex", "sessions")];
|
|
152424
152642
|
this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
152425
152643
|
this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
152426
152644
|
const flagResolution = resolveFeatureFlags({
|
|
@@ -152437,8 +152655,8 @@ var StreamerServer = class {
|
|
|
152437
152655
|
this.claudeFlagsPersistable = config2.claudeFlags === void 0;
|
|
152438
152656
|
this.claudeFlags = config2.claudeFlags ?? loadClaudeFlags();
|
|
152439
152657
|
this.claudeExtraArgs = config2.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
152440
|
-
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0,
|
|
152441
|
-
this.runtimeDbPath = config2.runtimeDbPath
|
|
152658
|
+
this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path34.join)((0, import_os16.homedir)(), ".threadbase", "cache");
|
|
152659
|
+
this.runtimeDbPath = resolveRuntimeDbPath(config2.runtimeDbPath);
|
|
152442
152660
|
this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
|
|
152443
152661
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
|
|
152444
152662
|
this.scannerManager = new ScannerManager({
|
|
@@ -152470,7 +152688,8 @@ var StreamerServer = class {
|
|
|
152470
152688
|
selfPtyEndedAt: this.selfPtyEndedAt,
|
|
152471
152689
|
resumeSession: (opts) => this.sessionHandlers.resumeSession(opts),
|
|
152472
152690
|
watchConversationFile: (sessionId, historyId) => this.sessionWatchers.watchConversationFile(sessionId, historyId),
|
|
152473
|
-
broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload())
|
|
152691
|
+
broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload()),
|
|
152692
|
+
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId)
|
|
152474
152693
|
});
|
|
152475
152694
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
152476
152695
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -152613,7 +152832,7 @@ var StreamerServer = class {
|
|
|
152613
152832
|
temporalClient,
|
|
152614
152833
|
taskQueue: agentConfig.temporal.taskQueue
|
|
152615
152834
|
});
|
|
152616
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
152835
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path34.join)((0, import_path34.dirname)(this.cacheDir), "conversations");
|
|
152617
152836
|
conversationWriter = createConversationWriter({
|
|
152618
152837
|
baseDir: conversationsBaseDir
|
|
152619
152838
|
});
|
|
@@ -152865,14 +153084,14 @@ var StreamerServer = class {
|
|
|
152865
153084
|
}
|
|
152866
153085
|
this.apnsClient = new ApnsClient(creds);
|
|
152867
153086
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
152868
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
152869
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0,
|
|
153087
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)();
|
|
153088
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os16.hostname)());
|
|
152870
153089
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
152871
153090
|
repo: pushRepo,
|
|
152872
153091
|
sender,
|
|
152873
153092
|
sessionStore: this.sessionStore,
|
|
152874
153093
|
serverId,
|
|
152875
|
-
serverLabel: (0,
|
|
153094
|
+
serverLabel: (0, import_os16.hostname)()
|
|
152876
153095
|
});
|
|
152877
153096
|
this.liveActivityRenewal.start();
|
|
152878
153097
|
this.log.info("Live Activity push enabled", {
|
|
@@ -152892,7 +153111,7 @@ var StreamerServer = class {
|
|
|
152892
153111
|
*/
|
|
152893
153112
|
initWaitingInputPush(pushRepo) {
|
|
152894
153113
|
const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
|
|
152895
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
153114
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)();
|
|
152896
153115
|
this.waitingInputNotifier = new WaitingInputNotifier(
|
|
152897
153116
|
sender,
|
|
152898
153117
|
serverId,
|
|
@@ -153029,7 +153248,7 @@ var StreamerServer = class {
|
|
|
153029
153248
|
let sessions = null;
|
|
153030
153249
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
153031
153250
|
const transport = await connectOrSpawnHost({
|
|
153032
|
-
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
153251
|
+
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)()
|
|
153033
153252
|
});
|
|
153034
153253
|
try {
|
|
153035
153254
|
sessions = await this.ptyManager.useRemoteRunner(transport);
|
|
@@ -153086,6 +153305,7 @@ var StreamerServer = class {
|
|
|
153086
153305
|
try {
|
|
153087
153306
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
153088
153307
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
153308
|
+
this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());
|
|
153089
153309
|
} catch (err) {
|
|
153090
153310
|
const message = err instanceof Error ? err.message : String(err);
|
|
153091
153311
|
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
@@ -153105,7 +153325,7 @@ var StreamerServer = class {
|
|
|
153105
153325
|
}
|
|
153106
153326
|
try {
|
|
153107
153327
|
this.cache = ConversationCache.open(
|
|
153108
|
-
(0,
|
|
153328
|
+
(0, import_path34.join)(this.cacheDir, "cache.db"),
|
|
153109
153329
|
this.tailSize,
|
|
153110
153330
|
void 0,
|
|
153111
153331
|
{
|
|
@@ -153135,18 +153355,39 @@ var StreamerServer = class {
|
|
|
153135
153355
|
if (copied > 0) {
|
|
153136
153356
|
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
153137
153357
|
copied,
|
|
153358
|
+
table: "managed_sessions",
|
|
153138
153359
|
event: "runtime.legacy_import"
|
|
153139
153360
|
});
|
|
153140
153361
|
}
|
|
153141
153362
|
} catch (err) {
|
|
153142
153363
|
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
153143
153364
|
event: "runtime.legacy_import_failed",
|
|
153365
|
+
table: "managed_sessions",
|
|
153366
|
+
err
|
|
153367
|
+
});
|
|
153368
|
+
}
|
|
153369
|
+
try {
|
|
153370
|
+
const result = this.runtimeStore?.importLegacyDevices(db);
|
|
153371
|
+
if (result && result.copied > 0) {
|
|
153372
|
+
this.log.info(
|
|
153373
|
+
`Moved ${result.copied} device row(s) from cache.db to runtime.db` + (result.purged ? "; removed the cache-side copy" : "; KEPT the cache-side copy (row count did not match after copy)"),
|
|
153374
|
+
{
|
|
153375
|
+
copied: result.copied,
|
|
153376
|
+
purged: result.purged,
|
|
153377
|
+
table: "devices",
|
|
153378
|
+
event: "runtime.legacy_import"
|
|
153379
|
+
}
|
|
153380
|
+
);
|
|
153381
|
+
}
|
|
153382
|
+
} catch (err) {
|
|
153383
|
+
this.log.warn("[registry] legacy devices move failed", {
|
|
153384
|
+
event: "runtime.legacy_import_failed",
|
|
153385
|
+
table: "devices",
|
|
153144
153386
|
err
|
|
153145
153387
|
});
|
|
153146
153388
|
}
|
|
153147
153389
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
153148
153390
|
this.pushRepo = new PushRepository(db);
|
|
153149
|
-
this.devicesRepo = new DevicesRepository(db);
|
|
153150
153391
|
this.initLiveActivityPush(this.pushRepo);
|
|
153151
153392
|
this.initWaitingInputPush(this.pushRepo);
|
|
153152
153393
|
this.cacheMonitor = new CacheIntegrityMonitor(
|
|
@@ -153472,7 +153713,7 @@ var StreamerServer = class {
|
|
|
153472
153713
|
nonce: sealed.nonce,
|
|
153473
153714
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
153474
153715
|
publicUrl: this.publicUrl,
|
|
153475
|
-
machineName: (0,
|
|
153716
|
+
machineName: (0, import_os16.hostname)(),
|
|
153476
153717
|
...device && {
|
|
153477
153718
|
deviceId: device.deviceId,
|
|
153478
153719
|
deviceToken: device.deviceToken,
|
|
@@ -153679,13 +153920,17 @@ var StreamerServer = class {
|
|
|
153679
153920
|
conv = await this.conversationHandlers.findConversationByUuid(boundId);
|
|
153680
153921
|
}
|
|
153681
153922
|
}
|
|
153923
|
+
const cachedConvMeta = this.cache?.getMetaById(historyId);
|
|
153924
|
+
const cachedPath = cachedConvMeta?.filePath ? toNativeFilePath(cachedConvMeta.filePath) : null;
|
|
153925
|
+
const cachedCodexPath = (registryProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) && cachedPath != null && (0, import_fs38.existsSync)(cachedPath) ? cachedPath : null;
|
|
153682
153926
|
const jsonlCwd = jsonlPath ? await this.conversationHandlers.readCwdFromJsonl(jsonlPath) : null;
|
|
153683
|
-
const projectPath = jsonlCwd ?? conv?.projectPath;
|
|
153927
|
+
const projectPath = jsonlCwd ?? conv?.projectPath ?? (cachedCodexPath ? cachedConvMeta?.projectPath : null);
|
|
153684
153928
|
if (!projectPath) {
|
|
153685
|
-
if (!conv && !jsonlPath)
|
|
153929
|
+
if (!conv && !jsonlPath && !cachedCodexPath) {
|
|
153930
|
+
return { ok: false, reason: "history_file_missing" };
|
|
153931
|
+
}
|
|
153686
153932
|
return { ok: false, reason: "no_project_path" };
|
|
153687
153933
|
}
|
|
153688
|
-
const cachedConvMeta = this.cache?.getMetaById(historyId);
|
|
153689
153934
|
const provider = coerceProviderForRunner(
|
|
153690
153935
|
conv?.provider ?? cachedConvMeta?.provider ?? registryProvider
|
|
153691
153936
|
);
|
|
@@ -153699,7 +153944,7 @@ var StreamerServer = class {
|
|
|
153699
153944
|
// conversation. Kept separate from `jsonlPath` deliberately: feeding it to
|
|
153700
153945
|
// conversationBusy() would newly arm the mtime heuristic for Codex, which
|
|
153701
153946
|
// is exactly the over-broad signal the report ruled out.
|
|
153702
|
-
historyPath: jsonlPath ?? conv?.filePath ?? null,
|
|
153947
|
+
historyPath: jsonlPath ?? conv?.filePath ?? cachedCodexPath ?? null,
|
|
153703
153948
|
conv,
|
|
153704
153949
|
projectPath,
|
|
153705
153950
|
provider
|
|
@@ -154535,7 +154780,7 @@ var import_node_stream2 = __toESM(require("stream"), 1);
|
|
|
154535
154780
|
var import_node_string_decoder = require("string_decoder");
|
|
154536
154781
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
154537
154782
|
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
154538
|
-
var
|
|
154783
|
+
var import_path35 = require("path");
|
|
154539
154784
|
var import_events4 = require("events");
|
|
154540
154785
|
var import_assert = __toESM(require("assert"), 1);
|
|
154541
154786
|
var import_buffer2 = require("buffer");
|
|
@@ -154545,9 +154790,9 @@ var import_node_path16 = require("path");
|
|
|
154545
154790
|
var import_node_path17 = require("path");
|
|
154546
154791
|
var import_fs40 = __toESM(require("fs"), 1);
|
|
154547
154792
|
var import_fs41 = __toESM(require("fs"), 1);
|
|
154548
|
-
var import_path35 = __toESM(require("path"), 1);
|
|
154549
|
-
var import_node_path18 = require("path");
|
|
154550
154793
|
var import_path36 = __toESM(require("path"), 1);
|
|
154794
|
+
var import_node_path18 = require("path");
|
|
154795
|
+
var import_path37 = __toESM(require("path"), 1);
|
|
154551
154796
|
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
154552
154797
|
var import_node_assert = __toESM(require("assert"), 1);
|
|
154553
154798
|
var import_node_crypto6 = require("crypto");
|
|
@@ -155919,11 +156164,11 @@ var vn = (s3) => {
|
|
|
155919
156164
|
var Qi = (s3, t) => {
|
|
155920
156165
|
let e = new Map(t.map((o) => [mt(o), true])), i = s3.filter, r = 100, n = (o, h = "", a = 0) => {
|
|
155921
156166
|
if (a >= r) return e.set(o, false), false;
|
|
155922
|
-
let l = h || (0,
|
|
156167
|
+
let l = h || (0, import_path35.parse)(o).root || ".", c;
|
|
155923
156168
|
if (o === l) c = false;
|
|
155924
156169
|
else {
|
|
155925
156170
|
let d = e.get(o);
|
|
155926
|
-
c = d !== void 0 ? d : n((0,
|
|
156171
|
+
c = d !== void 0 ? d : n((0, import_path35.dirname)(o), l, a + 1);
|
|
155927
156172
|
}
|
|
155928
156173
|
return e.set(o, c), c;
|
|
155929
156174
|
};
|
|
@@ -156043,7 +156288,7 @@ var de = class extends A {
|
|
|
156043
156288
|
let [o, h] = ce(this.path);
|
|
156044
156289
|
o && typeof h == "string" && (this.path = h, r = o);
|
|
156045
156290
|
}
|
|
156046
|
-
this.win32 = !!i.win32 || process.platform === "win32", this.win32 && (this.path = Qs(this.path.replaceAll(/\\/g, "/")), t = t.replaceAll(/\\/g, "/")), this.absolute = f(i.absolute ||
|
|
156291
|
+
this.win32 = !!i.win32 || process.platform === "win32", this.win32 && (this.path = Qs(this.path.replaceAll(/\\/g, "/")), t = t.replaceAll(/\\/g, "/")), this.absolute = f(i.absolute || import_path36.default.resolve(this.cwd, t)), this.path === "" && (this.path = "./"), r && this.warn("TAR_ENTRY_INFO", `stripping ${r} from absolute path`, { entry: this, path: r + this.path });
|
|
156047
156292
|
let n = this.statCache.get(this.absolute);
|
|
156048
156293
|
n ? this[si](n) : this[ss]();
|
|
156049
156294
|
}
|
|
@@ -156102,7 +156347,7 @@ var de = class extends A {
|
|
|
156102
156347
|
}
|
|
156103
156348
|
[sr](t) {
|
|
156104
156349
|
if (!this.stat) throw new Error("cannot create link entry without stat");
|
|
156105
|
-
this.type = "Link", this.linkpath = f(
|
|
156350
|
+
this.type = "Link", this.linkpath = f(import_path36.default.relative(this.cwd, t)), this.stat.size = 0, this[fe](), this.end();
|
|
156106
156351
|
}
|
|
156107
156352
|
[er]() {
|
|
156108
156353
|
if (!this.stat) throw new Error("cannot create file entry without stat");
|
|
@@ -156516,7 +156761,7 @@ var wt = class extends A {
|
|
|
156516
156761
|
return typeof t == "string" ? this[ci](t) : this[or](t), this.flowing;
|
|
156517
156762
|
}
|
|
156518
156763
|
[or](t) {
|
|
156519
|
-
let e = f(
|
|
156764
|
+
let e = f(import_path37.default.resolve(this.cwd, t.path));
|
|
156520
156765
|
if (!this.filter(t.path, t)) t.resume();
|
|
156521
156766
|
else {
|
|
156522
156767
|
let i = new pi(t.path, e);
|
|
@@ -156525,7 +156770,7 @@ var wt = class extends A {
|
|
|
156525
156770
|
this[Ft]();
|
|
156526
156771
|
}
|
|
156527
156772
|
[ci](t) {
|
|
156528
|
-
let e = f(
|
|
156773
|
+
let e = f(import_path37.default.resolve(this.cwd, t));
|
|
156529
156774
|
this[W2].push(new pi(t, e)), this[Ft]();
|
|
156530
156775
|
}
|
|
156531
156776
|
[ds](t) {
|
|
@@ -158180,8 +158425,8 @@ program2.command("cache").description("Manage the local SQLite conversation cach
|
|
|
158180
158425
|
`${process.env.HOME}/.threadbase/cache`
|
|
158181
158426
|
).action((opts) => {
|
|
158182
158427
|
const { rmSync: rmSync6, existsSync: existsSync21 } = require("fs");
|
|
158183
|
-
const { join:
|
|
158184
|
-
const dbPath =
|
|
158428
|
+
const { join: join35 } = require("path");
|
|
158429
|
+
const dbPath = join35(opts.cacheDir, "cache.db");
|
|
158185
158430
|
for (const suffix of ["", "-shm", "-wal"]) {
|
|
158186
158431
|
const f2 = dbPath + suffix;
|
|
158187
158432
|
if (existsSync21(f2)) {
|
|
@@ -158192,6 +158437,75 @@ program2.command("cache").description("Manage the local SQLite conversation cach
|
|
|
158192
158437
|
log14.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
|
|
158193
158438
|
})
|
|
158194
158439
|
);
|
|
158440
|
+
program2.command("devices").description("List, revoke and erase paired devices").addCommand(
|
|
158441
|
+
new Command("list").description("Show every paired device, including revoked ones").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (opts) => {
|
|
158442
|
+
const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
|
|
158443
|
+
const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
|
|
158444
|
+
const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
|
|
158445
|
+
const devices = new DevicesRepository2(store.getDatabase()).list();
|
|
158446
|
+
store.close();
|
|
158447
|
+
if (devices.length === 0) {
|
|
158448
|
+
log14.info("No paired devices.", void 0, "console");
|
|
158449
|
+
return;
|
|
158450
|
+
}
|
|
158451
|
+
for (const d of devices) {
|
|
158452
|
+
const state = d.revokedAt ? "revoked" : "active";
|
|
158453
|
+
const seen = d.lastSeenAt ? new Date(d.lastSeenAt).toISOString() : "never";
|
|
158454
|
+
log14.info(
|
|
158455
|
+
`${d.deviceId} ${state.padEnd(7)} ${d.name ?? "(unnamed)"} last seen ${seen} [${d.capabilities.join(", ")}]`,
|
|
158456
|
+
void 0,
|
|
158457
|
+
"console"
|
|
158458
|
+
);
|
|
158459
|
+
}
|
|
158460
|
+
})
|
|
158461
|
+
).addCommand(
|
|
158462
|
+
new Command("revoke").argument("<deviceId>").description("Refuse this device's token, keeping its record for the audit trail").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (deviceId, opts) => {
|
|
158463
|
+
const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
|
|
158464
|
+
const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
|
|
158465
|
+
const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
|
|
158466
|
+
const ok = new DevicesRepository2(store.getDatabase()).revoke(deviceId);
|
|
158467
|
+
store.close();
|
|
158468
|
+
log14.info(ok ? `Revoked ${deviceId}.` : `No such device: ${deviceId}`, void 0, "console");
|
|
158469
|
+
if (!ok) process.exitCode = 1;
|
|
158470
|
+
})
|
|
158471
|
+
).addCommand(
|
|
158472
|
+
new Command("delete").argument("[deviceId]").description("Erase a device record. With --revoked, erase every revoked device instead").option("--revoked", "Erase all revoked devices rather than one by id").option("--force", "Erase an ACTIVE device (revoking first is the safe order)").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (deviceId, opts) => {
|
|
158473
|
+
const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
|
|
158474
|
+
const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
|
|
158475
|
+
const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
|
|
158476
|
+
const repo = new DevicesRepository2(store.getDatabase());
|
|
158477
|
+
if (opts.revoked) {
|
|
158478
|
+
const n = repo.deleteRevoked();
|
|
158479
|
+
store.close();
|
|
158480
|
+
log14.info(`Erased ${n} revoked device record(s).`, void 0, "console");
|
|
158481
|
+
return;
|
|
158482
|
+
}
|
|
158483
|
+
if (!deviceId) {
|
|
158484
|
+
store.close();
|
|
158485
|
+
log14.error("Pass a device id, or --revoked to erase all revoked devices.");
|
|
158486
|
+
process.exitCode = 1;
|
|
158487
|
+
return;
|
|
158488
|
+
}
|
|
158489
|
+
const existing = repo.get(deviceId);
|
|
158490
|
+
if (!existing) {
|
|
158491
|
+
store.close();
|
|
158492
|
+
log14.info(`No such device: ${deviceId}`, void 0, "console");
|
|
158493
|
+
process.exitCode = 1;
|
|
158494
|
+
return;
|
|
158495
|
+
}
|
|
158496
|
+
if (existing.revoked_at == null && !opts.force) {
|
|
158497
|
+
store.close();
|
|
158498
|
+
log14.error(
|
|
158499
|
+
`${deviceId} is still active. Run 'devices revoke ${deviceId}' first, or pass --force.`
|
|
158500
|
+
);
|
|
158501
|
+
process.exitCode = 1;
|
|
158502
|
+
return;
|
|
158503
|
+
}
|
|
158504
|
+
repo.delete(deviceId);
|
|
158505
|
+
store.close();
|
|
158506
|
+
log14.info(`Erased ${deviceId}.`, void 0, "console");
|
|
158507
|
+
})
|
|
158508
|
+
);
|
|
158195
158509
|
program2.command("pair").description("Print a pairing QR code (server must already be running)").option("-p, --port <number>", "Port the server is listening on", "8766").action(async (opts) => {
|
|
158196
158510
|
const port = Number.parseInt(opts.port, 10);
|
|
158197
158511
|
const apiKey = loadOrCreateApiKey();
|