@threadbase-sh/streamer 1.52.5 → 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 +747 -466
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +221 -61
- 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 +218 -58
- 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.js
CHANGED
|
@@ -555,7 +555,8 @@ function loadOrCreateApiKey() {
|
|
|
555
555
|
const key = generateApiKey();
|
|
556
556
|
mkdirSync(configDir(), { recursive: true });
|
|
557
557
|
writeFileSync(configFile(), `api_key: ${key}
|
|
558
|
-
`, "utf-8");
|
|
558
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
559
|
+
chmodSync(configFile(), 384);
|
|
559
560
|
return key;
|
|
560
561
|
}
|
|
561
562
|
function loadBrowseRoot() {
|
|
@@ -3863,8 +3864,8 @@ import { EventEmitter } from "events";
|
|
|
3863
3864
|
import { existsSync as existsSync16 } from "fs";
|
|
3864
3865
|
import { realpath as realpath2 } from "fs/promises";
|
|
3865
3866
|
import { createServer as createServer2 } from "http";
|
|
3866
|
-
import { homedir as
|
|
3867
|
-
import { dirname as dirname10, join as
|
|
3867
|
+
import { homedir as homedir13, hostname as hostname3 } from "os";
|
|
3868
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
3868
3869
|
|
|
3869
3870
|
// src/api/app.ts
|
|
3870
3871
|
import { Hono as Hono18 } from "hono";
|
|
@@ -4000,6 +4001,8 @@ var DevicesRepository = class {
|
|
|
4000
4001
|
listStmt;
|
|
4001
4002
|
revokeStmt;
|
|
4002
4003
|
touchStmt;
|
|
4004
|
+
deleteStmt;
|
|
4005
|
+
deleteRevokedStmt;
|
|
4003
4006
|
constructor(db) {
|
|
4004
4007
|
this.insertStmt = db.prepare(`
|
|
4005
4008
|
INSERT INTO devices (
|
|
@@ -4013,6 +4016,8 @@ var DevicesRepository = class {
|
|
|
4013
4016
|
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
4014
4017
|
this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
|
|
4015
4018
|
this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
|
|
4019
|
+
this.deleteStmt = db.prepare("DELETE FROM devices WHERE device_id = ?");
|
|
4020
|
+
this.deleteRevokedStmt = db.prepare("DELETE FROM devices WHERE revoked_at IS NOT NULL");
|
|
4016
4021
|
}
|
|
4017
4022
|
/**
|
|
4018
4023
|
* Record a newly paired device and mint its token.
|
|
@@ -4059,6 +4064,33 @@ var DevicesRepository = class {
|
|
|
4059
4064
|
revoke(deviceId, now = Date.now()) {
|
|
4060
4065
|
return this.revokeStmt.run(now, deviceId).changes > 0;
|
|
4061
4066
|
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Erase one device's record outright.
|
|
4069
|
+
*
|
|
4070
|
+
* Deliberately separate from `revoke`, which is a soft delete that keeps the
|
|
4071
|
+
* row so `list()` can show what happened. That audit trail is the right
|
|
4072
|
+
* default — but it meant a `devices` row, including the user-supplied `name`
|
|
4073
|
+
* ("Ronen's iPhone"), had no removal path at all once the registry moved to
|
|
4074
|
+
* runtime.db, which no command deletes. This is that path.
|
|
4075
|
+
*
|
|
4076
|
+
* Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
|
|
4077
|
+
* device whose token is still on a phone somewhere stops being *known* rather
|
|
4078
|
+
* than being *refused*. Revoke first, delete second, is the safe order, and
|
|
4079
|
+
* `deleteRevoked()` exists so that is the easy thing to do.
|
|
4080
|
+
*/
|
|
4081
|
+
delete(deviceId) {
|
|
4082
|
+
return this.deleteStmt.run(deviceId).changes > 0;
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Erase every already-revoked device. The bulk companion to `delete`, and the
|
|
4086
|
+
* one that is safe by construction: a revoked device is already refused, so
|
|
4087
|
+
* removing its row cannot restore access to anything.
|
|
4088
|
+
*
|
|
4089
|
+
* Returns the number of rows removed.
|
|
4090
|
+
*/
|
|
4091
|
+
deleteRevoked() {
|
|
4092
|
+
return this.deleteRevokedStmt.run().changes;
|
|
4093
|
+
}
|
|
4062
4094
|
touch(deviceId, now = Date.now()) {
|
|
4063
4095
|
this.touchStmt.run(now, deviceId);
|
|
4064
4096
|
}
|
|
@@ -4585,6 +4617,34 @@ var createDeviceRoutes = (deps) => {
|
|
|
4585
4617
|
repo.revoke(id);
|
|
4586
4618
|
return c.json({ ok: true, alreadyRevoked: false });
|
|
4587
4619
|
});
|
|
4620
|
+
app.delete("/:id", (c) => {
|
|
4621
|
+
const repo = deps.devicesRepo();
|
|
4622
|
+
if (!repo) {
|
|
4623
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4624
|
+
}
|
|
4625
|
+
const id = c.req.param("id");
|
|
4626
|
+
const existing = repo.get(id);
|
|
4627
|
+
if (!existing) return c.json({ ok: true, alreadyDeleted: true });
|
|
4628
|
+
const force = c.req.query("force") === "1" || c.req.query("force") === "true";
|
|
4629
|
+
if (existing.revoked_at == null && !force) {
|
|
4630
|
+
return c.json(
|
|
4631
|
+
{
|
|
4632
|
+
error: "Revoke the device before deleting it, or pass ?force=1",
|
|
4633
|
+
code: "DEVICE_ACTIVE"
|
|
4634
|
+
},
|
|
4635
|
+
409
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
repo.delete(id);
|
|
4639
|
+
return c.json({ ok: true, alreadyDeleted: false });
|
|
4640
|
+
});
|
|
4641
|
+
app.delete("/", (c) => {
|
|
4642
|
+
const repo = deps.devicesRepo();
|
|
4643
|
+
if (!repo) {
|
|
4644
|
+
return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
4645
|
+
}
|
|
4646
|
+
return c.json({ ok: true, deleted: repo.deleteRevoked() });
|
|
4647
|
+
});
|
|
4588
4648
|
return app;
|
|
4589
4649
|
};
|
|
4590
4650
|
|
|
@@ -5501,6 +5561,13 @@ var createMiscRoutes = (deps) => {
|
|
|
5501
5561
|
// Same contract: this server serves GET /api/projects/summary, which the
|
|
5502
5562
|
// Hub's grouped views need before they can draw a tree.
|
|
5503
5563
|
projectSummary: true,
|
|
5564
|
+
// The paired-device registry lives in runtime.db, so it survives
|
|
5565
|
+
// `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.
|
|
5566
|
+
// A client may only prefer its scoped device token over the shared API
|
|
5567
|
+
// key when this is true: on an older server the registry is inside
|
|
5568
|
+
// cache.db, where a documented troubleshooting step deletes it and every
|
|
5569
|
+
// device token with it. Absent means "old server, assume not durable".
|
|
5570
|
+
devicesDurable: true,
|
|
5504
5571
|
// Delivery capability, not endpoint support: whether this server can
|
|
5505
5572
|
// actually send a push, so mobile can hide an affordance instead of
|
|
5506
5573
|
// registering tokens nothing will ever send to. Absent on older servers,
|
|
@@ -5911,7 +5978,13 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5911
5978
|
const app = new Hono17();
|
|
5912
5979
|
app.get(
|
|
5913
5980
|
"/ws",
|
|
5914
|
-
|
|
5981
|
+
// The principal is read here, at the upgrade, and captured for the life of
|
|
5982
|
+
// the socket. authMiddleware sets it because /ws is classified
|
|
5983
|
+
// `history:read`, but it only ever reaches the HTTP request — without
|
|
5984
|
+
// capturing it the socket has no principal at all, so every frame after
|
|
5985
|
+
// the upgrade is unauthorized-by-omission.
|
|
5986
|
+
upgradeWebSocket((c) => {
|
|
5987
|
+
const principal = c.get("principal") ?? null;
|
|
5915
5988
|
let openWs = null;
|
|
5916
5989
|
return {
|
|
5917
5990
|
onOpen(_evt, ws) {
|
|
@@ -5921,7 +5994,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
5921
5994
|
deps.handleWsOpen(raw);
|
|
5922
5995
|
},
|
|
5923
5996
|
onMessage(evt, _ws) {
|
|
5924
|
-
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
5997
|
+
if (openWs) deps.handleWsMessage(openWs, evt.data, principal);
|
|
5925
5998
|
},
|
|
5926
5999
|
onClose(_evt, _ws) {
|
|
5927
6000
|
if (openWs) deps.handleWsClose(openWs);
|
|
@@ -10651,6 +10724,11 @@ var SessionsRepository = class {
|
|
|
10651
10724
|
|
|
10652
10725
|
// src/db/runtime-store.ts
|
|
10653
10726
|
import Database2 from "better-sqlite3";
|
|
10727
|
+
import { homedir as homedir6 } from "os";
|
|
10728
|
+
import { join as join16 } from "path";
|
|
10729
|
+
function resolveRuntimeDbPath(override) {
|
|
10730
|
+
return override ?? process.env.THREADBASE_RUNTIME_DB ?? join16(process.env.THREADBASE_CONFIG_DIR ?? join16(homedir6(), ".threadbase"), "runtime.db");
|
|
10731
|
+
}
|
|
10654
10732
|
var RuntimeStore = class _RuntimeStore {
|
|
10655
10733
|
constructor(db) {
|
|
10656
10734
|
this.db = db;
|
|
@@ -10668,23 +10746,64 @@ var RuntimeStore = class _RuntimeStore {
|
|
|
10668
10746
|
/**
|
|
10669
10747
|
* One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
|
|
10670
10748
|
*
|
|
10671
|
-
* Non-destructive by design: the source table is left in place so an older
|
|
10672
|
-
* streamer rolled back onto the same machine still finds its registry. Runs
|
|
10673
|
-
* only when this file's table is empty, so a second boot is a no-op rather
|
|
10674
|
-
* than a re-copy that would resurrect rows deleted since.
|
|
10675
|
-
*
|
|
10676
10749
|
* Returns the number of rows copied.
|
|
10677
10750
|
*/
|
|
10678
10751
|
importLegacyManagedSessions(source) {
|
|
10679
|
-
|
|
10752
|
+
return this.importLegacyTable(source, "managed_sessions");
|
|
10753
|
+
}
|
|
10754
|
+
/**
|
|
10755
|
+
* One-time move of `devices` rows out of `cache.db`, where the registry used
|
|
10756
|
+
* to live (migration `011_create_devices.sql`).
|
|
10757
|
+
*
|
|
10758
|
+
* Losing this table invalidates every device token ever issued, and cache.db
|
|
10759
|
+
* is the file `tb-streamer cache clear` deletes and the integrity monitor
|
|
10760
|
+
* rebuilds — see `runtime-migrations/003_create_devices.sql`.
|
|
10761
|
+
*
|
|
10762
|
+
* Unlike `managed_sessions`, this one MOVES rather than copies: the source
|
|
10763
|
+
* rows are deleted once the copy is verified. A `devices` row carries a
|
|
10764
|
+
* user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
|
|
10765
|
+
* on disk indefinitely — in the one file the user is told to delete when
|
|
10766
|
+
* something goes wrong — is more retained personal data than the rollback
|
|
10767
|
+
* path is worth. Recovering from a rollback is re-scanning a pairing QR.
|
|
10768
|
+
*
|
|
10769
|
+
* The delete is conditional on the copy being complete: `INSERT OR IGNORE`
|
|
10770
|
+
* can silently skip a row, so the destination count must match what was read
|
|
10771
|
+
* before anything is removed. A mismatch keeps the source and reports
|
|
10772
|
+
* `purged: false` rather than throwing — the import itself still succeeded,
|
|
10773
|
+
* and keeping data is the safe direction to fail in.
|
|
10774
|
+
*/
|
|
10775
|
+
importLegacyDevices(source) {
|
|
10776
|
+
const copied = this.importLegacyTable(source, "devices");
|
|
10777
|
+
if (copied === 0) return { copied: 0, purged: false };
|
|
10778
|
+
const landed = this.db.prepare("SELECT COUNT(*) AS n FROM devices").get().n;
|
|
10779
|
+
if (landed !== copied) return { copied, purged: false };
|
|
10780
|
+
source.prepare("DELETE FROM devices").run();
|
|
10781
|
+
return { copied, purged: true };
|
|
10782
|
+
}
|
|
10783
|
+
/**
|
|
10784
|
+
* Copy a whole table out of a pre-split `cache.db` into this file.
|
|
10785
|
+
*
|
|
10786
|
+
* The copy itself is non-destructive — the source is left in place, so an
|
|
10787
|
+
* older streamer rolled back onto the same machine still finds its data.
|
|
10788
|
+
* `importLegacyDevices` deletes the source afterwards for its own reasons;
|
|
10789
|
+
* `managed_sessions` does not. Runs only when this file's table is empty, so
|
|
10790
|
+
* a second boot is a no-op rather than a re-copy that would resurrect rows
|
|
10791
|
+
* deleted since.
|
|
10792
|
+
*
|
|
10793
|
+
* The table name is interpolated into SQL, so it is typed as a closed union
|
|
10794
|
+
* rather than `string` — the set of tables that can ever be lifted is known
|
|
10795
|
+
* at compile time, and that is what keeps a caller from making this a hole.
|
|
10796
|
+
*/
|
|
10797
|
+
importLegacyTable(source, table) {
|
|
10798
|
+
const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
10680
10799
|
if (existing.n > 0) return 0;
|
|
10681
|
-
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name =
|
|
10800
|
+
const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
10682
10801
|
if (!hasTable) return 0;
|
|
10683
|
-
const rows = source.prepare(
|
|
10802
|
+
const rows = source.prepare(`SELECT * FROM ${table}`).all();
|
|
10684
10803
|
if (rows.length === 0) return 0;
|
|
10685
10804
|
const columns = Object.keys(rows[0]);
|
|
10686
10805
|
const insert = this.db.prepare(
|
|
10687
|
-
`INSERT OR IGNORE INTO
|
|
10806
|
+
`INSERT OR IGNORE INTO ${table} (${columns.join(", ")})
|
|
10688
10807
|
VALUES (${columns.map((c) => `@${c}`).join(", ")})`
|
|
10689
10808
|
);
|
|
10690
10809
|
this.db.transaction((batch) => {
|
|
@@ -10926,14 +11045,14 @@ import { spawn as spawn2 } from "child_process";
|
|
|
10926
11045
|
|
|
10927
11046
|
// src/pty-host/socket.ts
|
|
10928
11047
|
import { createConnection, createServer } from "net";
|
|
10929
|
-
import { homedir as
|
|
10930
|
-
import { join as
|
|
11048
|
+
import { homedir as homedir7 } from "os";
|
|
11049
|
+
import { join as join17 } from "path";
|
|
10931
11050
|
function hostSocketPath(instanceId) {
|
|
10932
11051
|
if (process.platform === "win32") {
|
|
10933
11052
|
return `\\\\.\\pipe\\threadbase-pty-host-${instanceId}`;
|
|
10934
11053
|
}
|
|
10935
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ??
|
|
10936
|
-
return
|
|
11054
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join17(homedir7(), ".threadbase");
|
|
11055
|
+
return join17(dir, "run", `pty-host-${instanceId}.sock`);
|
|
10937
11056
|
}
|
|
10938
11057
|
function socketTransport(socket) {
|
|
10939
11058
|
socket.setEncoding("utf8");
|
|
@@ -11008,8 +11127,8 @@ import {
|
|
|
11008
11127
|
ConversationScanner
|
|
11009
11128
|
} from "@threadbase-sh/scanner";
|
|
11010
11129
|
import { statSync as statSync7 } from "fs";
|
|
11011
|
-
import { homedir as
|
|
11012
|
-
import { join as
|
|
11130
|
+
import { homedir as homedir9 } from "os";
|
|
11131
|
+
import { join as join19 } from "path";
|
|
11013
11132
|
|
|
11014
11133
|
// src/services/cache/cacheMetadata.ts
|
|
11015
11134
|
function getCacheMetadata(repo, key) {
|
|
@@ -11109,9 +11228,9 @@ function refreshConversationCache(deps) {
|
|
|
11109
11228
|
|
|
11110
11229
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
11111
11230
|
import { readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
11112
|
-
import { homedir as
|
|
11113
|
-
import { join as
|
|
11114
|
-
var DEFAULT_PROJECTS_DIR =
|
|
11231
|
+
import { homedir as homedir8 } from "os";
|
|
11232
|
+
import { join as join18 } from "path";
|
|
11233
|
+
var DEFAULT_PROJECTS_DIR = join18(homedir8(), ".claude", "projects");
|
|
11115
11234
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
11116
11235
|
let maxMs;
|
|
11117
11236
|
try {
|
|
@@ -11123,7 +11242,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
|
|
|
11123
11242
|
for (const ent of readdirSync4(projectsDir, { withFileTypes: true })) {
|
|
11124
11243
|
if (!ent.isDirectory()) continue;
|
|
11125
11244
|
try {
|
|
11126
|
-
const childMs = statSync6(
|
|
11245
|
+
const childMs = statSync6(join18(projectsDir, ent.name)).mtimeMs;
|
|
11127
11246
|
if (childMs > maxMs) maxMs = childMs;
|
|
11128
11247
|
} catch {
|
|
11129
11248
|
}
|
|
@@ -11295,9 +11414,9 @@ var ScannerManager = class {
|
|
|
11295
11414
|
projectsDirs() {
|
|
11296
11415
|
const profiles = this.deps.scanProfiles;
|
|
11297
11416
|
if (profiles && profiles.length > 0) {
|
|
11298
|
-
return profiles.filter((p) => p.enabled).map((p) =>
|
|
11417
|
+
return profiles.filter((p) => p.enabled).map((p) => join19(p.configDir, "projects"));
|
|
11299
11418
|
}
|
|
11300
|
-
return [
|
|
11419
|
+
return [join19(homedir9(), ".claude", "projects")];
|
|
11301
11420
|
}
|
|
11302
11421
|
// ─── staleness ────────────────────────────────────────────────────
|
|
11303
11422
|
// Drain the stale set and disarm the flag together. The caller owns the
|
|
@@ -11578,8 +11697,8 @@ import { statSync as statSync9 } from "fs";
|
|
|
11578
11697
|
|
|
11579
11698
|
// src/handlers/handleListProjects.ts
|
|
11580
11699
|
import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as statSync8 } from "fs";
|
|
11581
|
-
import { homedir as
|
|
11582
|
-
import { join as
|
|
11700
|
+
import { homedir as homedir10 } from "os";
|
|
11701
|
+
import { join as join20 } from "path";
|
|
11583
11702
|
var HEAD_BYTES = 64 * 1024;
|
|
11584
11703
|
var MAX_FILES_PROBED = 3;
|
|
11585
11704
|
function readRecordedCwd(dir) {
|
|
@@ -11592,7 +11711,7 @@ function readRecordedCwd(dir) {
|
|
|
11592
11711
|
for (const file of files.slice(0, MAX_FILES_PROBED)) {
|
|
11593
11712
|
let fd;
|
|
11594
11713
|
try {
|
|
11595
|
-
fd = openSync4(
|
|
11714
|
+
fd = openSync4(join20(dir, file), "r");
|
|
11596
11715
|
const buf = Buffer.alloc(HEAD_BYTES);
|
|
11597
11716
|
const bytes = readSync4(fd, buf, 0, HEAD_BYTES, 0);
|
|
11598
11717
|
for (const line of buf.subarray(0, bytes).toString("utf8").split("\n")) {
|
|
@@ -11616,11 +11735,11 @@ function decodeProjectPath(dirName) {
|
|
|
11616
11735
|
function handleListProjects(url, res) {
|
|
11617
11736
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
11618
11737
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
11619
|
-
const projectsDir =
|
|
11738
|
+
const projectsDir = join20(homedir10(), ".claude", "projects");
|
|
11620
11739
|
let entries;
|
|
11621
11740
|
try {
|
|
11622
11741
|
entries = readdirSync5(projectsDir).map((dirName) => {
|
|
11623
|
-
const fullPath =
|
|
11742
|
+
const fullPath = join20(projectsDir, dirName);
|
|
11624
11743
|
let mtime = 0;
|
|
11625
11744
|
try {
|
|
11626
11745
|
mtime = statSync8(fullPath).mtimeMs;
|
|
@@ -11635,7 +11754,7 @@ function handleListProjects(url, res) {
|
|
|
11635
11754
|
}
|
|
11636
11755
|
const total = entries.length;
|
|
11637
11756
|
const page = entries.slice(offset, offset + limit).map(({ dirName }) => {
|
|
11638
|
-
const path = readRecordedCwd(
|
|
11757
|
+
const path = readRecordedCwd(join20(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
|
|
11639
11758
|
const name = path.split(/[\\/]/).filter(Boolean).pop() ?? dirName;
|
|
11640
11759
|
return { name, path, dirName };
|
|
11641
11760
|
});
|
|
@@ -11644,6 +11763,9 @@ function handleListProjects(url, res) {
|
|
|
11644
11763
|
}
|
|
11645
11764
|
|
|
11646
11765
|
// src/server-wiring.ts
|
|
11766
|
+
function wsAllows(principal, required) {
|
|
11767
|
+
return principal === null || hasCapability(principal, required);
|
|
11768
|
+
}
|
|
11647
11769
|
function createConversationWatcherEvents(deps) {
|
|
11648
11770
|
return {
|
|
11649
11771
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -11925,7 +12047,15 @@ function createApiDeps(deps) {
|
|
|
11925
12047
|
const alertMsg = deps.cacheMonitor()?.wsMessage();
|
|
11926
12048
|
if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
|
|
11927
12049
|
},
|
|
11928
|
-
handleWsMessage: async (ws, raw) => {
|
|
12050
|
+
handleWsMessage: async (ws, raw, principal) => {
|
|
12051
|
+
const deny = (type, required) => {
|
|
12052
|
+
deps.log().warn(`[ws.capability_denied] ${type} requires ${required}`, {
|
|
12053
|
+
event: "ws.capability_denied",
|
|
12054
|
+
type,
|
|
12055
|
+
required,
|
|
12056
|
+
...principal?.deviceId ? { deviceId: principal.deviceId } : {}
|
|
12057
|
+
});
|
|
12058
|
+
};
|
|
11929
12059
|
try {
|
|
11930
12060
|
const msg = JSON.parse(String(raw));
|
|
11931
12061
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
@@ -11935,6 +12065,10 @@ function createApiDeps(deps) {
|
|
|
11935
12065
|
deps.wsToClientId.set(ws, msg.clientId);
|
|
11936
12066
|
}
|
|
11937
12067
|
if (msg.type === "subscribe_session" && typeof msg.sessionId === "string") {
|
|
12068
|
+
if (!wsAllows(principal, "history:read")) {
|
|
12069
|
+
deny(msg.type, "history:read");
|
|
12070
|
+
return;
|
|
12071
|
+
}
|
|
11938
12072
|
deps.addSessionSubscriber(msg.sessionId, ws);
|
|
11939
12073
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
11940
12074
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, 200);
|
|
@@ -11983,6 +12117,10 @@ function createApiDeps(deps) {
|
|
|
11983
12117
|
}
|
|
11984
12118
|
}
|
|
11985
12119
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
12120
|
+
if (!wsAllows(principal, "session:control")) {
|
|
12121
|
+
deny(msg.type, "session:control");
|
|
12122
|
+
return;
|
|
12123
|
+
}
|
|
11986
12124
|
deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
|
|
11987
12125
|
}
|
|
11988
12126
|
} catch {
|
|
@@ -12010,11 +12148,11 @@ import { existsSync as existsSync12 } from "fs";
|
|
|
12010
12148
|
|
|
12011
12149
|
// src/services/cache-integrity/alertStore.ts
|
|
12012
12150
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
12013
|
-
import { homedir as
|
|
12014
|
-
import { dirname as dirname9, join as
|
|
12151
|
+
import { homedir as homedir11 } from "os";
|
|
12152
|
+
import { dirname as dirname9, join as join21 } from "path";
|
|
12015
12153
|
function alertStatePath() {
|
|
12016
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ??
|
|
12017
|
-
return
|
|
12154
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? join21(homedir11(), ".threadbase");
|
|
12155
|
+
return join21(dir, "cache-alert.json");
|
|
12018
12156
|
}
|
|
12019
12157
|
function loadAlertState() {
|
|
12020
12158
|
try {
|
|
@@ -12033,7 +12171,7 @@ function saveAlertState(state) {
|
|
|
12033
12171
|
|
|
12034
12172
|
// src/services/cache-integrity/backup.ts
|
|
12035
12173
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync6, statSync as statSync10, unlinkSync } from "fs";
|
|
12036
|
-
import { join as
|
|
12174
|
+
import { join as join22 } from "path";
|
|
12037
12175
|
var DEFAULT_RETAIN = 3;
|
|
12038
12176
|
function retainCount() {
|
|
12039
12177
|
const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
|
|
@@ -12044,13 +12182,13 @@ function timestamp(d) {
|
|
|
12044
12182
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
12045
12183
|
}
|
|
12046
12184
|
async function backupCacheDb(db, cacheDir) {
|
|
12047
|
-
const backupsDir =
|
|
12185
|
+
const backupsDir = join22(cacheDir, "backups");
|
|
12048
12186
|
mkdirSync5(backupsDir, { recursive: true });
|
|
12049
|
-
const destPath =
|
|
12187
|
+
const destPath = join22(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
12050
12188
|
await db.backup(destPath);
|
|
12051
12189
|
const retain = retainCount();
|
|
12052
12190
|
const backups = readdirSync6(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
12053
|
-
const full =
|
|
12191
|
+
const full = join22(backupsDir, f);
|
|
12054
12192
|
return { full, mtime: statSync10(full).mtimeMs };
|
|
12055
12193
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
12056
12194
|
for (const stale of backups.slice(retain)) {
|
|
@@ -14001,8 +14139,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14001
14139
|
|
|
14002
14140
|
// src/session-watchers.ts
|
|
14003
14141
|
import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync9, statSync as statSync12 } from "fs";
|
|
14004
|
-
import { homedir as
|
|
14005
|
-
import { basename as basename6, join as
|
|
14142
|
+
import { homedir as homedir12 } from "os";
|
|
14143
|
+
import { basename as basename6, join as join23 } from "path";
|
|
14006
14144
|
var SessionWatchers = class {
|
|
14007
14145
|
constructor(deps) {
|
|
14008
14146
|
this.deps = deps;
|
|
@@ -14083,9 +14221,9 @@ var SessionWatchers = class {
|
|
|
14083
14221
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
14084
14222
|
watchForJsonl(sessionId, projectPath) {
|
|
14085
14223
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
14086
|
-
const projectsDir =
|
|
14224
|
+
const projectsDir = join23(homedir12(), ".claude", "projects", encoded);
|
|
14087
14225
|
const expectedFile = `${sessionId}.jsonl`;
|
|
14088
|
-
const filePath =
|
|
14226
|
+
const filePath = join23(projectsDir, expectedFile);
|
|
14089
14227
|
const deadline = Date.now() + 12e4;
|
|
14090
14228
|
let watcher = null;
|
|
14091
14229
|
const cleanup = () => {
|
|
@@ -14107,10 +14245,10 @@ var SessionWatchers = class {
|
|
|
14107
14245
|
if (!resolvedFilePath && existsSync15(projectsDir)) {
|
|
14108
14246
|
try {
|
|
14109
14247
|
const now = Date.now();
|
|
14110
|
-
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync12(
|
|
14111
|
-
({ f }) => basename6(f, ".jsonl") === sessionId || this.readFirstLineSessionId(
|
|
14248
|
+
const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync12(join23(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
14249
|
+
({ f }) => basename6(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join23(projectsDir, f)) === sessionId
|
|
14112
14250
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
14113
|
-
if (match) resolvedFilePath =
|
|
14251
|
+
if (match) resolvedFilePath = join23(projectsDir, match.f);
|
|
14114
14252
|
} catch {
|
|
14115
14253
|
}
|
|
14116
14254
|
}
|
|
@@ -14154,7 +14292,7 @@ var SessionWatchers = class {
|
|
|
14154
14292
|
watchForCodexRollout(sessionId, projectPath) {
|
|
14155
14293
|
const deadline = Date.now() + 12e4;
|
|
14156
14294
|
const now = /* @__PURE__ */ new Date();
|
|
14157
|
-
const dateDir =
|
|
14295
|
+
const dateDir = join23(
|
|
14158
14296
|
String(now.getFullYear()),
|
|
14159
14297
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
14160
14298
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -14195,7 +14333,7 @@ var SessionWatchers = class {
|
|
|
14195
14333
|
this.deps.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
14196
14334
|
);
|
|
14197
14335
|
for (const root of this.deps.codexRoots) {
|
|
14198
|
-
const sessionsDir =
|
|
14336
|
+
const sessionsDir = join23(root, dateDir);
|
|
14199
14337
|
if (!existsSync15(sessionsDir)) continue;
|
|
14200
14338
|
let candidateFiles;
|
|
14201
14339
|
try {
|
|
@@ -14204,9 +14342,9 @@ var SessionWatchers = class {
|
|
|
14204
14342
|
continue;
|
|
14205
14343
|
}
|
|
14206
14344
|
const nowMs = Date.now();
|
|
14207
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync12(
|
|
14345
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync12(join23(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
14208
14346
|
for (const { f } of recentCandidates) {
|
|
14209
|
-
const candidatePath =
|
|
14347
|
+
const candidatePath = join23(sessionsDir, f);
|
|
14210
14348
|
const match = matchesProjectPath(candidatePath);
|
|
14211
14349
|
if (!match) continue;
|
|
14212
14350
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -14610,7 +14748,7 @@ var StreamerServer = class {
|
|
|
14610
14748
|
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
14611
14749
|
this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
|
|
14612
14750
|
this.scanProfiles = config.scanProfiles;
|
|
14613
|
-
this.codexRoots = config.codexRoots ?? [
|
|
14751
|
+
this.codexRoots = config.codexRoots ?? [join24(homedir13(), ".codex", "sessions")];
|
|
14614
14752
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
14615
14753
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
14616
14754
|
const flagResolution = resolveFeatureFlags({
|
|
@@ -14627,8 +14765,8 @@ var StreamerServer = class {
|
|
|
14627
14765
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
14628
14766
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
14629
14767
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
14630
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
14631
|
-
this.runtimeDbPath = config.runtimeDbPath
|
|
14768
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join24(homedir13(), ".threadbase", "cache");
|
|
14769
|
+
this.runtimeDbPath = resolveRuntimeDbPath(config.runtimeDbPath);
|
|
14632
14770
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
14633
14771
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
14634
14772
|
this.scannerManager = new ScannerManager({
|
|
@@ -14804,7 +14942,7 @@ var StreamerServer = class {
|
|
|
14804
14942
|
temporalClient,
|
|
14805
14943
|
taskQueue: agentConfig.temporal.taskQueue
|
|
14806
14944
|
});
|
|
14807
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
14945
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join24(dirname10(this.cacheDir), "conversations");
|
|
14808
14946
|
conversationWriter = createConversationWriter({
|
|
14809
14947
|
baseDir: conversationsBaseDir
|
|
14810
14948
|
});
|
|
@@ -15277,6 +15415,7 @@ var StreamerServer = class {
|
|
|
15277
15415
|
try {
|
|
15278
15416
|
this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
|
|
15279
15417
|
this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
|
|
15418
|
+
this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());
|
|
15280
15419
|
} catch (err) {
|
|
15281
15420
|
const message = err instanceof Error ? err.message : String(err);
|
|
15282
15421
|
const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
|
|
@@ -15296,7 +15435,7 @@ var StreamerServer = class {
|
|
|
15296
15435
|
}
|
|
15297
15436
|
try {
|
|
15298
15437
|
this.cache = ConversationCache.open(
|
|
15299
|
-
|
|
15438
|
+
join24(this.cacheDir, "cache.db"),
|
|
15300
15439
|
this.tailSize,
|
|
15301
15440
|
void 0,
|
|
15302
15441
|
{
|
|
@@ -15326,18 +15465,39 @@ var StreamerServer = class {
|
|
|
15326
15465
|
if (copied > 0) {
|
|
15327
15466
|
this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
|
|
15328
15467
|
copied,
|
|
15468
|
+
table: "managed_sessions",
|
|
15329
15469
|
event: "runtime.legacy_import"
|
|
15330
15470
|
});
|
|
15331
15471
|
}
|
|
15332
15472
|
} catch (err) {
|
|
15333
15473
|
this.log.warn("[registry] legacy managed_sessions copy failed", {
|
|
15334
15474
|
event: "runtime.legacy_import_failed",
|
|
15475
|
+
table: "managed_sessions",
|
|
15476
|
+
err
|
|
15477
|
+
});
|
|
15478
|
+
}
|
|
15479
|
+
try {
|
|
15480
|
+
const result = this.runtimeStore?.importLegacyDevices(db);
|
|
15481
|
+
if (result && result.copied > 0) {
|
|
15482
|
+
this.log.info(
|
|
15483
|
+
`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)"),
|
|
15484
|
+
{
|
|
15485
|
+
copied: result.copied,
|
|
15486
|
+
purged: result.purged,
|
|
15487
|
+
table: "devices",
|
|
15488
|
+
event: "runtime.legacy_import"
|
|
15489
|
+
}
|
|
15490
|
+
);
|
|
15491
|
+
}
|
|
15492
|
+
} catch (err) {
|
|
15493
|
+
this.log.warn("[registry] legacy devices move failed", {
|
|
15494
|
+
event: "runtime.legacy_import_failed",
|
|
15495
|
+
table: "devices",
|
|
15335
15496
|
err
|
|
15336
15497
|
});
|
|
15337
15498
|
}
|
|
15338
15499
|
this.cacheMetadataRepo = new CacheMetadataRepository(db);
|
|
15339
15500
|
this.pushRepo = new PushRepository(db);
|
|
15340
|
-
this.devicesRepo = new DevicesRepository(db);
|
|
15341
15501
|
this.initLiveActivityPush(this.pushRepo);
|
|
15342
15502
|
this.initWaitingInputPush(this.pushRepo);
|
|
15343
15503
|
this.cacheMonitor = new CacheIntegrityMonitor(
|