@threadbase-sh/streamer 1.39.0 → 1.40.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 +840 -357
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +668 -185
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +611 -128
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3061,10 +3061,10 @@ var import_client = require("@temporalio/client");
|
|
|
3061
3061
|
var import_scanner3 = require("@threadbase-sh/scanner");
|
|
3062
3062
|
var import_crypto11 = require("crypto");
|
|
3063
3063
|
var import_events = require("events");
|
|
3064
|
-
var
|
|
3064
|
+
var import_fs19 = require("fs");
|
|
3065
3065
|
var import_promises7 = require("fs/promises");
|
|
3066
3066
|
var import_http = require("http");
|
|
3067
|
-
var
|
|
3067
|
+
var import_os10 = require("os");
|
|
3068
3068
|
var import_path18 = require("path");
|
|
3069
3069
|
var import_readline = require("readline");
|
|
3070
3070
|
|
|
@@ -3324,7 +3324,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
3324
3324
|
}
|
|
3325
3325
|
|
|
3326
3326
|
// src/api/app.ts
|
|
3327
|
-
var
|
|
3327
|
+
var import_hono18 = require("hono");
|
|
3328
3328
|
|
|
3329
3329
|
// src/db/repositories/devices.repository.ts
|
|
3330
3330
|
var import_crypto5 = require("crypto");
|
|
@@ -3644,12 +3644,222 @@ var errorMiddleware = (err, c) => {
|
|
|
3644
3644
|
return c.json({ error: message }, 500);
|
|
3645
3645
|
};
|
|
3646
3646
|
|
|
3647
|
-
// src/api/routes/
|
|
3647
|
+
// src/api/routes/backup.routes.ts
|
|
3648
3648
|
var import_hono2 = require("hono");
|
|
3649
|
+
var import_os5 = require("os");
|
|
3650
|
+
|
|
3651
|
+
// src/services/backup/backup.ts
|
|
3652
|
+
var BACKUP_FORMAT_VERSION = 1;
|
|
3653
|
+
var BackupError = class extends Error {
|
|
3654
|
+
constructor(message, code) {
|
|
3655
|
+
super(message);
|
|
3656
|
+
this.code = code;
|
|
3657
|
+
}
|
|
3658
|
+
code;
|
|
3659
|
+
};
|
|
3660
|
+
function validateArchive(input) {
|
|
3661
|
+
if (!input || typeof input !== "object") {
|
|
3662
|
+
throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
|
|
3663
|
+
}
|
|
3664
|
+
const archive = input;
|
|
3665
|
+
const manifest = archive.manifest;
|
|
3666
|
+
if (!manifest || typeof manifest !== "object") {
|
|
3667
|
+
throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
|
|
3668
|
+
}
|
|
3669
|
+
if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
|
|
3670
|
+
throw new BackupError(
|
|
3671
|
+
`Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
|
|
3672
|
+
"UNSUPPORTED_VERSION"
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
if (!Array.isArray(archive.projects)) {
|
|
3676
|
+
throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
|
|
3677
|
+
}
|
|
3678
|
+
for (const [i, p] of archive.projects.entries()) {
|
|
3679
|
+
if (!p || typeof p !== "object") {
|
|
3680
|
+
throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
|
|
3681
|
+
}
|
|
3682
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
3683
|
+
throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
|
|
3684
|
+
}
|
|
3685
|
+
if (typeof p.path !== "string" || p.path.length === 0) {
|
|
3686
|
+
throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
const ids = new Set(archive.projects.map((p) => p.id));
|
|
3690
|
+
if (ids.size !== archive.projects.length) {
|
|
3691
|
+
throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
|
|
3692
|
+
}
|
|
3693
|
+
return archive;
|
|
3694
|
+
}
|
|
3695
|
+
function remapPaths(projects, rules) {
|
|
3696
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
3697
|
+
return projects.map((p) => {
|
|
3698
|
+
const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
|
|
3699
|
+
if (!rule) return p;
|
|
3700
|
+
return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
function planRestore(incoming, existing) {
|
|
3704
|
+
const byId = new Map(existing.map((e) => [e.id, e]));
|
|
3705
|
+
const byPath = new Map(existing.map((e) => [e.path, e]));
|
|
3706
|
+
const plan = { create: [], update: [], conflict: [] };
|
|
3707
|
+
for (const p of incoming) {
|
|
3708
|
+
const sameId = byId.get(p.id);
|
|
3709
|
+
if (sameId) {
|
|
3710
|
+
if (sameId.path !== p.path) plan.update.push(p);
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
const samePath = byPath.get(p.path);
|
|
3714
|
+
if (samePath) {
|
|
3715
|
+
plan.conflict.push({ incoming: p, existingId: samePath.id });
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
plan.create.push(p);
|
|
3719
|
+
}
|
|
3720
|
+
return plan;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
// src/version.ts
|
|
3724
|
+
var import_node_fs2 = require("fs");
|
|
3725
|
+
var import_node_path3 = require("path");
|
|
3726
|
+
var cached;
|
|
3727
|
+
function getVersion() {
|
|
3728
|
+
if (cached !== void 0) return cached;
|
|
3729
|
+
cached = resolveVersion();
|
|
3730
|
+
return cached;
|
|
3731
|
+
}
|
|
3732
|
+
function resolveVersion() {
|
|
3733
|
+
const scriptPath = process.argv[1] ?? "";
|
|
3734
|
+
const here = scriptPath ? (0, import_node_path3.dirname)(scriptPath) : process.cwd();
|
|
3735
|
+
let realHere = here;
|
|
3736
|
+
try {
|
|
3737
|
+
realHere = (0, import_node_path3.dirname)((0, import_node_fs2.realpathSync)(scriptPath));
|
|
3738
|
+
} catch {
|
|
3739
|
+
}
|
|
3740
|
+
const searchDirs = realHere === here ? [here, (0, import_node_path3.join)(here, "..")] : [here, (0, import_node_path3.join)(here, ".."), realHere, (0, import_node_path3.join)(realHere, "..")];
|
|
3741
|
+
for (const dir of searchDirs) {
|
|
3742
|
+
try {
|
|
3743
|
+
const v = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(dir, "version.txt"), "utf8").trim();
|
|
3744
|
+
if (v) return v;
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
try {
|
|
3749
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(here, "..", "package.json"), "utf8"));
|
|
3750
|
+
if (pkg.version) return `${pkg.version}+source`;
|
|
3751
|
+
} catch {
|
|
3752
|
+
}
|
|
3753
|
+
return "0.0.0+unknown";
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
// src/api/routes/backup.routes.ts
|
|
3757
|
+
function readBody(c) {
|
|
3758
|
+
return new Promise((resolve2, reject) => {
|
|
3759
|
+
const chunks = [];
|
|
3760
|
+
c.env.incoming.on("data", (chunk) => chunks.push(chunk));
|
|
3761
|
+
c.env.incoming.on("end", () => {
|
|
3762
|
+
try {
|
|
3763
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3764
|
+
resolve2(raw ? JSON.parse(raw) : {});
|
|
3765
|
+
} catch {
|
|
3766
|
+
reject(new Error("Invalid JSON body"));
|
|
3767
|
+
}
|
|
3768
|
+
});
|
|
3769
|
+
c.env.incoming.on("error", reject);
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
3772
|
+
var createBackupRoutes = (deps) => {
|
|
3773
|
+
const app = new import_hono2.Hono();
|
|
3774
|
+
app.get("/export", (c) => {
|
|
3775
|
+
const repo = deps.projectsRepo();
|
|
3776
|
+
if (!repo) {
|
|
3777
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3778
|
+
}
|
|
3779
|
+
const projects = repo.listProjects().map((p) => ({
|
|
3780
|
+
id: p.id,
|
|
3781
|
+
path: p.path,
|
|
3782
|
+
name: p.name ?? null,
|
|
3783
|
+
createdAt: p.createdAt,
|
|
3784
|
+
updatedAt: p.updatedAt
|
|
3785
|
+
}));
|
|
3786
|
+
return c.json({
|
|
3787
|
+
manifest: {
|
|
3788
|
+
formatVersion: BACKUP_FORMAT_VERSION,
|
|
3789
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3790
|
+
streamerVersion: getVersion(),
|
|
3791
|
+
sourceHost: (0, import_os5.hostname)(),
|
|
3792
|
+
// No endpoint here exports the API key. The flag is recorded so an
|
|
3793
|
+
// archive is self-describing about its own sensitivity rather than
|
|
3794
|
+
// requiring a reader to infer it.
|
|
3795
|
+
includesSecrets: false,
|
|
3796
|
+
counts: { projects: projects.length }
|
|
3797
|
+
},
|
|
3798
|
+
projects
|
|
3799
|
+
});
|
|
3800
|
+
});
|
|
3801
|
+
app.post("/restore", async (c) => {
|
|
3802
|
+
const repo = deps.projectsRepo();
|
|
3803
|
+
if (!repo) {
|
|
3804
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3805
|
+
}
|
|
3806
|
+
let body;
|
|
3807
|
+
try {
|
|
3808
|
+
body = await readBody(c);
|
|
3809
|
+
} catch {
|
|
3810
|
+
return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
|
|
3811
|
+
}
|
|
3812
|
+
let archive;
|
|
3813
|
+
try {
|
|
3814
|
+
archive = validateArchive(body.archive);
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
if (err instanceof BackupError) {
|
|
3817
|
+
return c.json({ error: err.message, code: err.code }, 400);
|
|
3818
|
+
}
|
|
3819
|
+
throw err;
|
|
3820
|
+
}
|
|
3821
|
+
const rules = Array.isArray(body.pathMap) ? body.pathMap.filter((r) => typeof r?.from === "string" && typeof r?.to === "string").map((r) => ({ from: r.from, to: r.to })) : [];
|
|
3822
|
+
const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
|
|
3823
|
+
const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
|
|
3824
|
+
const plan = planRestore(incoming, existing);
|
|
3825
|
+
const summary = {
|
|
3826
|
+
create: plan.create.length,
|
|
3827
|
+
update: plan.update.length,
|
|
3828
|
+
conflict: plan.conflict.length
|
|
3829
|
+
};
|
|
3830
|
+
if (body.apply !== true) {
|
|
3831
|
+
return c.json({ applied: false, summary, plan });
|
|
3832
|
+
}
|
|
3833
|
+
if (plan.conflict.length > 0) {
|
|
3834
|
+
return c.json(
|
|
3835
|
+
{
|
|
3836
|
+
error: "Restore has unresolved conflicts",
|
|
3837
|
+
code: "RESTORE_CONFLICT",
|
|
3838
|
+
summary,
|
|
3839
|
+
plan
|
|
3840
|
+
},
|
|
3841
|
+
409
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
let applied = 0;
|
|
3845
|
+
for (const p of [...plan.create, ...plan.update]) {
|
|
3846
|
+
try {
|
|
3847
|
+
repo.upsertProjectByPath(p.path, { name: p.name });
|
|
3848
|
+
applied++;
|
|
3849
|
+
} catch {
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
return c.json({ applied: true, summary, appliedCount: applied });
|
|
3853
|
+
});
|
|
3854
|
+
return app;
|
|
3855
|
+
};
|
|
3856
|
+
|
|
3857
|
+
// src/api/routes/browse.routes.ts
|
|
3858
|
+
var import_hono3 = require("hono");
|
|
3649
3859
|
var ALREADY_HANDLED = 597;
|
|
3650
3860
|
var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
|
|
3651
3861
|
var createBrowseRoutes = (deps) => {
|
|
3652
|
-
const app = new
|
|
3862
|
+
const app = new import_hono3.Hono();
|
|
3653
3863
|
app.get("/browse", async (c) => {
|
|
3654
3864
|
const url = new URL(c.req.url);
|
|
3655
3865
|
await deps.handleBrowse(url, c.env.outgoing);
|
|
@@ -3663,7 +3873,7 @@ var createBrowseRoutes = (deps) => {
|
|
|
3663
3873
|
};
|
|
3664
3874
|
|
|
3665
3875
|
// src/api/routes/cacheAlert.routes.ts
|
|
3666
|
-
var
|
|
3876
|
+
var import_hono4 = require("hono");
|
|
3667
3877
|
|
|
3668
3878
|
// src/schemas/cacheAlert.schema.ts
|
|
3669
3879
|
var import_zod = require("zod");
|
|
@@ -3686,7 +3896,7 @@ function readRawBody2(req) {
|
|
|
3686
3896
|
});
|
|
3687
3897
|
}
|
|
3688
3898
|
var createCacheAlertRoutes = (deps) => {
|
|
3689
|
-
const app = new
|
|
3899
|
+
const app = new import_hono4.Hono();
|
|
3690
3900
|
app.get("/", (c) => {
|
|
3691
3901
|
const monitor = deps.cacheMonitor();
|
|
3692
3902
|
return c.json({ pending: monitor?.pending ?? null });
|
|
@@ -3723,7 +3933,7 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
3723
3933
|
};
|
|
3724
3934
|
|
|
3725
3935
|
// src/api/routes/config.routes.ts
|
|
3726
|
-
var
|
|
3936
|
+
var import_hono5 = require("hono");
|
|
3727
3937
|
|
|
3728
3938
|
// src/schemas/claudeFlags.schema.ts
|
|
3729
3939
|
var import_zod2 = require("zod");
|
|
@@ -3744,7 +3954,7 @@ function readRawBody3(req) {
|
|
|
3744
3954
|
});
|
|
3745
3955
|
}
|
|
3746
3956
|
var createConfigRoutes = (deps) => {
|
|
3747
|
-
const app = new
|
|
3957
|
+
const app = new import_hono5.Hono();
|
|
3748
3958
|
app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
|
|
3749
3959
|
app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
|
|
3750
3960
|
app.put("/claude-flags", async (c) => {
|
|
@@ -3779,11 +3989,11 @@ var createConfigRoutes = (deps) => {
|
|
|
3779
3989
|
};
|
|
3780
3990
|
|
|
3781
3991
|
// src/api/routes/conversations.routes.ts
|
|
3782
|
-
var
|
|
3992
|
+
var import_hono6 = require("hono");
|
|
3783
3993
|
var ALREADY_HANDLED2 = 597;
|
|
3784
3994
|
var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
|
|
3785
3995
|
var createConversationRoutes = (deps) => {
|
|
3786
|
-
const app = new
|
|
3996
|
+
const app = new import_hono6.Hono();
|
|
3787
3997
|
app.get("/count", async (c) => {
|
|
3788
3998
|
const url = new URL(c.req.url);
|
|
3789
3999
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
@@ -3810,9 +4020,9 @@ var createConversationRoutes = (deps) => {
|
|
|
3810
4020
|
};
|
|
3811
4021
|
|
|
3812
4022
|
// src/api/routes/devices.routes.ts
|
|
3813
|
-
var
|
|
4023
|
+
var import_hono7 = require("hono");
|
|
3814
4024
|
var createDeviceRoutes = (deps) => {
|
|
3815
|
-
const app = new
|
|
4025
|
+
const app = new import_hono7.Hono();
|
|
3816
4026
|
app.get("/", (c) => {
|
|
3817
4027
|
const repo = deps.devicesRepo();
|
|
3818
4028
|
if (!repo) return c.json({ devices: [], available: false });
|
|
@@ -3835,45 +4045,134 @@ var createDeviceRoutes = (deps) => {
|
|
|
3835
4045
|
return app;
|
|
3836
4046
|
};
|
|
3837
4047
|
|
|
3838
|
-
// src/api/routes/
|
|
3839
|
-
var
|
|
4048
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4049
|
+
var import_fs7 = require("fs");
|
|
4050
|
+
var import_hono8 = require("hono");
|
|
3840
4051
|
|
|
3841
|
-
// src/
|
|
3842
|
-
var
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
if (
|
|
3847
|
-
|
|
3848
|
-
|
|
4052
|
+
// src/services/diagnostics/diagnostics.ts
|
|
4053
|
+
var DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
4054
|
+
function redactPath(path) {
|
|
4055
|
+
if (!path) return null;
|
|
4056
|
+
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
4057
|
+
if (parts.length <= 2) return parts.join("/");
|
|
4058
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
4059
|
+
}
|
|
4060
|
+
function worstStatus(checks) {
|
|
4061
|
+
const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
|
|
4062
|
+
return checks.reduce(
|
|
4063
|
+
(worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
|
|
4064
|
+
"ok"
|
|
4065
|
+
);
|
|
3849
4066
|
}
|
|
3850
|
-
function
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
}
|
|
4067
|
+
function buildReport(checks, now = /* @__PURE__ */ new Date()) {
|
|
4068
|
+
return {
|
|
4069
|
+
contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
|
|
4070
|
+
generatedAt: now.toISOString(),
|
|
4071
|
+
overall: worstStatus(checks),
|
|
4072
|
+
checks
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
|
|
4076
|
+
function redactValue(value) {
|
|
4077
|
+
if (Array.isArray(value)) {
|
|
4078
|
+
return value.map((v) => redactValue(v));
|
|
3857
4079
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
if (v) return v;
|
|
3863
|
-
} catch {
|
|
4080
|
+
if (value && typeof value === "object") {
|
|
4081
|
+
const out = {};
|
|
4082
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4083
|
+
out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
|
|
3864
4084
|
}
|
|
4085
|
+
return out;
|
|
3865
4086
|
}
|
|
4087
|
+
return value;
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4091
|
+
function providerCheck(name, resolve2) {
|
|
3866
4092
|
try {
|
|
3867
|
-
const
|
|
3868
|
-
|
|
4093
|
+
const exe = resolve2();
|
|
4094
|
+
return {
|
|
4095
|
+
id: `provider:${name}`,
|
|
4096
|
+
status: "ok",
|
|
4097
|
+
summary: `${name} CLI is installed.`,
|
|
4098
|
+
remediation: "NONE",
|
|
4099
|
+
detail: { location: redactPath(exe) }
|
|
4100
|
+
};
|
|
3869
4101
|
} catch {
|
|
4102
|
+
return {
|
|
4103
|
+
id: `provider:${name}`,
|
|
4104
|
+
status: "failed",
|
|
4105
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4106
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4107
|
+
};
|
|
3870
4108
|
}
|
|
3871
|
-
return "0.0.0+unknown";
|
|
3872
4109
|
}
|
|
4110
|
+
var createDiagnosticsRoutes = (deps) => {
|
|
4111
|
+
const app = new import_hono8.Hono();
|
|
4112
|
+
app.get("/", (c) => {
|
|
4113
|
+
const checks = [];
|
|
4114
|
+
checks.push({
|
|
4115
|
+
id: "streamer",
|
|
4116
|
+
status: "ok",
|
|
4117
|
+
summary: "Streamer is running.",
|
|
4118
|
+
remediation: "NONE",
|
|
4119
|
+
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4120
|
+
});
|
|
4121
|
+
checks.push(providerCheck("claude-code", resolveClaudeExe));
|
|
4122
|
+
checks.push(providerCheck("codex-cli", resolveCodexExe));
|
|
4123
|
+
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4124
|
+
checks.push(
|
|
4125
|
+
cacheAlert ? {
|
|
4126
|
+
id: "cache",
|
|
4127
|
+
status: "degraded",
|
|
4128
|
+
summary: "Conversation cache reported an integrity alert.",
|
|
4129
|
+
remediation: "CACHE_DEGRADED"
|
|
4130
|
+
} : {
|
|
4131
|
+
id: "cache",
|
|
4132
|
+
status: "ok",
|
|
4133
|
+
summary: "Conversation cache is healthy.",
|
|
4134
|
+
remediation: "NONE"
|
|
4135
|
+
}
|
|
4136
|
+
);
|
|
4137
|
+
let ptyOk = true;
|
|
4138
|
+
try {
|
|
4139
|
+
require.resolve("node-pty");
|
|
4140
|
+
} catch {
|
|
4141
|
+
ptyOk = false;
|
|
4142
|
+
}
|
|
4143
|
+
checks.push(
|
|
4144
|
+
ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
|
|
4145
|
+
id: "pty",
|
|
4146
|
+
status: "failed",
|
|
4147
|
+
summary: "node-pty failed to load, so no managed session can start.",
|
|
4148
|
+
remediation: "PTY_UNAVAILABLE"
|
|
4149
|
+
}
|
|
4150
|
+
);
|
|
4151
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4152
|
+
const claudeProjects = home ? `${home}/.claude/projects` : "";
|
|
4153
|
+
checks.push(
|
|
4154
|
+
claudeProjects && (0, import_fs7.existsSync)(claudeProjects) ? {
|
|
4155
|
+
id: "filesystem",
|
|
4156
|
+
status: "ok",
|
|
4157
|
+
summary: "Provider history directory is present.",
|
|
4158
|
+
remediation: "NONE",
|
|
4159
|
+
detail: { location: redactPath(claudeProjects) }
|
|
4160
|
+
} : {
|
|
4161
|
+
id: "filesystem",
|
|
4162
|
+
status: "degraded",
|
|
4163
|
+
summary: "Provider history directory was not found; history may be unavailable.",
|
|
4164
|
+
remediation: "FS_SCOPE_MISSING"
|
|
4165
|
+
}
|
|
4166
|
+
);
|
|
4167
|
+
return c.json(redactValue(buildReport(checks)));
|
|
4168
|
+
});
|
|
4169
|
+
return app;
|
|
4170
|
+
};
|
|
3873
4171
|
|
|
3874
4172
|
// src/api/routes/health.routes.ts
|
|
4173
|
+
var import_hono9 = require("hono");
|
|
3875
4174
|
var createHealthRoutes = (deps) => {
|
|
3876
|
-
const app = new
|
|
4175
|
+
const app = new import_hono9.Hono();
|
|
3877
4176
|
app.get("/", (c) => {
|
|
3878
4177
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
3879
4178
|
return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
|
|
@@ -3884,7 +4183,7 @@ var createHealthRoutes = (deps) => {
|
|
|
3884
4183
|
// src/api/routes/logs.routes.ts
|
|
3885
4184
|
var import_node_fs3 = require("fs");
|
|
3886
4185
|
var import_node_path5 = require("path");
|
|
3887
|
-
var
|
|
4186
|
+
var import_hono10 = require("hono");
|
|
3888
4187
|
|
|
3889
4188
|
// src/lifecycle/constants.ts
|
|
3890
4189
|
var import_node_os = require("os");
|
|
@@ -3942,7 +4241,7 @@ function readLogLines(filePath, sinceOffset, limit) {
|
|
|
3942
4241
|
}
|
|
3943
4242
|
}
|
|
3944
4243
|
function createLogsRoutes() {
|
|
3945
|
-
const app = new
|
|
4244
|
+
const app = new import_hono10.Hono();
|
|
3946
4245
|
app.get("/", (c) => {
|
|
3947
4246
|
try {
|
|
3948
4247
|
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
@@ -4013,8 +4312,8 @@ function createLogsRoutes() {
|
|
|
4013
4312
|
// src/api/routes/misc.routes.ts
|
|
4014
4313
|
var import_node_child_process = require("child_process");
|
|
4015
4314
|
var import_node_crypto2 = require("crypto");
|
|
4016
|
-
var
|
|
4017
|
-
var
|
|
4315
|
+
var import_hono11 = require("hono");
|
|
4316
|
+
var import_os6 = require("os");
|
|
4018
4317
|
|
|
4019
4318
|
// src/config/update-config.ts
|
|
4020
4319
|
var import_node_fs4 = require("fs");
|
|
@@ -4344,12 +4643,12 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
4344
4643
|
}
|
|
4345
4644
|
var clientLog = getLogger("client");
|
|
4346
4645
|
var createMiscRoutes = (deps) => {
|
|
4347
|
-
const app = new
|
|
4646
|
+
const app = new import_hono11.Hono();
|
|
4348
4647
|
app.get("/api/info", (c) => {
|
|
4349
4648
|
const ptyIds = deps.ptyAttachedIds();
|
|
4350
4649
|
return c.json({
|
|
4351
4650
|
version: getVersion(),
|
|
4352
|
-
machineName: (0,
|
|
4651
|
+
machineName: (0, import_os6.hostname)(),
|
|
4353
4652
|
platform: process.platform,
|
|
4354
4653
|
activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
|
|
4355
4654
|
publicUrl: deps.publicUrl,
|
|
@@ -4475,11 +4774,11 @@ var createMiscRoutes = (deps) => {
|
|
|
4475
4774
|
};
|
|
4476
4775
|
|
|
4477
4776
|
// src/api/routes/pair.routes.ts
|
|
4478
|
-
var
|
|
4777
|
+
var import_hono12 = require("hono");
|
|
4479
4778
|
var ALREADY_HANDLED3 = 597;
|
|
4480
4779
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
4481
4780
|
var createPairRoutes = (deps) => {
|
|
4482
|
-
const app = new
|
|
4781
|
+
const app = new import_hono12.Hono();
|
|
4483
4782
|
app.post("/start", (c) => {
|
|
4484
4783
|
deps.handlePairStart(c.env.outgoing);
|
|
4485
4784
|
return alreadyHandled3();
|
|
@@ -4492,11 +4791,11 @@ var createPairRoutes = (deps) => {
|
|
|
4492
4791
|
};
|
|
4493
4792
|
|
|
4494
4793
|
// src/api/routes/projects.routes.ts
|
|
4495
|
-
var
|
|
4794
|
+
var import_hono13 = require("hono");
|
|
4496
4795
|
var ALREADY_HANDLED4 = 597;
|
|
4497
4796
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
4498
4797
|
var createProjectRoutes = (deps) => {
|
|
4499
|
-
const app = new
|
|
4798
|
+
const app = new import_hono13.Hono();
|
|
4500
4799
|
app.get("/", (c) => {
|
|
4501
4800
|
const url = new URL(c.req.url);
|
|
4502
4801
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -4511,7 +4810,7 @@ var createProjectRoutes = (deps) => {
|
|
|
4511
4810
|
};
|
|
4512
4811
|
|
|
4513
4812
|
// src/api/routes/providers.routes.ts
|
|
4514
|
-
var
|
|
4813
|
+
var import_hono14 = require("hono");
|
|
4515
4814
|
|
|
4516
4815
|
// src/services/providers/providerHealth.ts
|
|
4517
4816
|
var import_child_process3 = require("child_process");
|
|
@@ -4634,7 +4933,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
|
|
|
4634
4933
|
|
|
4635
4934
|
// src/api/routes/providers.routes.ts
|
|
4636
4935
|
var createProviderRoutes = () => {
|
|
4637
|
-
const app = new
|
|
4936
|
+
const app = new import_hono14.Hono();
|
|
4638
4937
|
app.get("/", async (c) => {
|
|
4639
4938
|
const providers = await Promise.all([
|
|
4640
4939
|
providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
|
|
@@ -4646,11 +4945,11 @@ var createProviderRoutes = () => {
|
|
|
4646
4945
|
};
|
|
4647
4946
|
|
|
4648
4947
|
// src/api/routes/scanner.routes.ts
|
|
4649
|
-
var
|
|
4948
|
+
var import_hono15 = require("hono");
|
|
4650
4949
|
var ALREADY_HANDLED5 = 597;
|
|
4651
4950
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
4652
4951
|
var createScannerRoutes = (deps) => {
|
|
4653
|
-
const app = new
|
|
4952
|
+
const app = new import_hono15.Hono();
|
|
4654
4953
|
app.get("/api/search", async (c) => {
|
|
4655
4954
|
const url = new URL(c.req.url);
|
|
4656
4955
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -4660,11 +4959,11 @@ var createScannerRoutes = (deps) => {
|
|
|
4660
4959
|
};
|
|
4661
4960
|
|
|
4662
4961
|
// src/api/routes/sessions.routes.ts
|
|
4663
|
-
var
|
|
4962
|
+
var import_hono16 = require("hono");
|
|
4664
4963
|
var ALREADY_HANDLED6 = 597;
|
|
4665
4964
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
4666
4965
|
var createSessionRoutes = (deps) => {
|
|
4667
|
-
const app = new
|
|
4966
|
+
const app = new import_hono16.Hono();
|
|
4668
4967
|
app.get("/count", (c) => {
|
|
4669
4968
|
deps.handleSessionsCount(c.env.outgoing);
|
|
4670
4969
|
return alreadyHandled6();
|
|
@@ -4739,9 +5038,9 @@ var createSessionRoutes = (deps) => {
|
|
|
4739
5038
|
};
|
|
4740
5039
|
|
|
4741
5040
|
// src/api/routes/ws.routes.ts
|
|
4742
|
-
var
|
|
5041
|
+
var import_hono17 = require("hono");
|
|
4743
5042
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
4744
|
-
const app = new
|
|
5043
|
+
const app = new import_hono17.Hono();
|
|
4745
5044
|
app.get(
|
|
4746
5045
|
"/ws",
|
|
4747
5046
|
upgradeWebSocket(() => {
|
|
@@ -4767,7 +5066,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
4767
5066
|
|
|
4768
5067
|
// src/api/app.ts
|
|
4769
5068
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
4770
|
-
const app = new
|
|
5069
|
+
const app = new import_hono18.Hono();
|
|
4771
5070
|
const httpLog = getLogger("http");
|
|
4772
5071
|
app.use("*", async (c, next) => {
|
|
4773
5072
|
const start = Date.now();
|
|
@@ -4788,6 +5087,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4788
5087
|
app.use("*", authMiddleware(deps));
|
|
4789
5088
|
app.onError(errorMiddleware);
|
|
4790
5089
|
app.route("/healthz", createHealthRoutes(deps));
|
|
5090
|
+
app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
|
|
4791
5091
|
app.route("/", createMiscRoutes(deps));
|
|
4792
5092
|
app.route("/api/sessions", createSessionRoutes(deps));
|
|
4793
5093
|
app.route("/api/conversations", createConversationRoutes(deps));
|
|
@@ -4796,6 +5096,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4796
5096
|
app.route("/api/projects", createProjectRoutes(deps));
|
|
4797
5097
|
app.route("/api/providers", createProviderRoutes());
|
|
4798
5098
|
app.route("/api/devices", createDeviceRoutes(deps));
|
|
5099
|
+
app.route("/api/backup", createBackupRoutes(deps));
|
|
4799
5100
|
app.route("/api/pair", createPairRoutes(deps));
|
|
4800
5101
|
app.route("/api", createBrowseRoutes(deps));
|
|
4801
5102
|
app.route("/", createScannerRoutes(deps));
|
|
@@ -4861,13 +5162,13 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
4861
5162
|
// src/conversation-cache.ts
|
|
4862
5163
|
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
4863
5164
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
4864
|
-
var
|
|
5165
|
+
var import_fs10 = require("fs");
|
|
4865
5166
|
var import_promises3 = require("fs/promises");
|
|
4866
5167
|
var import_path12 = require("path");
|
|
4867
5168
|
var import_promises4 = require("timers/promises");
|
|
4868
5169
|
|
|
4869
5170
|
// src/db/sqlite-migrate.ts
|
|
4870
|
-
var
|
|
5171
|
+
var import_fs8 = require("fs");
|
|
4871
5172
|
var import_path10 = require("path");
|
|
4872
5173
|
var import_url2 = require("url");
|
|
4873
5174
|
var import_meta2 = {};
|
|
@@ -4886,7 +5187,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
4886
5187
|
function runSqliteMigrations(db, migrationsDir) {
|
|
4887
5188
|
db.exec(SCHEMA_MIGRATIONS_SQL);
|
|
4888
5189
|
const dir = migrationsDir ?? (0, import_path10.join)(getMigrationsDir2(), "migrations");
|
|
4889
|
-
const files = (0,
|
|
5190
|
+
const files = (0, import_fs8.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
4890
5191
|
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
4891
5192
|
const appliedSet = new Set(appliedRows.map((r) => r.id));
|
|
4892
5193
|
const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
@@ -4897,7 +5198,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
4897
5198
|
skipped.push(file);
|
|
4898
5199
|
continue;
|
|
4899
5200
|
}
|
|
4900
|
-
const sql = (0,
|
|
5201
|
+
const sql = (0, import_fs8.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
|
|
4901
5202
|
const tx = db.transaction(() => {
|
|
4902
5203
|
db.exec(sql);
|
|
4903
5204
|
recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -4909,7 +5210,7 @@ function runSqliteMigrations(db, migrationsDir) {
|
|
|
4909
5210
|
}
|
|
4910
5211
|
|
|
4911
5212
|
// src/services/conversations/isAgentConversation.ts
|
|
4912
|
-
var
|
|
5213
|
+
var import_fs9 = require("fs");
|
|
4913
5214
|
var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
|
|
4914
5215
|
var CHUNK_BYTES = 64 * 1024;
|
|
4915
5216
|
var ENTRYPOINT_PROBE = `"entrypoint":`;
|
|
@@ -4931,12 +5232,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4931
5232
|
if (cached2 !== void 0) return cached2;
|
|
4932
5233
|
let fd;
|
|
4933
5234
|
try {
|
|
4934
|
-
fd = (0,
|
|
5235
|
+
fd = (0, import_fs9.openSync)(filePath, "r");
|
|
4935
5236
|
} catch {
|
|
4936
5237
|
return false;
|
|
4937
5238
|
}
|
|
4938
5239
|
try {
|
|
4939
|
-
const fileSize = (0,
|
|
5240
|
+
const fileSize = (0, import_fs9.statSync)(filePath).size;
|
|
4940
5241
|
if (fileSize === 0) {
|
|
4941
5242
|
fileDecisionCache.set(key, false);
|
|
4942
5243
|
return false;
|
|
@@ -4947,7 +5248,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4947
5248
|
let carry = "";
|
|
4948
5249
|
while (offset < fileSize) {
|
|
4949
5250
|
const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
|
|
4950
|
-
const got = (0,
|
|
5251
|
+
const got = (0, import_fs9.readSync)(fd, buf, 0, toRead, offset);
|
|
4951
5252
|
if (got <= 0) break;
|
|
4952
5253
|
const chunk = carry + buf.toString("utf8", 0, got);
|
|
4953
5254
|
for (const marker of markers) {
|
|
@@ -4968,7 +5269,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
|
|
|
4968
5269
|
} catch {
|
|
4969
5270
|
return false;
|
|
4970
5271
|
} finally {
|
|
4971
|
-
(0,
|
|
5272
|
+
(0, import_fs9.closeSync)(fd);
|
|
4972
5273
|
}
|
|
4973
5274
|
}
|
|
4974
5275
|
function parseAgentEntrypointsEnv(raw) {
|
|
@@ -5543,7 +5844,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5543
5844
|
if (!fileState) return null;
|
|
5544
5845
|
let stat3;
|
|
5545
5846
|
try {
|
|
5546
|
-
stat3 = (0,
|
|
5847
|
+
stat3 = (0, import_fs10.statSync)(filePath);
|
|
5547
5848
|
} catch {
|
|
5548
5849
|
return null;
|
|
5549
5850
|
}
|
|
@@ -5564,17 +5865,17 @@ var ConversationCache = class _ConversationCache {
|
|
|
5564
5865
|
);
|
|
5565
5866
|
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
5566
5867
|
const messages = [];
|
|
5567
|
-
const fd = (0,
|
|
5868
|
+
const fd = (0, import_fs10.openSync)(filePath, "r");
|
|
5568
5869
|
try {
|
|
5569
5870
|
const state = (0, import_scanner2.createJsonlParseState)();
|
|
5570
5871
|
for (const row of rows) {
|
|
5571
5872
|
const buf = Buffer.alloc(row.byte_length);
|
|
5572
|
-
(0,
|
|
5873
|
+
(0, import_fs10.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
5573
5874
|
const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
|
|
5574
5875
|
if (msg) messages.push(msg);
|
|
5575
5876
|
}
|
|
5576
5877
|
} finally {
|
|
5577
|
-
(0,
|
|
5878
|
+
(0, import_fs10.closeSync)(fd);
|
|
5578
5879
|
}
|
|
5579
5880
|
return { messages, total, fromIndex: from };
|
|
5580
5881
|
}
|
|
@@ -5602,14 +5903,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
5602
5903
|
isAgentFileCached(filePath) {
|
|
5603
5904
|
let s;
|
|
5604
5905
|
try {
|
|
5605
|
-
s = (0,
|
|
5906
|
+
s = (0, import_fs10.statSync)(filePath);
|
|
5606
5907
|
} catch {
|
|
5607
5908
|
return false;
|
|
5608
5909
|
}
|
|
5609
5910
|
return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
|
|
5610
5911
|
}
|
|
5611
5912
|
static open(dbPath, tailSize = 10, migrationsDir, options) {
|
|
5612
|
-
(0,
|
|
5913
|
+
(0, import_fs10.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
|
|
5613
5914
|
const db = new import_better_sqlite3.default(dbPath);
|
|
5614
5915
|
db.pragma("journal_mode = WAL");
|
|
5615
5916
|
db.pragma("foreign_keys = ON");
|
|
@@ -5796,7 +6097,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5796
6097
|
let mtimeMs = null;
|
|
5797
6098
|
let fileSize = null;
|
|
5798
6099
|
try {
|
|
5799
|
-
const s = (0,
|
|
6100
|
+
const s = (0, import_fs10.statSync)(m.filePath);
|
|
5800
6101
|
mtimeMs = s.mtimeMs;
|
|
5801
6102
|
fileSize = s.size;
|
|
5802
6103
|
} catch {
|
|
@@ -5856,8 +6157,8 @@ var ConversationCache = class _ConversationCache {
|
|
|
5856
6157
|
let fileSize;
|
|
5857
6158
|
let fd;
|
|
5858
6159
|
try {
|
|
5859
|
-
fileSize = (0,
|
|
5860
|
-
fd = (0,
|
|
6160
|
+
fileSize = (0, import_fs10.statSync)(filePath).size;
|
|
6161
|
+
fd = (0, import_fs10.openSync)(filePath, "r");
|
|
5861
6162
|
} catch {
|
|
5862
6163
|
return false;
|
|
5863
6164
|
}
|
|
@@ -5870,7 +6171,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5870
6171
|
while (pos > 0 && lines.length < this.tailSize * 4) {
|
|
5871
6172
|
const toRead = Math.min(CHUNK, pos);
|
|
5872
6173
|
pos -= toRead;
|
|
5873
|
-
(0,
|
|
6174
|
+
(0, import_fs10.readSync)(fd, buf, 0, toRead, pos);
|
|
5874
6175
|
const chunk = buf.subarray(0, toRead).toString("utf8");
|
|
5875
6176
|
const combined = chunk + partial;
|
|
5876
6177
|
const parts = combined.split("\n");
|
|
@@ -5881,7 +6182,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
5881
6182
|
}
|
|
5882
6183
|
if (partial) lines.push(partial);
|
|
5883
6184
|
} finally {
|
|
5884
|
-
(0,
|
|
6185
|
+
(0, import_fs10.closeSync)(fd);
|
|
5885
6186
|
}
|
|
5886
6187
|
const msgs = [];
|
|
5887
6188
|
for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
|
|
@@ -6094,7 +6395,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6094
6395
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
6095
6396
|
* JSONL has been deleted.
|
|
6096
6397
|
*/
|
|
6097
|
-
pruneGhostFiles(exists =
|
|
6398
|
+
pruneGhostFiles(exists = import_fs10.existsSync) {
|
|
6098
6399
|
const rows = this.stmts.allFilePaths.all();
|
|
6099
6400
|
const ghosts = [];
|
|
6100
6401
|
const prune = this.db.transaction((ids) => {
|
|
@@ -6149,7 +6450,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6149
6450
|
* Returns the removed IDs.
|
|
6150
6451
|
*/
|
|
6151
6452
|
reconcileDeletions(livePaths, opts) {
|
|
6152
|
-
const exists = opts?.exists ??
|
|
6453
|
+
const exists = opts?.exists ?? import_fs10.existsSync;
|
|
6153
6454
|
const rows = this.stmts.allFilePaths.all();
|
|
6154
6455
|
const removed = [];
|
|
6155
6456
|
const drop = this.db.transaction((ids) => {
|
|
@@ -6184,7 +6485,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6184
6485
|
* reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
|
|
6185
6486
|
* rows that still have cached history (which pruneGhostFiles would keep).
|
|
6186
6487
|
*/
|
|
6187
|
-
listMissingFiles(exists =
|
|
6488
|
+
listMissingFiles(exists = import_fs10.existsSync) {
|
|
6188
6489
|
const rows = this.stmts.allFilePathsWithTitle.all();
|
|
6189
6490
|
const missing = [];
|
|
6190
6491
|
for (const row of rows) {
|
|
@@ -6554,8 +6855,8 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
6554
6855
|
}
|
|
6555
6856
|
|
|
6556
6857
|
// src/handlers/handleListProjects.ts
|
|
6557
|
-
var
|
|
6558
|
-
var
|
|
6858
|
+
var import_fs11 = require("fs");
|
|
6859
|
+
var import_os7 = require("os");
|
|
6559
6860
|
var import_path13 = require("path");
|
|
6560
6861
|
function decodeProjectPath(dirName) {
|
|
6561
6862
|
return dirName.replace(/-/g, "/");
|
|
@@ -6563,14 +6864,14 @@ function decodeProjectPath(dirName) {
|
|
|
6563
6864
|
function handleListProjects(url, res) {
|
|
6564
6865
|
const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
|
|
6565
6866
|
const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
|
|
6566
|
-
const projectsDir = (0, import_path13.join)((0,
|
|
6867
|
+
const projectsDir = (0, import_path13.join)((0, import_os7.homedir)(), ".claude", "projects");
|
|
6567
6868
|
let entries;
|
|
6568
6869
|
try {
|
|
6569
|
-
entries = (0,
|
|
6870
|
+
entries = (0, import_fs11.readdirSync)(projectsDir).map((dirName) => {
|
|
6570
6871
|
const fullPath = (0, import_path13.join)(projectsDir, dirName);
|
|
6571
6872
|
let mtime = 0;
|
|
6572
6873
|
try {
|
|
6573
|
-
mtime = (0,
|
|
6874
|
+
mtime = (0, import_fs11.statSync)(fullPath).mtimeMs;
|
|
6574
6875
|
} catch {
|
|
6575
6876
|
}
|
|
6576
6877
|
const path = decodeProjectPath(dirName);
|
|
@@ -6685,19 +6986,19 @@ function setCacheMetadata(repo, key, value) {
|
|
|
6685
6986
|
|
|
6686
6987
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
6687
6988
|
var import_crypto9 = require("crypto");
|
|
6688
|
-
var
|
|
6989
|
+
var import_fs14 = require("fs");
|
|
6689
6990
|
|
|
6690
6991
|
// src/services/cache-integrity/alertStore.ts
|
|
6691
|
-
var
|
|
6692
|
-
var
|
|
6992
|
+
var import_fs12 = require("fs");
|
|
6993
|
+
var import_os8 = require("os");
|
|
6693
6994
|
var import_path14 = require("path");
|
|
6694
6995
|
function alertStatePath() {
|
|
6695
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0,
|
|
6996
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0, import_os8.homedir)(), ".threadbase");
|
|
6696
6997
|
return (0, import_path14.join)(dir, "cache-alert.json");
|
|
6697
6998
|
}
|
|
6698
6999
|
function loadAlertState() {
|
|
6699
7000
|
try {
|
|
6700
|
-
const parsed = JSON.parse((0,
|
|
7001
|
+
const parsed = JSON.parse((0, import_fs12.readFileSync)(alertStatePath(), "utf-8"));
|
|
6701
7002
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6702
7003
|
} catch {
|
|
6703
7004
|
return {};
|
|
@@ -6705,13 +7006,13 @@ function loadAlertState() {
|
|
|
6705
7006
|
}
|
|
6706
7007
|
function saveAlertState(state) {
|
|
6707
7008
|
const path = alertStatePath();
|
|
6708
|
-
(0,
|
|
6709
|
-
(0,
|
|
7009
|
+
(0, import_fs12.mkdirSync)((0, import_path14.dirname)(path), { recursive: true });
|
|
7010
|
+
(0, import_fs12.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
|
|
6710
7011
|
`);
|
|
6711
7012
|
}
|
|
6712
7013
|
|
|
6713
7014
|
// src/services/cache-integrity/backup.ts
|
|
6714
|
-
var
|
|
7015
|
+
var import_fs13 = require("fs");
|
|
6715
7016
|
var import_path15 = require("path");
|
|
6716
7017
|
var DEFAULT_RETAIN = 3;
|
|
6717
7018
|
function retainCount() {
|
|
@@ -6724,16 +7025,16 @@ function timestamp(d) {
|
|
|
6724
7025
|
}
|
|
6725
7026
|
async function backupCacheDb(db, cacheDir) {
|
|
6726
7027
|
const backupsDir = (0, import_path15.join)(cacheDir, "backups");
|
|
6727
|
-
(0,
|
|
7028
|
+
(0, import_fs13.mkdirSync)(backupsDir, { recursive: true });
|
|
6728
7029
|
const destPath = (0, import_path15.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
|
|
6729
7030
|
await db.backup(destPath);
|
|
6730
7031
|
const retain = retainCount();
|
|
6731
|
-
const backups = (0,
|
|
7032
|
+
const backups = (0, import_fs13.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
|
|
6732
7033
|
const full = (0, import_path15.join)(backupsDir, f);
|
|
6733
|
-
return { full, mtime: (0,
|
|
7034
|
+
return { full, mtime: (0, import_fs13.statSync)(full).mtimeMs };
|
|
6734
7035
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6735
7036
|
for (const stale of backups.slice(retain)) {
|
|
6736
|
-
if ((0,
|
|
7037
|
+
if ((0, import_fs13.existsSync)(stale.full)) (0, import_fs13.unlinkSync)(stale.full);
|
|
6737
7038
|
}
|
|
6738
7039
|
return destPath;
|
|
6739
7040
|
}
|
|
@@ -6830,7 +7131,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6830
7131
|
* the pending record, back up on high severity, and broadcast the alert.
|
|
6831
7132
|
*/
|
|
6832
7133
|
async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6833
|
-
const all = this.cache.listMissingFiles(
|
|
7134
|
+
const all = this.cache.listMissingFiles(import_fs14.existsSync);
|
|
6834
7135
|
const missing = all.filter((m) => !this.ignoredIds.has(m.id));
|
|
6835
7136
|
if (missing.length === 0) {
|
|
6836
7137
|
if (this._pending) {
|
|
@@ -6934,7 +7235,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6934
7235
|
case "prune_all": {
|
|
6935
7236
|
await this.ensureBackup(pending);
|
|
6936
7237
|
const backupPath = pending.backupPath;
|
|
6937
|
-
const stillMissing = pending.missing.filter((m) => !(0,
|
|
7238
|
+
const stillMissing = pending.missing.filter((m) => !(0, import_fs14.existsSync)(m.filePath)).map((m) => m.id);
|
|
6938
7239
|
const pruned = this.cache.dropRowsById(stillMissing);
|
|
6939
7240
|
this.applyDeferredUnlinks();
|
|
6940
7241
|
this.clearPending();
|
|
@@ -6996,7 +7297,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6996
7297
|
|
|
6997
7298
|
// src/services/conversations/conversationWatcher.ts
|
|
6998
7299
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
6999
|
-
var
|
|
7300
|
+
var import_fs15 = require("fs");
|
|
7000
7301
|
var import_promises5 = require("fs/promises");
|
|
7001
7302
|
var ConversationWatcher = class {
|
|
7002
7303
|
files = /* @__PURE__ */ new Map();
|
|
@@ -7022,7 +7323,7 @@ var ConversationWatcher = class {
|
|
|
7022
7323
|
if (this.files.has(key)) return;
|
|
7023
7324
|
let offset;
|
|
7024
7325
|
try {
|
|
7025
|
-
offset = (0,
|
|
7326
|
+
offset = (0, import_fs15.statSync)(filePath).size;
|
|
7026
7327
|
} catch {
|
|
7027
7328
|
offset = 0;
|
|
7028
7329
|
}
|
|
@@ -7204,14 +7505,14 @@ function findSearchTarget(messages, query) {
|
|
|
7204
7505
|
}
|
|
7205
7506
|
|
|
7206
7507
|
// src/services/conversations/pruneAgentConversations.ts
|
|
7207
|
-
var
|
|
7508
|
+
var import_fs16 = require("fs");
|
|
7208
7509
|
function pruneAgentConversations(cache) {
|
|
7209
7510
|
const db = cache.getDatabase();
|
|
7210
7511
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
7211
7512
|
let pruned = 0;
|
|
7212
7513
|
let missing = 0;
|
|
7213
7514
|
for (const row of rows) {
|
|
7214
|
-
if (!(0,
|
|
7515
|
+
if (!(0, import_fs16.existsSync)(row.file_path)) {
|
|
7215
7516
|
missing += 1;
|
|
7216
7517
|
continue;
|
|
7217
7518
|
}
|
|
@@ -7312,22 +7613,22 @@ function refreshConversationCache(deps) {
|
|
|
7312
7613
|
}
|
|
7313
7614
|
|
|
7314
7615
|
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
7315
|
-
var
|
|
7316
|
-
var
|
|
7616
|
+
var import_fs17 = require("fs");
|
|
7617
|
+
var import_os9 = require("os");
|
|
7317
7618
|
var import_path16 = require("path");
|
|
7318
|
-
var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0,
|
|
7619
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os9.homedir)(), ".claude", "projects");
|
|
7319
7620
|
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
7320
7621
|
let maxMs;
|
|
7321
7622
|
try {
|
|
7322
|
-
maxMs = (0,
|
|
7623
|
+
maxMs = (0, import_fs17.statSync)(projectsDir).mtimeMs;
|
|
7323
7624
|
} catch {
|
|
7324
7625
|
return null;
|
|
7325
7626
|
}
|
|
7326
7627
|
try {
|
|
7327
|
-
for (const ent of (0,
|
|
7628
|
+
for (const ent of (0, import_fs17.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
7328
7629
|
if (!ent.isDirectory()) continue;
|
|
7329
7630
|
try {
|
|
7330
|
-
const childMs = (0,
|
|
7631
|
+
const childMs = (0, import_fs17.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
|
|
7331
7632
|
if (childMs > maxMs) maxMs = childMs;
|
|
7332
7633
|
} catch {
|
|
7333
7634
|
}
|
|
@@ -8088,8 +8389,90 @@ function resolveAnswer(pending, body) {
|
|
|
8088
8389
|
}
|
|
8089
8390
|
}
|
|
8090
8391
|
|
|
8392
|
+
// src/services/search/searchQuery.ts
|
|
8393
|
+
var DEFAULT_SEARCH_LIMIT = 50;
|
|
8394
|
+
var MAX_SEARCH_LIMIT = 200;
|
|
8395
|
+
var MAX_QUERY_LENGTH = 256;
|
|
8396
|
+
var SearchQueryError = class extends Error {
|
|
8397
|
+
constructor(message, code) {
|
|
8398
|
+
super(message);
|
|
8399
|
+
this.code = code;
|
|
8400
|
+
}
|
|
8401
|
+
code;
|
|
8402
|
+
};
|
|
8403
|
+
function intOr(raw, fallback) {
|
|
8404
|
+
if (raw === null) return fallback;
|
|
8405
|
+
const n = Number.parseInt(raw, 10);
|
|
8406
|
+
return Number.isFinite(n) ? n : fallback;
|
|
8407
|
+
}
|
|
8408
|
+
function parseSearchQuery(params) {
|
|
8409
|
+
const q = (params.get("q") ?? "").trim();
|
|
8410
|
+
if (!q) {
|
|
8411
|
+
throw new SearchQueryError("Missing query parameter: q", "invalid_query");
|
|
8412
|
+
}
|
|
8413
|
+
if (q.length > MAX_QUERY_LENGTH) {
|
|
8414
|
+
throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
|
|
8415
|
+
}
|
|
8416
|
+
const limit = Math.min(
|
|
8417
|
+
Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
|
|
8418
|
+
MAX_SEARCH_LIMIT
|
|
8419
|
+
);
|
|
8420
|
+
const offset = Math.max(intOr(params.get("offset"), 0), 0);
|
|
8421
|
+
const filters = {};
|
|
8422
|
+
const provider = params.get("provider");
|
|
8423
|
+
if (provider !== null) {
|
|
8424
|
+
if (!isProviderName(provider)) {
|
|
8425
|
+
throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
|
|
8426
|
+
}
|
|
8427
|
+
filters.provider = provider;
|
|
8428
|
+
}
|
|
8429
|
+
const projectPath = params.get("projectPath");
|
|
8430
|
+
if (projectPath) filters.projectPath = projectPath;
|
|
8431
|
+
const branch = params.get("branch");
|
|
8432
|
+
if (branch) filters.branch = branch;
|
|
8433
|
+
for (const [key, field] of [
|
|
8434
|
+
["since", "since"],
|
|
8435
|
+
["until", "until"]
|
|
8436
|
+
]) {
|
|
8437
|
+
const raw = params.get(key);
|
|
8438
|
+
if (raw === null) continue;
|
|
8439
|
+
const ms = Date.parse(raw);
|
|
8440
|
+
if (Number.isNaN(ms)) {
|
|
8441
|
+
throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
|
|
8442
|
+
}
|
|
8443
|
+
filters[field] = ms;
|
|
8444
|
+
}
|
|
8445
|
+
if (filters.since != null && filters.until != null && filters.since > filters.until) {
|
|
8446
|
+
throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
|
|
8447
|
+
}
|
|
8448
|
+
return { q, limit, offset, filters };
|
|
8449
|
+
}
|
|
8450
|
+
function applyFilters(results, filters) {
|
|
8451
|
+
return results.filter((r) => {
|
|
8452
|
+
if (filters.provider && r.provider !== filters.provider) return false;
|
|
8453
|
+
if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
|
|
8454
|
+
if (filters.branch && r.branch !== filters.branch) return false;
|
|
8455
|
+
if (filters.since != null || filters.until != null) {
|
|
8456
|
+
const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
|
|
8457
|
+
if (Number.isNaN(ts)) return false;
|
|
8458
|
+
if (filters.since != null && ts < filters.since) return false;
|
|
8459
|
+
if (filters.until != null && ts > filters.until) return false;
|
|
8460
|
+
}
|
|
8461
|
+
return true;
|
|
8462
|
+
});
|
|
8463
|
+
}
|
|
8464
|
+
function paginate(results, offset, limit) {
|
|
8465
|
+
const items = results.slice(offset, offset + limit);
|
|
8466
|
+
return {
|
|
8467
|
+
items,
|
|
8468
|
+
total: results.length,
|
|
8469
|
+
offset,
|
|
8470
|
+
hasMore: offset + items.length < results.length
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
|
|
8091
8474
|
// src/services/sessions/conversationBusy.ts
|
|
8092
|
-
var
|
|
8475
|
+
var import_fs18 = require("fs");
|
|
8093
8476
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
8094
8477
|
function resolveResumeBusyWindowMs(env = process.env) {
|
|
8095
8478
|
const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
|
|
@@ -8106,7 +8489,7 @@ function conversationBusy(input) {
|
|
|
8106
8489
|
let lastActivityMs = null;
|
|
8107
8490
|
if (input.jsonlPath) {
|
|
8108
8491
|
try {
|
|
8109
|
-
const mtimeMs = (0,
|
|
8492
|
+
const mtimeMs = (0, import_fs18.statSync)(input.jsonlPath).mtimeMs;
|
|
8110
8493
|
const age = now - mtimeMs;
|
|
8111
8494
|
lastActivityMs = Math.max(0, age);
|
|
8112
8495
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -8790,6 +9173,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
|
8790
9173
|
var GRACE_MAX_DEFERS = 4;
|
|
8791
9174
|
var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
|
|
8792
9175
|
var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
|
|
9176
|
+
var SEARCH_OVERFETCH = 4;
|
|
9177
|
+
var SEARCH_MAX_SCAN = 1e3;
|
|
8793
9178
|
var RESUME_DISCOVERY_TIMEOUT_MS = 750;
|
|
8794
9179
|
var DISCOVERY_TTL_MS = 15e3;
|
|
8795
9180
|
var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
@@ -8867,6 +9252,20 @@ var StreamerServer = class {
|
|
|
8867
9252
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
8868
9253
|
// a single rescan after the current one completes instead of restarting it.
|
|
8869
9254
|
scannerStale = false;
|
|
9255
|
+
// WHICH files scannerStale is about. A directory event names exactly one
|
|
9256
|
+
// JSONL, and the only correct response is refreshFile() on that one file —
|
|
9257
|
+
// but scannerStale alone carries no identity, so honoring it used to mean a
|
|
9258
|
+
// full-tree rescan. On the non-persistent scanner this server actually runs
|
|
9259
|
+
// (buildStatCache => persistent:false, see listen()), scan() opens by
|
|
9260
|
+
// clearing metadataCache AND conversationLRU — so one live session appending
|
|
9261
|
+
// to its own transcript threw away every OTHER conversation's parsed
|
|
9262
|
+
// snapshot, and the next full fetch of an unrelated conversation re-parsed
|
|
9263
|
+
// it from disk (745-2877ms on a 5MB/1112-message history) while the
|
|
9264
|
+
// per-file paginated path stayed at ~20ms throughout. Populated alongside
|
|
9265
|
+
// scannerStale and drained with it by takeStaleFiles(); an armed flag with
|
|
9266
|
+
// an EMPTY set means "stale, source unknown" and still falls back to the
|
|
9267
|
+
// full rescan.
|
|
9268
|
+
staleFiles = /* @__PURE__ */ new Set();
|
|
8870
9269
|
// Single-flight guard for the background disk reconcile: a burst of list
|
|
8871
9270
|
// polls during active session writes shares one rescan instead of queueing
|
|
8872
9271
|
// a full rescan per request.
|
|
@@ -9016,7 +9415,7 @@ var StreamerServer = class {
|
|
|
9016
9415
|
this.skipStartupWarmup = config.skipStartupWarmup ?? false;
|
|
9017
9416
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
9018
9417
|
this.scanProfiles = config.scanProfiles;
|
|
9019
|
-
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0,
|
|
9418
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os10.homedir)(), ".codex", "sessions")];
|
|
9020
9419
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
9021
9420
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
9022
9421
|
this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
|
|
@@ -9030,12 +9429,15 @@ var StreamerServer = class {
|
|
|
9030
9429
|
this.claudeFlagsPersistable = config.claudeFlags === void 0;
|
|
9031
9430
|
this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
|
|
9032
9431
|
this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
|
|
9033
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0,
|
|
9432
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase", "cache");
|
|
9034
9433
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
9035
9434
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
9036
9435
|
this.markScannerStaleDebounced = debounce(() => {
|
|
9037
9436
|
if (this.scannerReady) this.scannerStale = true;
|
|
9038
|
-
else
|
|
9437
|
+
else {
|
|
9438
|
+
this.scanner = null;
|
|
9439
|
+
this.staleFiles.clear();
|
|
9440
|
+
}
|
|
9039
9441
|
}, this.directoryDebounceMs);
|
|
9040
9442
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
9041
9443
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -9078,7 +9480,7 @@ var StreamerServer = class {
|
|
|
9078
9480
|
const seqs = cache.extendMessageIndex(
|
|
9079
9481
|
filePath,
|
|
9080
9482
|
spans,
|
|
9081
|
-
(0,
|
|
9483
|
+
(0, import_fs19.statSync)(filePath),
|
|
9082
9484
|
readFrom,
|
|
9083
9485
|
endOffset
|
|
9084
9486
|
);
|
|
@@ -9128,6 +9530,7 @@ var StreamerServer = class {
|
|
|
9128
9530
|
if (!tailed) this.maybeAttachExternalTail(filePath);
|
|
9129
9531
|
this.sweepIdleExternalTails();
|
|
9130
9532
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
9533
|
+
this.staleFiles.add(filePath);
|
|
9131
9534
|
this.markScannerStaleDebounced();
|
|
9132
9535
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
9133
9536
|
filePath,
|
|
@@ -9540,14 +9943,14 @@ var StreamerServer = class {
|
|
|
9540
9943
|
}
|
|
9541
9944
|
this.apnsClient = new ApnsClient(creds);
|
|
9542
9945
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
9543
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0,
|
|
9544
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0,
|
|
9946
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os10.hostname)();
|
|
9947
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os10.hostname)());
|
|
9545
9948
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
9546
9949
|
repo: pushRepo,
|
|
9547
9950
|
sender,
|
|
9548
9951
|
sessionStore: this.sessionStore,
|
|
9549
9952
|
serverId,
|
|
9550
|
-
serverLabel: (0,
|
|
9953
|
+
serverLabel: (0, import_os10.hostname)()
|
|
9551
9954
|
});
|
|
9552
9955
|
this.liveActivityRenewal.start();
|
|
9553
9956
|
this.log.info("Live Activity push enabled", {
|
|
@@ -9886,7 +10289,7 @@ var StreamerServer = class {
|
|
|
9886
10289
|
this.fileWatcher.watchDirectory(dir);
|
|
9887
10290
|
}
|
|
9888
10291
|
for (const dir of this.codexRoots) {
|
|
9889
|
-
if (!(0,
|
|
10292
|
+
if (!(0, import_fs19.existsSync)(dir)) continue;
|
|
9890
10293
|
this.fileWatcher.watchDirectory(dir);
|
|
9891
10294
|
}
|
|
9892
10295
|
} catch (err) {
|
|
@@ -10159,7 +10562,7 @@ var StreamerServer = class {
|
|
|
10159
10562
|
}
|
|
10160
10563
|
let body;
|
|
10161
10564
|
try {
|
|
10162
|
-
body = await
|
|
10565
|
+
body = await readBody2(req);
|
|
10163
10566
|
} catch (err) {
|
|
10164
10567
|
const message = err instanceof Error ? err.message : "Invalid body";
|
|
10165
10568
|
json(res, 400, { error: message });
|
|
@@ -10205,7 +10608,7 @@ var StreamerServer = class {
|
|
|
10205
10608
|
nonce: sealed.nonce,
|
|
10206
10609
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
10207
10610
|
publicUrl: this.publicUrl,
|
|
10208
|
-
machineName: (0,
|
|
10611
|
+
machineName: (0, import_os10.hostname)(),
|
|
10209
10612
|
...device && {
|
|
10210
10613
|
deviceId: device.deviceId,
|
|
10211
10614
|
deviceToken: device.deviceToken,
|
|
@@ -10333,7 +10736,7 @@ var StreamerServer = class {
|
|
|
10333
10736
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
10334
10737
|
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
10335
10738
|
}
|
|
10336
|
-
return [(0, import_path18.join)((0,
|
|
10739
|
+
return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
|
|
10337
10740
|
}
|
|
10338
10741
|
/**
|
|
10339
10742
|
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
@@ -10374,21 +10777,46 @@ var StreamerServer = class {
|
|
|
10374
10777
|
// so a burst of list polls during active session writes shares one rescan
|
|
10375
10778
|
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
10376
10779
|
// in-flight cache write before shutting the DB.
|
|
10377
|
-
startBackgroundConversationReconcile() {
|
|
10780
|
+
startBackgroundConversationReconcile(mode = "full") {
|
|
10378
10781
|
if (this.conversationReconcileInFlight) return;
|
|
10379
|
-
const
|
|
10782
|
+
const paths = mode === "files" ? this.takeStaleFiles() : [];
|
|
10783
|
+
const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
|
|
10380
10784
|
this.conversationReconcileInFlight = null;
|
|
10381
10785
|
});
|
|
10382
10786
|
this.conversationReconcileInFlight = task;
|
|
10383
10787
|
this.trackCacheWrite(task);
|
|
10384
10788
|
}
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10789
|
+
// "files": a directory event named specific JSONLs, so refresh only those.
|
|
10790
|
+
// "full": disk drifted in ways a per-file refresh can't see (a project dir
|
|
10791
|
+
// appeared, rows vanished), so walk the tree. Order matters — the staleness
|
|
10792
|
+
// check short-circuits first so the HDD freshness probe stays off the hot
|
|
10793
|
+
// poll path, exactly as it did when this returned a boolean.
|
|
10794
|
+
conversationReconcileMode() {
|
|
10795
|
+
if (!this.cache) return null;
|
|
10796
|
+
if (this.scannerStale) return "files";
|
|
10797
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
|
|
10389
10798
|
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
10390
10799
|
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
10391
|
-
});
|
|
10800
|
+
}) ? "full" : null;
|
|
10801
|
+
}
|
|
10802
|
+
// The per-file half of reconcileConversationsCacheFromDisk: re-index just the
|
|
10803
|
+
// changed JSONLs and upsert their rows. No reconcileDeletions here — that
|
|
10804
|
+
// needs the whole live-path set, and deletions already have their own path
|
|
10805
|
+
// (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
|
|
10806
|
+
// HDD-freshness "full" mode.
|
|
10807
|
+
async reconcileStaleFilesFromDisk(paths) {
|
|
10808
|
+
if (!this.cache) return;
|
|
10809
|
+
const scanner = await this.getScanner(true);
|
|
10810
|
+
const metas = await this.refreshStaleFiles(scanner, paths);
|
|
10811
|
+
if (metas.length === 0) return;
|
|
10812
|
+
try {
|
|
10813
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
10814
|
+
} catch (err) {
|
|
10815
|
+
this.log.warn(
|
|
10816
|
+
`stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
10817
|
+
{ event: "conversations.reconcile_failed" }
|
|
10818
|
+
);
|
|
10819
|
+
}
|
|
10392
10820
|
}
|
|
10393
10821
|
async handleListConversations(url, res) {
|
|
10394
10822
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10398,10 +10826,11 @@ var StreamerServer = class {
|
|
|
10398
10826
|
const project = url.searchParams.get("project") ?? void 0;
|
|
10399
10827
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
10400
10828
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
10401
|
-
|
|
10829
|
+
const reconcileMode = this.conversationReconcileMode();
|
|
10830
|
+
if (this.cache && (bustCache || reconcileMode)) {
|
|
10402
10831
|
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
10403
10832
|
if (canServeStale) {
|
|
10404
|
-
this.startBackgroundConversationReconcile();
|
|
10833
|
+
this.startBackgroundConversationReconcile(reconcileMode ?? "full");
|
|
10405
10834
|
} else {
|
|
10406
10835
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
10407
10836
|
await this.withWarmup(
|
|
@@ -10601,21 +11030,54 @@ var StreamerServer = class {
|
|
|
10601
11030
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
10602
11031
|
);
|
|
10603
11032
|
}
|
|
11033
|
+
// Drain the stale set and disarm the flag together. The caller owns the
|
|
11034
|
+
// returned paths: clearing before the refresh means events that land DURING
|
|
11035
|
+
// it re-arm the flag and get their own pass instead of being swallowed.
|
|
11036
|
+
takeStaleFiles() {
|
|
11037
|
+
const paths = [...this.staleFiles];
|
|
11038
|
+
this.staleFiles.clear();
|
|
11039
|
+
this.scannerStale = false;
|
|
11040
|
+
return paths;
|
|
11041
|
+
}
|
|
11042
|
+
// Reconcile exactly the JSONLs a directory event named. Failures are logged
|
|
11043
|
+
// and swallowed per file: one unreadable transcript must not abort the
|
|
11044
|
+
// others, and the file simply stays on its previous snapshot until the next
|
|
11045
|
+
// event — the same outcome the full rescan gave on a parse failure.
|
|
11046
|
+
async refreshStaleFiles(scanner, paths) {
|
|
11047
|
+
const metas = await Promise.all(
|
|
11048
|
+
paths.map(
|
|
11049
|
+
(filePath) => scanner.refreshFile(filePath).catch((err) => {
|
|
11050
|
+
this.log.warn("scanner.refreshFile: failed", {
|
|
11051
|
+
event: "scanner.refresh_failed",
|
|
11052
|
+
filePath,
|
|
11053
|
+
trigger: "directory-event",
|
|
11054
|
+
err
|
|
11055
|
+
});
|
|
11056
|
+
return null;
|
|
11057
|
+
})
|
|
11058
|
+
)
|
|
11059
|
+
);
|
|
11060
|
+
return metas.filter((m) => m !== null);
|
|
11061
|
+
}
|
|
10604
11062
|
async getScanner(skipStaleRescan = false) {
|
|
10605
11063
|
if (this.scannerReady) {
|
|
10606
11064
|
await this.scannerReady;
|
|
10607
11065
|
if (this.scanner) {
|
|
10608
11066
|
if (skipStaleRescan) return this.scanner;
|
|
10609
11067
|
if (this.scannerStale) {
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
11068
|
+
const paths = this.takeStaleFiles();
|
|
11069
|
+
if (paths.length === 0) {
|
|
11070
|
+
this.scanner = null;
|
|
11071
|
+
this.scannerReady = null;
|
|
11072
|
+
return this.getScanner();
|
|
11073
|
+
}
|
|
11074
|
+
await this.refreshStaleFiles(this.scanner, paths);
|
|
11075
|
+
return this.scanner ?? this.getScanner();
|
|
10614
11076
|
}
|
|
10615
11077
|
return this.scanner;
|
|
10616
11078
|
}
|
|
10617
11079
|
}
|
|
10618
|
-
this.
|
|
11080
|
+
this.takeStaleFiles();
|
|
10619
11081
|
const statCache = this.buildStatCache(this.scanner);
|
|
10620
11082
|
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
10621
11083
|
this.allScanners.add(this.scanner);
|
|
@@ -10649,7 +11111,7 @@ var StreamerServer = class {
|
|
|
10649
11111
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
10650
11112
|
async rescanForRefresh(onProgress) {
|
|
10651
11113
|
if (this.scannerReady) await this.scannerReady;
|
|
10652
|
-
this.
|
|
11114
|
+
this.takeStaleFiles();
|
|
10653
11115
|
if (!this.scanner) {
|
|
10654
11116
|
this.scanner = new import_scanner3.ConversationScanner();
|
|
10655
11117
|
this.allScanners.add(this.scanner);
|
|
@@ -10676,20 +11138,20 @@ var StreamerServer = class {
|
|
|
10676
11138
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
10677
11139
|
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
10678
11140
|
}
|
|
10679
|
-
return [(0, import_path18.join)((0,
|
|
11141
|
+
return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
|
|
10680
11142
|
}
|
|
10681
11143
|
findJsonlPath(uuid) {
|
|
10682
11144
|
const filename = `${uuid}.jsonl`;
|
|
10683
11145
|
for (const projectsDir of this.projectsDirs()) {
|
|
10684
|
-
if (!(0,
|
|
10685
|
-
for (const dir of (0,
|
|
11146
|
+
if (!(0, import_fs19.existsSync)(projectsDir)) continue;
|
|
11147
|
+
for (const dir of (0, import_fs19.readdirSync)(projectsDir)) {
|
|
10686
11148
|
const fp = (0, import_path18.join)(projectsDir, dir, filename);
|
|
10687
|
-
if ((0,
|
|
11149
|
+
if ((0, import_fs19.existsSync)(fp)) return fp;
|
|
10688
11150
|
const projectDir = (0, import_path18.join)(projectsDir, dir);
|
|
10689
11151
|
try {
|
|
10690
|
-
for (const sub of (0,
|
|
11152
|
+
for (const sub of (0, import_fs19.readdirSync)(projectDir)) {
|
|
10691
11153
|
const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
|
|
10692
|
-
if ((0,
|
|
11154
|
+
if ((0, import_fs19.existsSync)(subagentPath)) return subagentPath;
|
|
10693
11155
|
}
|
|
10694
11156
|
} catch {
|
|
10695
11157
|
}
|
|
@@ -10699,7 +11161,7 @@ var StreamerServer = class {
|
|
|
10699
11161
|
}
|
|
10700
11162
|
async readCwdFromJsonl(filePath) {
|
|
10701
11163
|
return new Promise((resolve2) => {
|
|
10702
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
11164
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs19.createReadStream)(filePath), crlfDelay: Infinity });
|
|
10703
11165
|
let found = false;
|
|
10704
11166
|
rl.on("line", (line) => {
|
|
10705
11167
|
if (found) return;
|
|
@@ -10789,7 +11251,7 @@ var StreamerServer = class {
|
|
|
10789
11251
|
if (this.isManagedTailPath(key)) return;
|
|
10790
11252
|
let mtimeMs;
|
|
10791
11253
|
try {
|
|
10792
|
-
mtimeMs = (0,
|
|
11254
|
+
mtimeMs = (0, import_fs19.statSync)(filePath).mtimeMs;
|
|
10793
11255
|
} catch {
|
|
10794
11256
|
return;
|
|
10795
11257
|
}
|
|
@@ -10971,7 +11433,7 @@ var StreamerServer = class {
|
|
|
10971
11433
|
if (!conv.filePath) return false;
|
|
10972
11434
|
let mtimeMs = null;
|
|
10973
11435
|
try {
|
|
10974
|
-
mtimeMs = (0,
|
|
11436
|
+
mtimeMs = (0, import_fs19.statSync)(conv.filePath).mtimeMs;
|
|
10975
11437
|
} catch {
|
|
10976
11438
|
return false;
|
|
10977
11439
|
}
|
|
@@ -11226,7 +11688,7 @@ var StreamerServer = class {
|
|
|
11226
11688
|
}
|
|
11227
11689
|
let body;
|
|
11228
11690
|
try {
|
|
11229
|
-
body = await
|
|
11691
|
+
body = await readBody2(req);
|
|
11230
11692
|
} catch {
|
|
11231
11693
|
res.setHeader("Accept-Query", "application/json");
|
|
11232
11694
|
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
@@ -11264,17 +11726,27 @@ var StreamerServer = class {
|
|
|
11264
11726
|
});
|
|
11265
11727
|
}
|
|
11266
11728
|
async handleSearch(url, res) {
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11729
|
+
let parsed;
|
|
11730
|
+
try {
|
|
11731
|
+
parsed = parseSearchQuery(url.searchParams);
|
|
11732
|
+
} catch (err) {
|
|
11733
|
+
if (err instanceof SearchQueryError) {
|
|
11734
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11737
|
+
throw err;
|
|
11271
11738
|
}
|
|
11272
|
-
const
|
|
11739
|
+
const { q, limit, offset, filters } = parsed;
|
|
11740
|
+
const startedAt = Date.now();
|
|
11273
11741
|
const scanner = await this.getScanner();
|
|
11274
11742
|
const results = await (0, import_scanner3.search)(
|
|
11275
11743
|
q,
|
|
11276
11744
|
{
|
|
11277
|
-
|
|
11745
|
+
// Fetch beyond the requested page: filters below are applied AFTER the
|
|
11746
|
+
// scanner returns, so slicing at `limit` here would drop results that a
|
|
11747
|
+
// later page should contain. Bounded so a broad query cannot pull an
|
|
11748
|
+
// unbounded set into memory.
|
|
11749
|
+
limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
|
|
11278
11750
|
include: "conversations",
|
|
11279
11751
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
11280
11752
|
...this.codexScanOpts()
|
|
@@ -11298,13 +11770,24 @@ var StreamerServer = class {
|
|
|
11298
11770
|
lastActivity: r.meta.timestamp,
|
|
11299
11771
|
firstMessage: r.meta.firstMessage ?? void 0,
|
|
11300
11772
|
lastMessage: r.meta.lastMessage ?? void 0,
|
|
11301
|
-
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
|
|
11773
|
+
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
|
|
11774
|
+
// The scanner already computes relevance and match snippets; the previous
|
|
11775
|
+
// adapter discarded both, so results arrived in an unexplained order with
|
|
11776
|
+
// no indication of WHY anything matched.
|
|
11777
|
+
score: r.score,
|
|
11778
|
+
matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
|
|
11779
|
+
field: m.field,
|
|
11780
|
+
snippet: m.snippet
|
|
11781
|
+
})) : []
|
|
11302
11782
|
}));
|
|
11783
|
+
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
11303
11784
|
json(res, 200, {
|
|
11304
|
-
conversations:
|
|
11305
|
-
hasMore:
|
|
11306
|
-
offset:
|
|
11307
|
-
total:
|
|
11785
|
+
conversations: page.items,
|
|
11786
|
+
hasMore: page.hasMore,
|
|
11787
|
+
offset: page.offset,
|
|
11788
|
+
total: page.total,
|
|
11789
|
+
// Query timing, so a slow search is diagnosable rather than merely felt.
|
|
11790
|
+
tookMs: Date.now() - startedAt
|
|
11308
11791
|
});
|
|
11309
11792
|
}
|
|
11310
11793
|
async handleListSessions(url, res) {
|
|
@@ -11350,7 +11833,7 @@ var StreamerServer = class {
|
|
|
11350
11833
|
if (this.rejectIfWarmingUp(res)) return;
|
|
11351
11834
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
11352
11835
|
if (session) {
|
|
11353
|
-
if (!(0,
|
|
11836
|
+
if (!(0, import_fs19.existsSync)(session.projectPath)) {
|
|
11354
11837
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
11355
11838
|
}
|
|
11356
11839
|
const reconciled = this.withReconciledLifecycle([session])[0];
|
|
@@ -11377,7 +11860,7 @@ var StreamerServer = class {
|
|
|
11377
11860
|
json(res, 404, { error: "Session not found" });
|
|
11378
11861
|
}
|
|
11379
11862
|
async handleResume(req, res) {
|
|
11380
|
-
const body = await
|
|
11863
|
+
const body = await readBody2(req);
|
|
11381
11864
|
const sessionId = body.sessionId ?? body.conversationId;
|
|
11382
11865
|
if (!sessionId) {
|
|
11383
11866
|
json(res, 400, { error: "Missing sessionId" });
|
|
@@ -11509,7 +11992,7 @@ var StreamerServer = class {
|
|
|
11509
11992
|
return;
|
|
11510
11993
|
}
|
|
11511
11994
|
if (this.agentConfig.enabled) {
|
|
11512
|
-
const body2 = await
|
|
11995
|
+
const body2 = await readBody2(req);
|
|
11513
11996
|
const cache = this.cache;
|
|
11514
11997
|
if (!cache) {
|
|
11515
11998
|
json(res, 503, {
|
|
@@ -11528,7 +12011,7 @@ var StreamerServer = class {
|
|
|
11528
12011
|
json(res, result.status, result.body);
|
|
11529
12012
|
return;
|
|
11530
12013
|
}
|
|
11531
|
-
const body = await
|
|
12014
|
+
const body = await readBody2(req);
|
|
11532
12015
|
const { input, keys } = body;
|
|
11533
12016
|
let idempotencyKey;
|
|
11534
12017
|
try {
|
|
@@ -11692,7 +12175,7 @@ var StreamerServer = class {
|
|
|
11692
12175
|
});
|
|
11693
12176
|
}
|
|
11694
12177
|
async handleSendAnswer(sessionId, req, res) {
|
|
11695
|
-
const body = await
|
|
12178
|
+
const body = await readBody2(req);
|
|
11696
12179
|
const pending = this.pendingQuestions.get(sessionId);
|
|
11697
12180
|
const resolution = resolveAnswer(pending, body);
|
|
11698
12181
|
if (!resolution.ok) {
|
|
@@ -11721,7 +12204,7 @@ var StreamerServer = class {
|
|
|
11721
12204
|
json(res, 400, { error: "Session has no project path" });
|
|
11722
12205
|
return;
|
|
11723
12206
|
}
|
|
11724
|
-
const body = await
|
|
12207
|
+
const body = await readBody2(req);
|
|
11725
12208
|
const { filename, mimeType, dataBase64 } = body ?? {};
|
|
11726
12209
|
if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
|
|
11727
12210
|
json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
|
|
@@ -11923,7 +12406,7 @@ var StreamerServer = class {
|
|
|
11923
12406
|
return;
|
|
11924
12407
|
}
|
|
11925
12408
|
if (this.agentConfig.enabled) {
|
|
11926
|
-
const body2 = await
|
|
12409
|
+
const body2 = await readBody2(req);
|
|
11927
12410
|
const result = await handleStartAgentSession(body2, {
|
|
11928
12411
|
sessionStore: this.sessionStore,
|
|
11929
12412
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
@@ -11937,7 +12420,7 @@ var StreamerServer = class {
|
|
|
11937
12420
|
}
|
|
11938
12421
|
return;
|
|
11939
12422
|
}
|
|
11940
|
-
const body = await
|
|
12423
|
+
const body = await readBody2(req);
|
|
11941
12424
|
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
11942
12425
|
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
11943
12426
|
json(res, 400, { error: "Invalid provider" });
|
|
@@ -12074,7 +12557,7 @@ var StreamerServer = class {
|
|
|
12074
12557
|
// file isn't slurped in full.
|
|
12075
12558
|
readFirstLineSessionId(filePath) {
|
|
12076
12559
|
try {
|
|
12077
|
-
const content = (0,
|
|
12560
|
+
const content = (0, import_fs19.readFileSync)(filePath, "utf8");
|
|
12078
12561
|
const nl = content.indexOf("\n");
|
|
12079
12562
|
const firstLine = nl === -1 ? content : content.slice(0, nl);
|
|
12080
12563
|
if (!firstLine.trim()) return null;
|
|
@@ -12089,7 +12572,7 @@ var StreamerServer = class {
|
|
|
12089
12572
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
12090
12573
|
watchForJsonl(sessionId, projectPath) {
|
|
12091
12574
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
12092
|
-
const projectsDir = (0, import_path18.join)((0,
|
|
12575
|
+
const projectsDir = (0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects", encoded);
|
|
12093
12576
|
const expectedFile = `${sessionId}.jsonl`;
|
|
12094
12577
|
const filePath = (0, import_path18.join)(projectsDir, expectedFile);
|
|
12095
12578
|
const deadline = Date.now() + 12e4;
|
|
@@ -12109,11 +12592,11 @@ var StreamerServer = class {
|
|
|
12109
12592
|
cleanup();
|
|
12110
12593
|
return;
|
|
12111
12594
|
}
|
|
12112
|
-
let resolvedFilePath = (0,
|
|
12113
|
-
if (!resolvedFilePath && (0,
|
|
12595
|
+
let resolvedFilePath = (0, import_fs19.existsSync)(filePath) ? filePath : null;
|
|
12596
|
+
if (!resolvedFilePath && (0, import_fs19.existsSync)(projectsDir)) {
|
|
12114
12597
|
try {
|
|
12115
12598
|
const now = Date.now();
|
|
12116
|
-
const match = (0,
|
|
12599
|
+
const match = (0, import_fs19.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
12117
12600
|
({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
|
|
12118
12601
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
12119
12602
|
if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
|
|
@@ -12124,7 +12607,7 @@ var StreamerServer = class {
|
|
|
12124
12607
|
cleanup();
|
|
12125
12608
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
12126
12609
|
try {
|
|
12127
|
-
const existing = (0,
|
|
12610
|
+
const existing = (0, import_fs19.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
12128
12611
|
if (existing.length > 0) {
|
|
12129
12612
|
this.broadcastConversationLines(sessionId, existing);
|
|
12130
12613
|
}
|
|
@@ -12148,7 +12631,7 @@ var StreamerServer = class {
|
|
|
12148
12631
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
12149
12632
|
try {
|
|
12150
12633
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
12151
|
-
watcher = (0,
|
|
12634
|
+
watcher = (0, import_fs19.watch)(projectsDir, tryWire);
|
|
12152
12635
|
watcher.on("error", cleanup);
|
|
12153
12636
|
} catch {
|
|
12154
12637
|
}
|
|
@@ -12177,7 +12660,7 @@ var StreamerServer = class {
|
|
|
12177
12660
|
};
|
|
12178
12661
|
const matchesProjectPath = (candidatePath) => {
|
|
12179
12662
|
try {
|
|
12180
|
-
const firstLine = (0,
|
|
12663
|
+
const firstLine = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
12181
12664
|
if (!firstLine) return null;
|
|
12182
12665
|
const parsed = JSON.parse(firstLine);
|
|
12183
12666
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -12206,15 +12689,15 @@ var StreamerServer = class {
|
|
|
12206
12689
|
);
|
|
12207
12690
|
for (const root of this.codexRoots) {
|
|
12208
12691
|
const sessionsDir = (0, import_path18.join)(root, dateDir);
|
|
12209
|
-
if (!(0,
|
|
12692
|
+
if (!(0, import_fs19.existsSync)(sessionsDir)) continue;
|
|
12210
12693
|
let candidateFiles;
|
|
12211
12694
|
try {
|
|
12212
|
-
candidateFiles = (0,
|
|
12695
|
+
candidateFiles = (0, import_fs19.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
12213
12696
|
} catch {
|
|
12214
12697
|
continue;
|
|
12215
12698
|
}
|
|
12216
12699
|
const nowMs = Date.now();
|
|
12217
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0,
|
|
12700
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
12218
12701
|
for (const { f } of recentCandidates) {
|
|
12219
12702
|
const candidatePath = (0, import_path18.join)(sessionsDir, f);
|
|
12220
12703
|
const match = matchesProjectPath(candidatePath);
|
|
@@ -12226,7 +12709,7 @@ var StreamerServer = class {
|
|
|
12226
12709
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
12227
12710
|
this.fileWatcher.watch(candidatePath);
|
|
12228
12711
|
try {
|
|
12229
|
-
const existing = (0,
|
|
12712
|
+
const existing = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
12230
12713
|
if (existing.length > 0) {
|
|
12231
12714
|
this.broadcastConversationLines(sessionId, existing);
|
|
12232
12715
|
}
|
|
@@ -12295,7 +12778,7 @@ var StreamerServer = class {
|
|
|
12295
12778
|
});
|
|
12296
12779
|
return;
|
|
12297
12780
|
}
|
|
12298
|
-
const body = await
|
|
12781
|
+
const body = await readBody2(req);
|
|
12299
12782
|
const { path: relativePath, name } = body;
|
|
12300
12783
|
if (!name || typeof name !== "string") {
|
|
12301
12784
|
json(res, 400, { error: "Missing name field" });
|
|
@@ -12325,7 +12808,7 @@ var StreamerServer = class {
|
|
|
12325
12808
|
}
|
|
12326
12809
|
let parsed;
|
|
12327
12810
|
try {
|
|
12328
|
-
parsed = await
|
|
12811
|
+
parsed = await readBody2(req);
|
|
12329
12812
|
} catch {
|
|
12330
12813
|
json(res, 400, { error: "Invalid JSON" });
|
|
12331
12814
|
return;
|
|
@@ -12382,7 +12865,7 @@ var StreamerServer = class {
|
|
|
12382
12865
|
}
|
|
12383
12866
|
let parsed;
|
|
12384
12867
|
try {
|
|
12385
|
-
parsed = await
|
|
12868
|
+
parsed = await readBody2(req);
|
|
12386
12869
|
} catch {
|
|
12387
12870
|
json(res, 400, { error: "Invalid JSON" });
|
|
12388
12871
|
return;
|
|
@@ -12441,7 +12924,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
12441
12924
|
}
|
|
12442
12925
|
function classifyResumability(cwd) {
|
|
12443
12926
|
if (!cwd) return { resumable: true };
|
|
12444
|
-
if ((0,
|
|
12927
|
+
if ((0, import_fs19.existsSync)(cwd)) return { resumable: true };
|
|
12445
12928
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
12446
12929
|
return {
|
|
12447
12930
|
resumable: false,
|
|
@@ -12559,7 +13042,7 @@ function parseSessionListQuery(url) {
|
|
|
12559
13042
|
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
12560
13043
|
return { query: { limit, sortBy, order, status, cursor } };
|
|
12561
13044
|
}
|
|
12562
|
-
function
|
|
13045
|
+
function readBody2(req) {
|
|
12563
13046
|
return new Promise((resolve2, reject) => {
|
|
12564
13047
|
const chunks = [];
|
|
12565
13048
|
req.on("data", (chunk) => chunks.push(chunk));
|