@threadbase-sh/streamer 1.52.5 → 1.53.0
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 +754 -467
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +228 -62
- 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 +225 -59
- 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/index.cjs
CHANGED
|
@@ -607,7 +607,8 @@ function loadOrCreateApiKey() {
|
|
|
607
607
|
const key = generateApiKey();
|
|
608
608
|
(0, import_fs.mkdirSync)(configDir(), { recursive: true });
|
|
609
609
|
(0, import_fs.writeFileSync)(configFile(), `api_key: ${key}
|
|
610
|
-
`, "utf-8");
|
|
610
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
611
|
+
(0, import_fs.chmodSync)(configFile(), 384);
|
|
611
612
|
return key;
|
|
612
613
|
}
|
|
613
614
|
function loadBrowseRoot() {
|
|
@@ -3916,8 +3917,8 @@ var import_events = require("events");
|
|
|
3916
3917
|
var import_fs27 = require("fs");
|
|
3917
3918
|
var import_promises7 = require("fs/promises");
|
|
3918
3919
|
var import_http = require("http");
|
|
3919
|
-
var
|
|
3920
|
-
var
|
|
3920
|
+
var import_os13 = require("os");
|
|
3921
|
+
var import_path23 = require("path");
|
|
3921
3922
|
|
|
3922
3923
|
// src/api/app.ts
|
|
3923
3924
|
var import_hono18 = require("hono");
|
|
@@ -4053,6 +4054,8 @@ var DevicesRepository = class {
|
|
|
4053
4054
|
listStmt;
|
|
4054
4055
|
revokeStmt;
|
|
4055
4056
|
touchStmt;
|
|
4057
|
+
deleteStmt;
|
|
4058
|
+
deleteRevokedStmt;
|
|
4056
4059
|
constructor(db) {
|
|
4057
4060
|
this.insertStmt = db.prepare(`
|
|
4058
4061
|
INSERT INTO devices (
|
|
@@ -4066,6 +4069,8 @@ var DevicesRepository = class {
|
|
|
4066
4069
|
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
4067
4070
|
this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
|
|
4068
4071
|
this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
|
|
4072
|
+
this.deleteStmt = db.prepare("DELETE FROM devices WHERE device_id = ?");
|
|
4073
|
+
this.deleteRevokedStmt = db.prepare("DELETE FROM devices WHERE revoked_at IS NOT NULL");
|
|
4069
4074
|
}
|
|
4070
4075
|
/**
|
|
4071
4076
|
* Record a newly paired device and mint its token.
|
|
@@ -4112,6 +4117,33 @@ var DevicesRepository = class {
|
|
|
4112
4117
|
revoke(deviceId, now = Date.now()) {
|
|
4113
4118
|
return this.revokeStmt.run(now, deviceId).changes > 0;
|
|
4114
4119
|
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Erase one device's record outright.
|
|
4122
|
+
*
|
|
4123
|
+
* Deliberately separate from `revoke`, which is a soft delete that keeps the
|
|
4124
|
+
* row so `list()` can show what happened. That audit trail is the right
|
|
4125
|
+
* default — but it meant a `devices` row, including the user-supplied `name`
|
|
4126
|
+
* ("Ronen's iPhone"), had no removal path at all once the registry moved to
|
|
4127
|
+
* runtime.db, which no command deletes. This is that path.
|
|
4128
|
+
*
|
|
4129
|
+
* Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
|
|
4130
|
+
* device whose token is still on a phone somewhere stops being *known* rather
|
|
4131
|
+
* than being *refused*. Revoke first, delete second, is the safe order, and
|
|
4132
|
+
* `deleteRevoked()` exists so that is the easy thing to do.
|
|
4133
|
+
*/
|
|
4134
|
+
delete(deviceId) {
|
|
4135
|
+
return this.deleteStmt.run(deviceId).changes > 0;
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Erase every already-revoked device. The bulk companion to `delete`, and the
|
|
4139
|
+
* one that is safe by construction: a revoked device is already refused, so
|
|
4140
|
+
* removing its row cannot restore access to anything.
|
|
4141
|
+
*
|
|
4142
|
+
* Returns the number of rows removed.
|
|
4143
|
+
*/
|
|
4144
|
+
deleteRevoked() {
|
|
4145
|
+
return this.deleteRevokedStmt.run().changes;
|
|
4146
|
+
}
|
|
4115
4147
|
touch(deviceId, now = Date.now()) {
|
|
4116
4148
|
this.touchStmt.run(now, deviceId);
|
|
4117
4149
|
}
|
|
@@ -4638,6 +4670,34 @@ var createDeviceRoutes = (deps) => {
|
|
|
4638
4670
|
repo.revoke(id);
|
|
4639
4671
|
return c.json({ ok: true, alreadyRevoked: false });
|
|
4640
4672
|
});
|
|
4673
|
+
app.delete("/:id", (c) => {
|
|
4674
|
+
const repo = deps.devicesRepo();
|
|
4675
|
+
if (!repo) {
|
|
4676
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4677
|
+
}
|
|
4678
|
+
const id = c.req.param("id");
|
|
4679
|
+
const existing = repo.get(id);
|
|
4680
|
+
if (!existing) return c.json({ ok: true, alreadyDeleted: true });
|
|
4681
|
+
const force = c.req.query("force") === "1" || c.req.query("force") === "true";
|
|
4682
|
+
if (existing.revoked_at == null && !force) {
|
|
4683
|
+
return c.json(
|
|
4684
|
+
{
|
|
4685
|
+
error: "Revoke the device before deleting it, or pass ?force=1",
|
|
4686
|
+
code: "DEVICE_ACTIVE"
|
|
4687
|
+
},
|
|
4688
|
+
409
|
|
4689
|
+
);
|
|
4690
|
+
}
|
|
4691
|
+
repo.delete(id);
|
|
4692
|
+
return c.json({ ok: true, alreadyDeleted: false });
|
|
4693
|
+
});
|
|
4694
|
+
app.delete("/", (c) => {
|
|
4695
|
+
const repo = deps.devicesRepo();
|
|
4696
|
+
if (!repo) {
|
|
4697
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4698
|
+
}
|
|
4699
|
+
return c.json({ ok: true, deleted: repo.deleteRevoked() });
|
|
4700
|
+
});
|
|
4641
4701
|
return app;
|
|
4642
4702
|
};
|
|
4643
4703
|
|
|
@@ -5554,6 +5614,13 @@ var createMiscRoutes = (deps) => {
|
|
|
5554
5614
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5555
5615
|
// Hub's grouped views need before they can draw a tree.
|
|
5556
5616
|
projectSummary: true,
|
|
5617
|
+
// The paired-device registry lives in runtime.db, so it survives
|
|
5618
|
+
// `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.
|
|
5619
|
+
// A client may only prefer its scoped device token over the shared API
|
|
5620
|
+
// key when this is true: on an older server the registry is inside
|
|
5621
|
+
// cache.db, where a documented troubleshooting step deletes it and every
|
|
5622
|
+
// device token with it. Absent means "old server, assume not durable".
|
|
5623
|
+
devicesDurable: true,
|
|
5557
5624
|
// Delivery capability, not endpoint support: whether this server can
|
|
5558
5625
|
// actually send a push, so mobile can hide an affordance instead of
|
|
5559
5626
|
// registering tokens nothing will ever send to. Absent on older servers,
|
|
@@ -5964,7 +6031,13 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5964
6031
|
const app = new import_hono17.Hono();
|
|
5965
6032
|
app.get(
|
|
5966
6033
|
"/ws",
|
|
5967
|
-
|
|
6034
|
+
// The principal is read here, at the upgrade, and captured for the life of
|
|
6035
|
+
// the socket. authMiddleware sets it because /ws is classified
|
|
6036
|
+
// `history:read`, but it only ever reaches the HTTP request — without
|
|
6037
|
+
// capturing it the socket has no principal at all, so every frame after
|
|
6038
|
+
// the upgrade is unauthorized-by-omission.
|
|
6039
|
+
upgradeWebSocket((c) => {
|
|
6040
|
+
const principal = c.get("principal") ?? null;
|
|
5968
6041
|
let openWs = null;
|
|
5969
6042
|
return {
|
|
5970
6043
|
onOpen(_evt, ws) {
|
|
@@ -5974,7 +6047,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5974
6047
|
deps.handleWsOpen(raw);
|
|
5975
6048
|
},
|
|
5976
6049
|
onMessage(evt, _ws) {
|
|
5977
|
-
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
6050
|
+
if (openWs) deps.handleWsMessage(openWs, evt.data, principal);
|
|
5978
6051
|
},
|
|
5979
6052
|
onClose(_evt, _ws) {
|
|
5980
6053
|
if (openWs) deps.handleWsClose(openWs);
|
|
@@ -10696,6 +10769,11 @@ var SessionsRepository = class {
|
|
|
10696
10769
|
|
|
10697
10770
|
// src/db/runtime-store.ts
|
|
10698
10771
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
10772
|
+
var import_os7 = require("os");
|
|
10773
|
+
var import_path16 = require("path");
|
|
10774
|
+
function resolveRuntimeDbPath(override) {
|
|
10775
|
+
return override ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path16.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path16.join)((0, import_os7.homedir)(), ".threadbase"), "runtime.db");
|
|
10776
|
+
}
|
|
10699
10777
|
var RuntimeStore = class _RuntimeStore {
|
|
10700
10778
|
constructor(db) {
|
|
10701
10779
|
this.db = db;
|
|
@@ -10713,23 +10791,64 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
10713
10791
|
/**
|
|
10714
10792
|
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
10715
10793
|
*
|
|
10716
|
-
* Non-destructive by design: the source table is left in place so an older
|
|
10717
|
-
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
10718
|
-
* only when this file's table is empty, so a second boot is a no-op rather
|
|
10719
|
-
* than a re-copy that would resurrect rows deleted since.
|
|
10720
|
-
*
|
|
10721
10794
|
* Returns the number of rows copied.
|
|
10722
10795
|
*/
|
|
10723
10796
|
importLegacyManagedSessions(source) {
|
|
10724
|
-
|
|
10797
|
+
return this.importLegacyTable(source, "managed_sessions");
|
|
10798
|
+
}
|
|
10799
|
+
/**
|
|
10800
|
+
* One-time move of `devices` rows out of `cache.db`, where the registry used
|
|
10801
|
+
* to live (migration `011_create_devices.sql`).
|
|
10802
|
+
*
|
|
10803
|
+
* Losing this table invalidates every device token ever issued, and cache.db
|
|
10804
|
+
* is the file `tb-streamer cache clear` deletes and the integrity monitor
|
|
10805
|
+
* rebuilds — see `runtime-migrations/003_create_devices.sql`.
|
|
10806
|
+
*
|
|
10807
|
+
* Unlike `managed_sessions`, this one MOVES rather than copies: the source
|
|
10808
|
+
* rows are deleted once the copy is verified. A `devices` row carries a
|
|
10809
|
+
* user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
|
|
10810
|
+
* on disk indefinitely — in the one file the user is told to delete when
|
|
10811
|
+
* something goes wrong — is more retained personal data than the rollback
|
|
10812
|
+
* path is worth. Recovering from a rollback is re-scanning a pairing QR.
|
|
10813
|
+
*
|
|
10814
|
+
* The delete is conditional on the copy being complete: `INSERT OR IGNORE`
|
|
10815
|
+
* can silently skip a row, so the destination count must match what was read
|
|
10816
|
+
* before anything is removed. A mismatch keeps the source and reports
|
|
10817
|
+
* `purged: false` rather than throwing — the import itself still succeeded,
|
|
10818
|
+
* and keeping data is the safe direction to fail in.
|
|
10819
|
+
*/
|
|
10820
|
+
importLegacyDevices(source) {
|
|
10821
|
+
const copied = this.importLegacyTable(source, "devices");
|
|
10822
|
+
if (copied === 0) return { copied: 0, purged: false };
|
|
10823
|
+
const landed = this.db.prepare("SELECT COUNT(*) AS n FROM devices").get().n;
|
|
10824
|
+
if (landed !== copied) return { copied, purged: false };
|
|
10825
|
+
source.prepare("DELETE FROM devices").run();
|
|
10826
|
+
return { copied, purged: true };
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* Copy a whole table out of a pre-split `cache.db` into this file.
|
|
10830
|
+
*
|
|
10831
|
+
* The copy itself is non-destructive — the source is left in place, so an
|
|
10832
|
+
* older streamer rolled back onto the same machine still finds its data.
|
|
10833
|
+
* `importLegacyDevices` deletes the source afterwards for its own reasons;
|
|
10834
|
+
* `managed_sessions` does not. Runs only when this file's table is empty, so
|
|
10835
|
+
* a second boot is a no-op rather than a re-copy that would resurrect rows
|
|
10836
|
+
* deleted since.
|
|
10837
|
+
*
|
|
10838
|
+
* The table name is interpolated into SQL, so it is typed as a closed union
|
|
10839
|
+
* rather than `string` — the set of tables that can ever be lifted is known
|
|
10840
|
+
* at compile time, and that is what keeps a caller from making this a hole.
|
|
10841
|
+
*/
|
|
10842
|
+
importLegacyTable(source, table) {
|
|
10843
|
+
const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
10725
10844
|
if (existing.n > 0) return 0;
|
|
10726
|
-
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name =
|
|
10845
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
10727
10846
|
if (!hasTable) return 0;
|
|
10728
|
-
const rows = source.prepare(
|
|
10847
|
+
const rows = source.prepare(`SELECT * FROM ${table}`).all();
|
|
10729
10848
|
if (rows.length === 0) return 0;
|
|
10730
10849
|
const columns = Object.keys(rows[0]);
|
|
10731
10850
|
const insert = this.db.prepare(
|
|
10732
|
-
`INSERT OR IGNORE INTO
|
|
10851
|
+
`INSERT OR IGNORE INTO ${table} (${columns.join(", ")})
|
|
10733
10852
|
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
10734
10853
|
);
|
|
10735
10854
|
this.db.transaction((batch) => {
|
|
@@ -11051,8 +11170,8 @@ function spawnDetachedHost(socketPath, entryPoint) {
|
|
|
11051
11170
|
// src/scanner-manager.ts
|
|
11052
11171
|
var import_scanner4 = require("@threadbase-sh/scanner");
|
|
11053
11172
|
var import_fs17 = require("fs");
|
|
11054
|
-
var
|
|
11055
|
-
var
|
|
11173
|
+
var import_os9 = require("os");
|
|
11174
|
+
var import_path18 = require("path");
|
|
11056
11175
|
|
|
11057
11176
|
// src/services/cache/cacheMetadata.ts
|
|
11058
11177
|
function getCacheMetadata(repo, key) {
|
|
@@ -11152,9 +11271,9 @@ function refreshConversationCache(deps) {
|
|
|
11152
11271
|
|
|
11153
11272
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
11154
11273
|
var import_fs16 = require("fs");
|
|
11155
|
-
var
|
|
11156
|
-
var
|
|
11157
|
-
var DEFAULT_PROJECTS_DIR = (0,
|
|
11274
|
+
var import_os8 = require("os");
|
|
11275
|
+
var import_path17 = require("path");
|
|
11276
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects");
|
|
11158
11277
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
11159
11278
|
let maxMs;
|
|
11160
11279
|
try {
|
|
@@ -11166,7 +11285,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
11166
11285
|
for (const ent of (0, import_fs16.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
11167
11286
|
if (!ent.isDirectory()) continue;
|
|
11168
11287
|
try {
|
|
11169
|
-
const childMs = (0, import_fs16.statSync)((0,
|
|
11288
|
+
const childMs = (0, import_fs16.statSync)((0, import_path17.join)(projectsDir, ent.name)).mtimeMs;
|
|
11170
11289
|
if (childMs > maxMs) maxMs = childMs;
|
|
11171
11290
|
} catch {
|
|
11172
11291
|
}
|
|
@@ -11338,9 +11457,9 @@ var ScannerManager = class {
|
|
|
11338
11457
|
projectsDirs() {
|
|
11339
11458
|
const profiles = this.deps.scanProfiles;
|
|
11340
11459
|
if (profiles && profiles.length > 0) {
|
|
11341
|
-
return profiles.filter((p) => p.enabled).map((p) => (0,
|
|
11460
|
+
return profiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
11342
11461
|
}
|
|
11343
|
-
return [(0,
|
|
11462
|
+
return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
|
|
11344
11463
|
}
|
|
11345
11464
|
// ─── staleness ────────────────────────────────────────────────────
|
|
11346
11465
|
// Drain the stale set and disarm the flag together. The caller owns the
|
|
@@ -11621,8 +11740,8 @@ var import_fs19 = require("fs");
|
|
|
11621
11740
|
|
|
11622
11741
|
// src/handlers/handleListProjects.ts
|
|
11623
11742
|
var import_fs18 = require("fs");
|
|
11624
|
-
var
|
|
11625
|
-
var
|
|
11743
|
+
var import_os10 = require("os");
|
|
11744
|
+
var import_path19 = require("path");
|
|
11626
11745
|
var HEAD_BYTES = 64 * 1024;
|
|
11627
11746
|
var MAX_FILES_PROBED = 3;
|
|
11628
11747
|
function readRecordedCwd(dir) {
|
|
@@ -11635,7 +11754,7 @@ function readRecordedCwd(dir) {
|
|
|
11635
11754
|
for (const file of files.slice(0, MAX_FILES_PROBED)) {
|
|
11636
11755
|
let fd;
|
|
11637
11756
|
try {
|
|
11638
|
-
fd = (0, import_fs18.openSync)((0,
|
|
11757
|
+
fd = (0, import_fs18.openSync)((0, import_path19.join)(dir, file), "r");
|
|
11639
11758
|
const buf = Buffer.alloc(HEAD_BYTES);
|
|
11640
11759
|
const bytes = (0, import_fs18.readSync)(fd, buf, 0, HEAD_BYTES, 0);
|
|
11641
11760
|
for (const line of buf.subarray(0, bytes).toString("utf8").split("\n")) {
|
|
@@ -11659,11 +11778,11 @@ function decodeProjectPath(dirName) {
|
|
|
11659
11778
|
function handleListProjects(url, res) {
|
|
11660
11779
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
11661
11780
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
11662
|
-
const projectsDir = (0,
|
|
11781
|
+
const projectsDir = (0, import_path19.join)((0, import_os10.homedir)(), ".claude", "projects");
|
|
11663
11782
|
let entries;
|
|
11664
11783
|
try {
|
|
11665
11784
|
entries = (0, import_fs18.readdirSync)(projectsDir).map((dirName) => {
|
|
11666
|
-
const fullPath = (0,
|
|
11785
|
+
const fullPath = (0, import_path19.join)(projectsDir, dirName);
|
|
11667
11786
|
let mtime = 0;
|
|
11668
11787
|
try {
|
|
11669
11788
|
mtime = (0, import_fs18.statSync)(fullPath).mtimeMs;
|
|
@@ -11678,7 +11797,7 @@ function handleListProjects(url, res) {
|
|
|
11678
11797
|
}
|
|
11679
11798
|
const total = entries.length;
|
|
11680
11799
|
const page = entries.slice(offset, offset + limit).map(({ dirName }) => {
|
|
11681
|
-
const path = readRecordedCwd((0,
|
|
11800
|
+
const path = readRecordedCwd((0, import_path19.join)(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
|
|
11682
11801
|
const name = path.split(/[\\/]/).filter(Boolean).pop() ?? dirName;
|
|
11683
11802
|
return { name, path, dirName };
|
|
11684
11803
|
});
|
|
@@ -11687,6 +11806,9 @@ function handleListProjects(url, res) {
|
|
|
11687
11806
|
}
|
|
11688
11807
|
|
|
11689
11808
|
// src/server-wiring.ts
|
|
11809
|
+
function wsAllows(principal, required) {
|
|
11810
|
+
return principal === null || hasCapability(principal, required);
|
|
11811
|
+
}
|
|
11690
11812
|
function createConversationWatcherEvents(deps) {
|
|
11691
11813
|
return {
|
|
11692
11814
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -11968,7 +12090,15 @@ function createApiDeps(deps) {
|
|
|
11968
12090
|
const alertMsg = deps.cacheMonitor()?.wsMessage();
|
|
11969
12091
|
if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
|
|
11970
12092
|
},
|
|
11971
|
-
handleWsMessage: async (ws, raw) => {
|
|
12093
|
+
handleWsMessage: async (ws, raw, principal) => {
|
|
12094
|
+
const deny = (type, required) => {
|
|
12095
|
+
deps.log().warn(`[ws.capability_denied] ${type} requires ${required}`, {
|
|
12096
|
+
event: "ws.capability_denied",
|
|
12097
|
+
type,
|
|
12098
|
+
required,
|
|
12099
|
+
...principal?.deviceId ? { deviceId: principal.deviceId } : {}
|
|
12100
|
+
});
|
|
12101
|
+
};
|
|
11972
12102
|
try {
|
|
11973
12103
|
const msg = JSON.parse(String(raw));
|
|
11974
12104
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
@@ -11978,6 +12108,10 @@ function createApiDeps(deps) {
|
|
|
11978
12108
|
deps.wsToClientId.set(ws, msg.clientId);
|
|
11979
12109
|
}
|
|
11980
12110
|
if (msg.type === "subscribe_session" && typeof msg.sessionId === "string") {
|
|
12111
|
+
if (!wsAllows(principal, "history:read")) {
|
|
12112
|
+
deny(msg.type, "history:read");
|
|
12113
|
+
return;
|
|
12114
|
+
}
|
|
11981
12115
|
deps.addSessionSubscriber(msg.sessionId, ws);
|
|
11982
12116
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
11983
12117
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
@@ -12026,6 +12160,10 @@ function createApiDeps(deps) {
|
|
|
12026
12160
|
}
|
|
12027
12161
|
}
|
|
12028
12162
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
12163
|
+
if (!wsAllows(principal, "session:control")) {
|
|
12164
|
+
deny(msg.type, "session:control");
|
|
12165
|
+
return;
|
|
12166
|
+
}
|
|
12029
12167
|
deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
|
|
12030
12168
|
}
|
|
12031
12169
|
} catch {
|
|
@@ -12053,11 +12191,11 @@ var import_fs22 = require("fs");
|
|
|
12053
12191
|
|
|
12054
12192
|
// src/services/cache-integrity/alertStore.ts
|
|
12055
12193
|
var import_fs20 = require("fs");
|
|
12056
|
-
var
|
|
12057
|
-
var
|
|
12194
|
+
var import_os11 = require("os");
|
|
12195
|
+
var import_path20 = require("path");
|
|
12058
12196
|
function alertStatePath() {
|
|
12059
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0,
|
|
12060
|
-
return (0,
|
|
12197
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path20.join)((0, import_os11.homedir)(), ".threadbase");
|
|
12198
|
+
return (0, import_path20.join)(dir, "cache-alert.json");
|
|
12061
12199
|
}
|
|
12062
12200
|
function loadAlertState() {
|
|
12063
12201
|
try {
|
|
@@ -12069,14 +12207,14 @@ function loadAlertState() {
|
|
|
12069
12207
|
}
|
|
12070
12208
|
function saveAlertState(state) {
|
|
12071
12209
|
const path = alertStatePath();
|
|
12072
|
-
(0, import_fs20.mkdirSync)((0,
|
|
12210
|
+
(0, import_fs20.mkdirSync)((0, import_path20.dirname)(path), { recursive: true });
|
|
12073
12211
|
(0, import_fs20.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
|
|
12074
12212
|
`);
|
|
12075
12213
|
}
|
|
12076
12214
|
|
|
12077
12215
|
// src/services/cache-integrity/backup.ts
|
|
12078
12216
|
var import_fs21 = require("fs");
|
|
12079
|
-
var
|
|
12217
|
+
var import_path21 = require("path");
|
|
12080
12218
|
var DEFAULT_RETAIN = 3;
|
|
12081
12219
|
function retainCount() {
|
|
12082
12220
|
const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
|
|
@@ -12087,13 +12225,13 @@ function timestamp(d) {
|
|
|
12087
12225
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
12088
12226
|
}
|
|
12089
12227
|
async function backupCacheDb(db, cacheDir) {
|
|
12090
|
-
const backupsDir = (0,
|
|
12228
|
+
const backupsDir = (0, import_path21.join)(cacheDir, "backups");
|
|
12091
12229
|
(0, import_fs21.mkdirSync)(backupsDir, { recursive: true });
|
|
12092
|
-
const destPath = (0,
|
|
12230
|
+
const destPath = (0, import_path21.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
12093
12231
|
await db.backup(destPath);
|
|
12094
12232
|
const retain = retainCount();
|
|
12095
12233
|
const backups = (0, import_fs21.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
12096
|
-
const full = (0,
|
|
12234
|
+
const full = (0, import_path21.join)(backupsDir, f);
|
|
12097
12235
|
return { full, mtime: (0, import_fs21.statSync)(full).mtimeMs };
|
|
12098
12236
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
12099
12237
|
for (const stale of backups.slice(retain)) {
|
|
@@ -13907,9 +14045,15 @@ function getSortValue(s, key) {
|
|
|
13907
14045
|
case "projectName":
|
|
13908
14046
|
return s.projectName;
|
|
13909
14047
|
case "status":
|
|
13910
|
-
return s
|
|
14048
|
+
return statusSortValue(s);
|
|
13911
14049
|
}
|
|
13912
14050
|
}
|
|
14051
|
+
function statusSortValue(s) {
|
|
14052
|
+
const rank = s.status === "idle" ? 1 : 0;
|
|
14053
|
+
const activeAt = Date.parse(s.lastActivityAt ?? s.startedAt);
|
|
14054
|
+
const recency = String(1e15 - (Number.isNaN(activeAt) ? 0 : activeAt)).padStart(16, "0");
|
|
14055
|
+
return `${rank}:${recency}`;
|
|
14056
|
+
}
|
|
13913
14057
|
function compareValues(a, b) {
|
|
13914
14058
|
if (a === void 0 && b === void 0) return 0;
|
|
13915
14059
|
if (a === void 0) return 1;
|
|
@@ -14044,8 +14188,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14044
14188
|
|
|
14045
14189
|
// src/session-watchers.ts
|
|
14046
14190
|
var import_fs26 = require("fs");
|
|
14047
|
-
var
|
|
14048
|
-
var
|
|
14191
|
+
var import_os12 = require("os");
|
|
14192
|
+
var import_path22 = require("path");
|
|
14049
14193
|
var SessionWatchers = class {
|
|
14050
14194
|
constructor(deps) {
|
|
14051
14195
|
this.deps = deps;
|
|
@@ -14126,9 +14270,9 @@ var SessionWatchers = class {
|
|
|
14126
14270
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
14127
14271
|
watchForJsonl(sessionId, projectPath) {
|
|
14128
14272
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
14129
|
-
const projectsDir = (0,
|
|
14273
|
+
const projectsDir = (0, import_path22.join)((0, import_os12.homedir)(), ".claude", "projects", encoded);
|
|
14130
14274
|
const expectedFile = `${sessionId}.jsonl`;
|
|
14131
|
-
const filePath = (0,
|
|
14275
|
+
const filePath = (0, import_path22.join)(projectsDir, expectedFile);
|
|
14132
14276
|
const deadline = Date.now() + 12e4;
|
|
14133
14277
|
let watcher = null;
|
|
14134
14278
|
const cleanup = () => {
|
|
@@ -14150,10 +14294,10 @@ var SessionWatchers = class {
|
|
|
14150
14294
|
if (!resolvedFilePath && (0, import_fs26.existsSync)(projectsDir)) {
|
|
14151
14295
|
try {
|
|
14152
14296
|
const now = Date.now();
|
|
14153
|
-
const match = (0, import_fs26.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs26.statSync)((0,
|
|
14154
|
-
({ f }) => (0,
|
|
14297
|
+
const match = (0, import_fs26.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs26.statSync)((0, import_path22.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14298
|
+
({ f }) => (0, import_path22.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path22.join)(projectsDir, f)) === sessionId
|
|
14155
14299
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
14156
|
-
if (match) resolvedFilePath = (0,
|
|
14300
|
+
if (match) resolvedFilePath = (0, import_path22.join)(projectsDir, match.f);
|
|
14157
14301
|
} catch {
|
|
14158
14302
|
}
|
|
14159
14303
|
}
|
|
@@ -14197,7 +14341,7 @@ var SessionWatchers = class {
|
|
|
14197
14341
|
watchForCodexRollout(sessionId, projectPath) {
|
|
14198
14342
|
const deadline = Date.now() + 12e4;
|
|
14199
14343
|
const now = /* @__PURE__ */ new Date();
|
|
14200
|
-
const dateDir = (0,
|
|
14344
|
+
const dateDir = (0, import_path22.join)(
|
|
14201
14345
|
String(now.getFullYear()),
|
|
14202
14346
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
14203
14347
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -14238,7 +14382,7 @@ var SessionWatchers = class {
|
|
|
14238
14382
|
this.deps.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
14239
14383
|
);
|
|
14240
14384
|
for (const root of this.deps.codexRoots) {
|
|
14241
|
-
const sessionsDir = (0,
|
|
14385
|
+
const sessionsDir = (0, import_path22.join)(root, dateDir);
|
|
14242
14386
|
if (!(0, import_fs26.existsSync)(sessionsDir)) continue;
|
|
14243
14387
|
let candidateFiles;
|
|
14244
14388
|
try {
|
|
@@ -14247,9 +14391,9 @@ var SessionWatchers = class {
|
|
|
14247
14391
|
continue;
|
|
14248
14392
|
}
|
|
14249
14393
|
const nowMs = Date.now();
|
|
14250
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs26.statSync)((0,
|
|
14394
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs26.statSync)((0, import_path22.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
14251
14395
|
for (const { f } of recentCandidates) {
|
|
14252
|
-
const candidatePath = (0,
|
|
14396
|
+
const candidatePath = (0, import_path22.join)(sessionsDir, f);
|
|
14253
14397
|
const match = matchesProjectPath(candidatePath);
|
|
14254
14398
|
if (!match) continue;
|
|
14255
14399
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -14653,7 +14797,7 @@ var StreamerServer = class {
|
|
|
14653
14797
|
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
14654
14798
|
this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
|
|
14655
14799
|
this.scanProfiles = config.scanProfiles;
|
|
14656
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
14800
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path23.join)((0, import_os13.homedir)(), ".codex", "sessions")];
|
|
14657
14801
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
14658
14802
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
14659
14803
|
const flagResolution = resolveFeatureFlags({
|
|
@@ -14670,8 +14814,8 @@ var StreamerServer = class {
|
|
|
14670
14814
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
14671
14815
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
14672
14816
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
14673
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
14674
|
-
this.runtimeDbPath = config.runtimeDbPath
|
|
14817
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path23.join)((0, import_os13.homedir)(), ".threadbase", "cache");
|
|
14818
|
+
this.runtimeDbPath = resolveRuntimeDbPath(config.runtimeDbPath);
|
|
14675
14819
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
14676
14820
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
14677
14821
|
this.scannerManager = new ScannerManager({
|
|
@@ -14847,7 +14991,7 @@ var StreamerServer = class {
|
|
|
14847
14991
|
temporalClient,
|
|
14848
14992
|
taskQueue: agentConfig.temporal.taskQueue
|
|
14849
14993
|
});
|
|
14850
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
14994
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path23.join)((0, import_path23.dirname)(this.cacheDir), "conversations");
|
|
14851
14995
|
conversationWriter = createConversationWriter({
|
|
14852
14996
|
baseDir: conversationsBaseDir
|
|
14853
14997
|
});
|
|
@@ -15099,14 +15243,14 @@ var StreamerServer = class {
|
|
|
15099
15243
|
}
|
|
15100
15244
|
this.apnsClient = new ApnsClient(creds);
|
|
15101
15245
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
15102
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15103
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0,
|
|
15246
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
|
|
15247
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os13.hostname)());
|
|
15104
15248
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
15105
15249
|
repo: pushRepo,
|
|
15106
15250
|
sender,
|
|
15107
15251
|
sessionStore: this.sessionStore,
|
|
15108
15252
|
serverId,
|
|
15109
|
-
serverLabel: (0,
|
|
15253
|
+
serverLabel: (0, import_os13.hostname)()
|
|
15110
15254
|
});
|
|
15111
15255
|
this.liveActivityRenewal.start();
|
|
15112
15256
|
this.log.info("Live Activity push enabled", {
|
|
@@ -15126,7 +15270,7 @@ var StreamerServer = class {
|
|
|
15126
15270
|
*/
|
|
15127
15271
|
initWaitingInputPush(pushRepo) {
|
|
15128
15272
|
const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
|
|
15129
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15273
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
|
|
15130
15274
|
this.waitingInputNotifier = new WaitingInputNotifier(
|
|
15131
15275
|
sender,
|
|
15132
15276
|
serverId,
|
|
@@ -15263,7 +15407,7 @@ var StreamerServer = class {
|
|
|
15263
15407
|
let sessions = null;
|
|
15264
15408
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
15265
15409
|
const transport = await connectOrSpawnHost({
|
|
15266
|
-
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
15410
|
+
instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)()
|
|
15267
15411
|
});
|
|
15268
15412
|
try {
|
|
15269
15413
|
sessions = await this.ptyManager.useRemoteRunner(transport);
|
|
@@ -15320,6 +15464,7 @@ var StreamerServer = class {
|
|
|
15320
15464
|
try {
|
|
15321
15465
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15322
15466
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
15467
|
+
this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());
|
|
15323
15468
|
} catch (err) {
|
|
15324
15469
|
const message = err instanceof Error ? err.message : String(err);
|
|
15325
15470
|
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
@@ -15339,7 +15484,7 @@ var StreamerServer = class {
|
|
|
15339
15484
|
}
|
|
15340
15485
|
try {
|
|
15341
15486
|
this.cache = ConversationCache.open(
|
|
15342
|
-
(0,
|
|
15487
|
+
(0, import_path23.join)(this.cacheDir, "cache.db"),
|
|
15343
15488
|
this.tailSize,
|
|
15344
15489
|
void 0,
|
|
15345
15490
|
{
|
|
@@ -15369,18 +15514,39 @@ var StreamerServer = class {
|
|
|
15369
15514
|
if (copied > 0) {
|
|
15370
15515
|
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
15371
15516
|
copied,
|
|
15517
|
+
table: "managed_sessions",
|
|
15372
15518
|
event: "runtime.legacy_import"
|
|
15373
15519
|
});
|
|
15374
15520
|
}
|
|
15375
15521
|
} catch (err) {
|
|
15376
15522
|
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
15377
15523
|
event: "runtime.legacy_import_failed",
|
|
15524
|
+
table: "managed_sessions",
|
|
15525
|
+
err
|
|
15526
|
+
});
|
|
15527
|
+
}
|
|
15528
|
+
try {
|
|
15529
|
+
const result = this.runtimeStore?.importLegacyDevices(db);
|
|
15530
|
+
if (result && result.copied > 0) {
|
|
15531
|
+
this.log.info(
|
|
15532
|
+
`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)"),
|
|
15533
|
+
{
|
|
15534
|
+
copied: result.copied,
|
|
15535
|
+
purged: result.purged,
|
|
15536
|
+
table: "devices",
|
|
15537
|
+
event: "runtime.legacy_import"
|
|
15538
|
+
}
|
|
15539
|
+
);
|
|
15540
|
+
}
|
|
15541
|
+
} catch (err) {
|
|
15542
|
+
this.log.warn("[registry] legacy devices move failed", {
|
|
15543
|
+
event: "runtime.legacy_import_failed",
|
|
15544
|
+
table: "devices",
|
|
15378
15545
|
err
|
|
15379
15546
|
});
|
|
15380
15547
|
}
|
|
15381
15548
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
15382
15549
|
this.pushRepo = new PushRepository(db);
|
|
15383
|
-
this.devicesRepo = new DevicesRepository(db);
|
|
15384
15550
|
this.initLiveActivityPush(this.pushRepo);
|
|
15385
15551
|
this.initWaitingInputPush(this.pushRepo);
|
|
15386
15552
|
this.cacheMonitor = new CacheIntegrityMonitor(
|
|
@@ -15706,7 +15872,7 @@ var StreamerServer = class {
|
|
|
15706
15872
|
nonce: sealed.nonce,
|
|
15707
15873
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
15708
15874
|
publicUrl: this.publicUrl,
|
|
15709
|
-
machineName: (0,
|
|
15875
|
+
machineName: (0, import_os13.hostname)(),
|
|
15710
15876
|
...device && {
|
|
15711
15877
|
deviceId: device.deviceId,
|
|
15712
15878
|
deviceToken: device.deviceToken,
|