@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.js
CHANGED
|
@@ -3017,7 +3017,7 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
3017
3017
|
import { EventEmitter } from "events";
|
|
3018
3018
|
import {
|
|
3019
3019
|
createReadStream,
|
|
3020
|
-
existsSync as
|
|
3020
|
+
existsSync as existsSync11,
|
|
3021
3021
|
watch as fsWatch,
|
|
3022
3022
|
readdirSync as readdirSync6,
|
|
3023
3023
|
readFileSync as readFileSync8,
|
|
@@ -3025,7 +3025,7 @@ import {
|
|
|
3025
3025
|
} from "fs";
|
|
3026
3026
|
import { realpath as realpath2 } from "fs/promises";
|
|
3027
3027
|
import { createServer } from "http";
|
|
3028
|
-
import { homedir as homedir9, hostname as
|
|
3028
|
+
import { homedir as homedir9, hostname as hostname3 } from "os";
|
|
3029
3029
|
import { basename as basename5, dirname as dirname9, join as join18 } from "path";
|
|
3030
3030
|
import { createInterface } from "readline";
|
|
3031
3031
|
|
|
@@ -3285,7 +3285,7 @@ async function handleStartAgentSession(body, deps) {
|
|
|
3285
3285
|
}
|
|
3286
3286
|
|
|
3287
3287
|
// src/api/app.ts
|
|
3288
|
-
import { Hono as
|
|
3288
|
+
import { Hono as Hono18 } from "hono";
|
|
3289
3289
|
|
|
3290
3290
|
// src/db/repositories/devices.repository.ts
|
|
3291
3291
|
import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -3605,12 +3605,222 @@ var errorMiddleware = (err, c) => {
|
|
|
3605
3605
|
return c.json({ error: message }, 500);
|
|
3606
3606
|
};
|
|
3607
3607
|
|
|
3608
|
-
// src/api/routes/
|
|
3608
|
+
// src/api/routes/backup.routes.ts
|
|
3609
3609
|
import { Hono as Hono2 } from "hono";
|
|
3610
|
+
import { hostname } from "os";
|
|
3611
|
+
|
|
3612
|
+
// src/services/backup/backup.ts
|
|
3613
|
+
var BACKUP_FORMAT_VERSION = 1;
|
|
3614
|
+
var BackupError = class extends Error {
|
|
3615
|
+
constructor(message, code) {
|
|
3616
|
+
super(message);
|
|
3617
|
+
this.code = code;
|
|
3618
|
+
}
|
|
3619
|
+
code;
|
|
3620
|
+
};
|
|
3621
|
+
function validateArchive(input) {
|
|
3622
|
+
if (!input || typeof input !== "object") {
|
|
3623
|
+
throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
|
|
3624
|
+
}
|
|
3625
|
+
const archive = input;
|
|
3626
|
+
const manifest = archive.manifest;
|
|
3627
|
+
if (!manifest || typeof manifest !== "object") {
|
|
3628
|
+
throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
|
|
3629
|
+
}
|
|
3630
|
+
if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
|
|
3631
|
+
throw new BackupError(
|
|
3632
|
+
`Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
|
|
3633
|
+
"UNSUPPORTED_VERSION"
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
if (!Array.isArray(archive.projects)) {
|
|
3637
|
+
throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
|
|
3638
|
+
}
|
|
3639
|
+
for (const [i, p] of archive.projects.entries()) {
|
|
3640
|
+
if (!p || typeof p !== "object") {
|
|
3641
|
+
throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
|
|
3642
|
+
}
|
|
3643
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
3644
|
+
throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
|
|
3645
|
+
}
|
|
3646
|
+
if (typeof p.path !== "string" || p.path.length === 0) {
|
|
3647
|
+
throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const ids = new Set(archive.projects.map((p) => p.id));
|
|
3651
|
+
if (ids.size !== archive.projects.length) {
|
|
3652
|
+
throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
|
|
3653
|
+
}
|
|
3654
|
+
return archive;
|
|
3655
|
+
}
|
|
3656
|
+
function remapPaths(projects, rules) {
|
|
3657
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
3658
|
+
return projects.map((p) => {
|
|
3659
|
+
const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
|
|
3660
|
+
if (!rule) return p;
|
|
3661
|
+
return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
function planRestore(incoming, existing) {
|
|
3665
|
+
const byId = new Map(existing.map((e) => [e.id, e]));
|
|
3666
|
+
const byPath = new Map(existing.map((e) => [e.path, e]));
|
|
3667
|
+
const plan = { create: [], update: [], conflict: [] };
|
|
3668
|
+
for (const p of incoming) {
|
|
3669
|
+
const sameId = byId.get(p.id);
|
|
3670
|
+
if (sameId) {
|
|
3671
|
+
if (sameId.path !== p.path) plan.update.push(p);
|
|
3672
|
+
continue;
|
|
3673
|
+
}
|
|
3674
|
+
const samePath = byPath.get(p.path);
|
|
3675
|
+
if (samePath) {
|
|
3676
|
+
plan.conflict.push({ incoming: p, existingId: samePath.id });
|
|
3677
|
+
continue;
|
|
3678
|
+
}
|
|
3679
|
+
plan.create.push(p);
|
|
3680
|
+
}
|
|
3681
|
+
return plan;
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// src/version.ts
|
|
3685
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
3686
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
3687
|
+
var cached;
|
|
3688
|
+
function getVersion() {
|
|
3689
|
+
if (cached !== void 0) return cached;
|
|
3690
|
+
cached = resolveVersion();
|
|
3691
|
+
return cached;
|
|
3692
|
+
}
|
|
3693
|
+
function resolveVersion() {
|
|
3694
|
+
const scriptPath = process.argv[1] ?? "";
|
|
3695
|
+
const here = scriptPath ? dirname5(scriptPath) : process.cwd();
|
|
3696
|
+
let realHere = here;
|
|
3697
|
+
try {
|
|
3698
|
+
realHere = dirname5(realpathSync(scriptPath));
|
|
3699
|
+
} catch {
|
|
3700
|
+
}
|
|
3701
|
+
const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
|
|
3702
|
+
for (const dir of searchDirs) {
|
|
3703
|
+
try {
|
|
3704
|
+
const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
|
|
3705
|
+
if (v) return v;
|
|
3706
|
+
} catch {
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
|
|
3711
|
+
if (pkg.version) return `${pkg.version}+source`;
|
|
3712
|
+
} catch {
|
|
3713
|
+
}
|
|
3714
|
+
return "0.0.0+unknown";
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
// src/api/routes/backup.routes.ts
|
|
3718
|
+
function readBody(c) {
|
|
3719
|
+
return new Promise((resolve2, reject) => {
|
|
3720
|
+
const chunks = [];
|
|
3721
|
+
c.env.incoming.on("data", (chunk) => chunks.push(chunk));
|
|
3722
|
+
c.env.incoming.on("end", () => {
|
|
3723
|
+
try {
|
|
3724
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3725
|
+
resolve2(raw ? JSON.parse(raw) : {});
|
|
3726
|
+
} catch {
|
|
3727
|
+
reject(new Error("Invalid JSON body"));
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
c.env.incoming.on("error", reject);
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
var createBackupRoutes = (deps) => {
|
|
3734
|
+
const app = new Hono2();
|
|
3735
|
+
app.get("/export", (c) => {
|
|
3736
|
+
const repo = deps.projectsRepo();
|
|
3737
|
+
if (!repo) {
|
|
3738
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3739
|
+
}
|
|
3740
|
+
const projects = repo.listProjects().map((p) => ({
|
|
3741
|
+
id: p.id,
|
|
3742
|
+
path: p.path,
|
|
3743
|
+
name: p.name ?? null,
|
|
3744
|
+
createdAt: p.createdAt,
|
|
3745
|
+
updatedAt: p.updatedAt
|
|
3746
|
+
}));
|
|
3747
|
+
return c.json({
|
|
3748
|
+
manifest: {
|
|
3749
|
+
formatVersion: BACKUP_FORMAT_VERSION,
|
|
3750
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3751
|
+
streamerVersion: getVersion(),
|
|
3752
|
+
sourceHost: hostname(),
|
|
3753
|
+
// No endpoint here exports the API key. The flag is recorded so an
|
|
3754
|
+
// archive is self-describing about its own sensitivity rather than
|
|
3755
|
+
// requiring a reader to infer it.
|
|
3756
|
+
includesSecrets: false,
|
|
3757
|
+
counts: { projects: projects.length }
|
|
3758
|
+
},
|
|
3759
|
+
projects
|
|
3760
|
+
});
|
|
3761
|
+
});
|
|
3762
|
+
app.post("/restore", async (c) => {
|
|
3763
|
+
const repo = deps.projectsRepo();
|
|
3764
|
+
if (!repo) {
|
|
3765
|
+
return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
|
|
3766
|
+
}
|
|
3767
|
+
let body;
|
|
3768
|
+
try {
|
|
3769
|
+
body = await readBody(c);
|
|
3770
|
+
} catch {
|
|
3771
|
+
return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
|
|
3772
|
+
}
|
|
3773
|
+
let archive;
|
|
3774
|
+
try {
|
|
3775
|
+
archive = validateArchive(body.archive);
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
if (err instanceof BackupError) {
|
|
3778
|
+
return c.json({ error: err.message, code: err.code }, 400);
|
|
3779
|
+
}
|
|
3780
|
+
throw err;
|
|
3781
|
+
}
|
|
3782
|
+
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 })) : [];
|
|
3783
|
+
const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
|
|
3784
|
+
const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
|
|
3785
|
+
const plan = planRestore(incoming, existing);
|
|
3786
|
+
const summary = {
|
|
3787
|
+
create: plan.create.length,
|
|
3788
|
+
update: plan.update.length,
|
|
3789
|
+
conflict: plan.conflict.length
|
|
3790
|
+
};
|
|
3791
|
+
if (body.apply !== true) {
|
|
3792
|
+
return c.json({ applied: false, summary, plan });
|
|
3793
|
+
}
|
|
3794
|
+
if (plan.conflict.length > 0) {
|
|
3795
|
+
return c.json(
|
|
3796
|
+
{
|
|
3797
|
+
error: "Restore has unresolved conflicts",
|
|
3798
|
+
code: "RESTORE_CONFLICT",
|
|
3799
|
+
summary,
|
|
3800
|
+
plan
|
|
3801
|
+
},
|
|
3802
|
+
409
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
let applied = 0;
|
|
3806
|
+
for (const p of [...plan.create, ...plan.update]) {
|
|
3807
|
+
try {
|
|
3808
|
+
repo.upsertProjectByPath(p.path, { name: p.name });
|
|
3809
|
+
applied++;
|
|
3810
|
+
} catch {
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return c.json({ applied: true, summary, appliedCount: applied });
|
|
3814
|
+
});
|
|
3815
|
+
return app;
|
|
3816
|
+
};
|
|
3817
|
+
|
|
3818
|
+
// src/api/routes/browse.routes.ts
|
|
3819
|
+
import { Hono as Hono3 } from "hono";
|
|
3610
3820
|
var ALREADY_HANDLED = 597;
|
|
3611
3821
|
var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
|
|
3612
3822
|
var createBrowseRoutes = (deps) => {
|
|
3613
|
-
const app = new
|
|
3823
|
+
const app = new Hono3();
|
|
3614
3824
|
app.get("/browse", async (c) => {
|
|
3615
3825
|
const url = new URL(c.req.url);
|
|
3616
3826
|
await deps.handleBrowse(url, c.env.outgoing);
|
|
@@ -3624,7 +3834,7 @@ var createBrowseRoutes = (deps) => {
|
|
|
3624
3834
|
};
|
|
3625
3835
|
|
|
3626
3836
|
// src/api/routes/cacheAlert.routes.ts
|
|
3627
|
-
import { Hono as
|
|
3837
|
+
import { Hono as Hono4 } from "hono";
|
|
3628
3838
|
|
|
3629
3839
|
// src/schemas/cacheAlert.schema.ts
|
|
3630
3840
|
import { z } from "zod";
|
|
@@ -3647,7 +3857,7 @@ function readRawBody2(req) {
|
|
|
3647
3857
|
});
|
|
3648
3858
|
}
|
|
3649
3859
|
var createCacheAlertRoutes = (deps) => {
|
|
3650
|
-
const app = new
|
|
3860
|
+
const app = new Hono4();
|
|
3651
3861
|
app.get("/", (c) => {
|
|
3652
3862
|
const monitor = deps.cacheMonitor();
|
|
3653
3863
|
return c.json({ pending: monitor?.pending ?? null });
|
|
@@ -3684,7 +3894,7 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
3684
3894
|
};
|
|
3685
3895
|
|
|
3686
3896
|
// src/api/routes/config.routes.ts
|
|
3687
|
-
import { Hono as
|
|
3897
|
+
import { Hono as Hono5 } from "hono";
|
|
3688
3898
|
|
|
3689
3899
|
// src/schemas/claudeFlags.schema.ts
|
|
3690
3900
|
import { z as z2 } from "zod";
|
|
@@ -3705,7 +3915,7 @@ function readRawBody3(req) {
|
|
|
3705
3915
|
});
|
|
3706
3916
|
}
|
|
3707
3917
|
var createConfigRoutes = (deps) => {
|
|
3708
|
-
const app = new
|
|
3918
|
+
const app = new Hono5();
|
|
3709
3919
|
app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
|
|
3710
3920
|
app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
|
|
3711
3921
|
app.put("/claude-flags", async (c) => {
|
|
@@ -3740,11 +3950,11 @@ var createConfigRoutes = (deps) => {
|
|
|
3740
3950
|
};
|
|
3741
3951
|
|
|
3742
3952
|
// src/api/routes/conversations.routes.ts
|
|
3743
|
-
import { Hono as
|
|
3953
|
+
import { Hono as Hono6 } from "hono";
|
|
3744
3954
|
var ALREADY_HANDLED2 = 597;
|
|
3745
3955
|
var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
|
|
3746
3956
|
var createConversationRoutes = (deps) => {
|
|
3747
|
-
const app = new
|
|
3957
|
+
const app = new Hono6();
|
|
3748
3958
|
app.get("/count", async (c) => {
|
|
3749
3959
|
const url = new URL(c.req.url);
|
|
3750
3960
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
@@ -3771,9 +3981,9 @@ var createConversationRoutes = (deps) => {
|
|
|
3771
3981
|
};
|
|
3772
3982
|
|
|
3773
3983
|
// src/api/routes/devices.routes.ts
|
|
3774
|
-
import { Hono as
|
|
3984
|
+
import { Hono as Hono7 } from "hono";
|
|
3775
3985
|
var createDeviceRoutes = (deps) => {
|
|
3776
|
-
const app = new
|
|
3986
|
+
const app = new Hono7();
|
|
3777
3987
|
app.get("/", (c) => {
|
|
3778
3988
|
const repo = deps.devicesRepo();
|
|
3779
3989
|
if (!repo) return c.json({ devices: [], available: false });
|
|
@@ -3796,45 +4006,134 @@ var createDeviceRoutes = (deps) => {
|
|
|
3796
4006
|
return app;
|
|
3797
4007
|
};
|
|
3798
4008
|
|
|
3799
|
-
// src/api/routes/
|
|
3800
|
-
import {
|
|
4009
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4010
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4011
|
+
import { Hono as Hono8 } from "hono";
|
|
3801
4012
|
|
|
3802
|
-
// src/
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
if (
|
|
3808
|
-
|
|
3809
|
-
|
|
4013
|
+
// src/services/diagnostics/diagnostics.ts
|
|
4014
|
+
var DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
4015
|
+
function redactPath(path) {
|
|
4016
|
+
if (!path) return null;
|
|
4017
|
+
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
4018
|
+
if (parts.length <= 2) return parts.join("/");
|
|
4019
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
4020
|
+
}
|
|
4021
|
+
function worstStatus(checks) {
|
|
4022
|
+
const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
|
|
4023
|
+
return checks.reduce(
|
|
4024
|
+
(worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
|
|
4025
|
+
"ok"
|
|
4026
|
+
);
|
|
3810
4027
|
}
|
|
3811
|
-
function
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
}
|
|
4028
|
+
function buildReport(checks, now = /* @__PURE__ */ new Date()) {
|
|
4029
|
+
return {
|
|
4030
|
+
contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
|
|
4031
|
+
generatedAt: now.toISOString(),
|
|
4032
|
+
overall: worstStatus(checks),
|
|
4033
|
+
checks
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
|
|
4037
|
+
function redactValue(value) {
|
|
4038
|
+
if (Array.isArray(value)) {
|
|
4039
|
+
return value.map((v) => redactValue(v));
|
|
3818
4040
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (v) return v;
|
|
3824
|
-
} catch {
|
|
4041
|
+
if (value && typeof value === "object") {
|
|
4042
|
+
const out = {};
|
|
4043
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4044
|
+
out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
|
|
3825
4045
|
}
|
|
4046
|
+
return out;
|
|
3826
4047
|
}
|
|
4048
|
+
return value;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
// src/api/routes/diagnostics.routes.ts
|
|
4052
|
+
function providerCheck(name, resolve2) {
|
|
3827
4053
|
try {
|
|
3828
|
-
const
|
|
3829
|
-
|
|
4054
|
+
const exe = resolve2();
|
|
4055
|
+
return {
|
|
4056
|
+
id: `provider:${name}`,
|
|
4057
|
+
status: "ok",
|
|
4058
|
+
summary: `${name} CLI is installed.`,
|
|
4059
|
+
remediation: "NONE",
|
|
4060
|
+
detail: { location: redactPath(exe) }
|
|
4061
|
+
};
|
|
3830
4062
|
} catch {
|
|
4063
|
+
return {
|
|
4064
|
+
id: `provider:${name}`,
|
|
4065
|
+
status: "failed",
|
|
4066
|
+
summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
|
|
4067
|
+
remediation: "PROVIDER_NOT_INSTALLED"
|
|
4068
|
+
};
|
|
3831
4069
|
}
|
|
3832
|
-
return "0.0.0+unknown";
|
|
3833
4070
|
}
|
|
4071
|
+
var createDiagnosticsRoutes = (deps) => {
|
|
4072
|
+
const app = new Hono8();
|
|
4073
|
+
app.get("/", (c) => {
|
|
4074
|
+
const checks = [];
|
|
4075
|
+
checks.push({
|
|
4076
|
+
id: "streamer",
|
|
4077
|
+
status: "ok",
|
|
4078
|
+
summary: "Streamer is running.",
|
|
4079
|
+
remediation: "NONE",
|
|
4080
|
+
detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
|
|
4081
|
+
});
|
|
4082
|
+
checks.push(providerCheck("claude-code", resolveClaudeExe));
|
|
4083
|
+
checks.push(providerCheck("codex-cli", resolveCodexExe));
|
|
4084
|
+
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
4085
|
+
checks.push(
|
|
4086
|
+
cacheAlert ? {
|
|
4087
|
+
id: "cache",
|
|
4088
|
+
status: "degraded",
|
|
4089
|
+
summary: "Conversation cache reported an integrity alert.",
|
|
4090
|
+
remediation: "CACHE_DEGRADED"
|
|
4091
|
+
} : {
|
|
4092
|
+
id: "cache",
|
|
4093
|
+
status: "ok",
|
|
4094
|
+
summary: "Conversation cache is healthy.",
|
|
4095
|
+
remediation: "NONE"
|
|
4096
|
+
}
|
|
4097
|
+
);
|
|
4098
|
+
let ptyOk = true;
|
|
4099
|
+
try {
|
|
4100
|
+
__require.resolve("node-pty");
|
|
4101
|
+
} catch {
|
|
4102
|
+
ptyOk = false;
|
|
4103
|
+
}
|
|
4104
|
+
checks.push(
|
|
4105
|
+
ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
|
|
4106
|
+
id: "pty",
|
|
4107
|
+
status: "failed",
|
|
4108
|
+
summary: "node-pty failed to load, so no managed session can start.",
|
|
4109
|
+
remediation: "PTY_UNAVAILABLE"
|
|
4110
|
+
}
|
|
4111
|
+
);
|
|
4112
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4113
|
+
const claudeProjects = home ? `${home}/.claude/projects` : "";
|
|
4114
|
+
checks.push(
|
|
4115
|
+
claudeProjects && existsSync5(claudeProjects) ? {
|
|
4116
|
+
id: "filesystem",
|
|
4117
|
+
status: "ok",
|
|
4118
|
+
summary: "Provider history directory is present.",
|
|
4119
|
+
remediation: "NONE",
|
|
4120
|
+
detail: { location: redactPath(claudeProjects) }
|
|
4121
|
+
} : {
|
|
4122
|
+
id: "filesystem",
|
|
4123
|
+
status: "degraded",
|
|
4124
|
+
summary: "Provider history directory was not found; history may be unavailable.",
|
|
4125
|
+
remediation: "FS_SCOPE_MISSING"
|
|
4126
|
+
}
|
|
4127
|
+
);
|
|
4128
|
+
return c.json(redactValue(buildReport(checks)));
|
|
4129
|
+
});
|
|
4130
|
+
return app;
|
|
4131
|
+
};
|
|
3834
4132
|
|
|
3835
4133
|
// src/api/routes/health.routes.ts
|
|
4134
|
+
import { Hono as Hono9 } from "hono";
|
|
3836
4135
|
var createHealthRoutes = (deps) => {
|
|
3837
|
-
const app = new
|
|
4136
|
+
const app = new Hono9();
|
|
3838
4137
|
app.get("/", (c) => {
|
|
3839
4138
|
const cacheAlert = deps.cacheMonitor()?.healthzField();
|
|
3840
4139
|
return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
|
|
@@ -3843,9 +4142,9 @@ var createHealthRoutes = (deps) => {
|
|
|
3843
4142
|
};
|
|
3844
4143
|
|
|
3845
4144
|
// src/api/routes/logs.routes.ts
|
|
3846
|
-
import { closeSync, existsSync as
|
|
4145
|
+
import { closeSync, existsSync as existsSync6, fstatSync, openSync, readSync, statSync } from "fs";
|
|
3847
4146
|
import { join as join9 } from "path";
|
|
3848
|
-
import { Hono as
|
|
4147
|
+
import { Hono as Hono10 } from "hono";
|
|
3849
4148
|
|
|
3850
4149
|
// src/lifecycle/constants.ts
|
|
3851
4150
|
import { homedir as homedir4 } from "os";
|
|
@@ -3863,12 +4162,12 @@ function resolveLogPath(source) {
|
|
|
3863
4162
|
function pickDefaultSource() {
|
|
3864
4163
|
for (const source of ["stdout", "stderr", "dev"]) {
|
|
3865
4164
|
const p = resolveLogPath(source);
|
|
3866
|
-
if (
|
|
4165
|
+
if (existsSync6(p) && statSync(p).size > 0) return source;
|
|
3867
4166
|
}
|
|
3868
4167
|
return "stdout";
|
|
3869
4168
|
}
|
|
3870
4169
|
function readLogLines(filePath, sinceOffset, limit) {
|
|
3871
|
-
if (!
|
|
4170
|
+
if (!existsSync6(filePath)) {
|
|
3872
4171
|
return { lines: [], offset: 0, total: 0 };
|
|
3873
4172
|
}
|
|
3874
4173
|
const fd = openSync(filePath, "r");
|
|
@@ -3903,7 +4202,7 @@ function readLogLines(filePath, sinceOffset, limit) {
|
|
|
3903
4202
|
}
|
|
3904
4203
|
}
|
|
3905
4204
|
function createLogsRoutes() {
|
|
3906
|
-
const app = new
|
|
4205
|
+
const app = new Hono10();
|
|
3907
4206
|
app.get("/", (c) => {
|
|
3908
4207
|
try {
|
|
3909
4208
|
const sourceParam = (c.req.query("source") || "").toLowerCase();
|
|
@@ -3911,7 +4210,7 @@ function createLogsRoutes() {
|
|
|
3911
4210
|
const logPath = resolveLogPath(source);
|
|
3912
4211
|
const sinceOffset = parseInt(c.req.query("since") || "0", 10);
|
|
3913
4212
|
const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
|
|
3914
|
-
if (!
|
|
4213
|
+
if (!existsSync6(logPath)) {
|
|
3915
4214
|
return c.json({
|
|
3916
4215
|
logs: [],
|
|
3917
4216
|
message: `No log file found for source=${source}`,
|
|
@@ -3948,7 +4247,7 @@ function createLogsRoutes() {
|
|
|
3948
4247
|
try {
|
|
3949
4248
|
const sources = ["stdout", "stderr", "dev"].map((source) => {
|
|
3950
4249
|
const logPath = resolveLogPath(source);
|
|
3951
|
-
if (!
|
|
4250
|
+
if (!existsSync6(logPath)) {
|
|
3952
4251
|
return { source, exists: false, total: 0, fileSize: 0 };
|
|
3953
4252
|
}
|
|
3954
4253
|
const stats = statSync(logPath);
|
|
@@ -3974,8 +4273,8 @@ function createLogsRoutes() {
|
|
|
3974
4273
|
// src/api/routes/misc.routes.ts
|
|
3975
4274
|
import { spawn } from "child_process";
|
|
3976
4275
|
import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3977
|
-
import { Hono as
|
|
3978
|
-
import { hostname } from "os";
|
|
4276
|
+
import { Hono as Hono11 } from "hono";
|
|
4277
|
+
import { hostname as hostname2 } from "os";
|
|
3979
4278
|
|
|
3980
4279
|
// src/config/update-config.ts
|
|
3981
4280
|
import { readFileSync as readFileSync5 } from "fs";
|
|
@@ -4305,12 +4604,12 @@ function verifyWebhookSignature(body, header, secret) {
|
|
|
4305
4604
|
}
|
|
4306
4605
|
var clientLog = getLogger("client");
|
|
4307
4606
|
var createMiscRoutes = (deps) => {
|
|
4308
|
-
const app = new
|
|
4607
|
+
const app = new Hono11();
|
|
4309
4608
|
app.get("/api/info", (c) => {
|
|
4310
4609
|
const ptyIds = deps.ptyAttachedIds();
|
|
4311
4610
|
return c.json({
|
|
4312
4611
|
version: getVersion(),
|
|
4313
|
-
machineName:
|
|
4612
|
+
machineName: hostname2(),
|
|
4314
4613
|
platform: process.platform,
|
|
4315
4614
|
activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
|
|
4316
4615
|
publicUrl: deps.publicUrl,
|
|
@@ -4436,11 +4735,11 @@ var createMiscRoutes = (deps) => {
|
|
|
4436
4735
|
};
|
|
4437
4736
|
|
|
4438
4737
|
// src/api/routes/pair.routes.ts
|
|
4439
|
-
import { Hono as
|
|
4738
|
+
import { Hono as Hono12 } from "hono";
|
|
4440
4739
|
var ALREADY_HANDLED3 = 597;
|
|
4441
4740
|
var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
|
|
4442
4741
|
var createPairRoutes = (deps) => {
|
|
4443
|
-
const app = new
|
|
4742
|
+
const app = new Hono12();
|
|
4444
4743
|
app.post("/start", (c) => {
|
|
4445
4744
|
deps.handlePairStart(c.env.outgoing);
|
|
4446
4745
|
return alreadyHandled3();
|
|
@@ -4453,11 +4752,11 @@ var createPairRoutes = (deps) => {
|
|
|
4453
4752
|
};
|
|
4454
4753
|
|
|
4455
4754
|
// src/api/routes/projects.routes.ts
|
|
4456
|
-
import { Hono as
|
|
4755
|
+
import { Hono as Hono13 } from "hono";
|
|
4457
4756
|
var ALREADY_HANDLED4 = 597;
|
|
4458
4757
|
var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
|
|
4459
4758
|
var createProjectRoutes = (deps) => {
|
|
4460
|
-
const app = new
|
|
4759
|
+
const app = new Hono13();
|
|
4461
4760
|
app.get("/", (c) => {
|
|
4462
4761
|
const url = new URL(c.req.url);
|
|
4463
4762
|
deps.handleListProjects(url, c.env.outgoing);
|
|
@@ -4472,7 +4771,7 @@ var createProjectRoutes = (deps) => {
|
|
|
4472
4771
|
};
|
|
4473
4772
|
|
|
4474
4773
|
// src/api/routes/providers.routes.ts
|
|
4475
|
-
import { Hono as
|
|
4774
|
+
import { Hono as Hono14 } from "hono";
|
|
4476
4775
|
|
|
4477
4776
|
// src/services/providers/providerHealth.ts
|
|
4478
4777
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -4595,7 +4894,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
|
|
|
4595
4894
|
|
|
4596
4895
|
// src/api/routes/providers.routes.ts
|
|
4597
4896
|
var createProviderRoutes = () => {
|
|
4598
|
-
const app = new
|
|
4897
|
+
const app = new Hono14();
|
|
4599
4898
|
app.get("/", async (c) => {
|
|
4600
4899
|
const providers = await Promise.all([
|
|
4601
4900
|
providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
|
|
@@ -4607,11 +4906,11 @@ var createProviderRoutes = () => {
|
|
|
4607
4906
|
};
|
|
4608
4907
|
|
|
4609
4908
|
// src/api/routes/scanner.routes.ts
|
|
4610
|
-
import { Hono as
|
|
4909
|
+
import { Hono as Hono15 } from "hono";
|
|
4611
4910
|
var ALREADY_HANDLED5 = 597;
|
|
4612
4911
|
var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
|
|
4613
4912
|
var createScannerRoutes = (deps) => {
|
|
4614
|
-
const app = new
|
|
4913
|
+
const app = new Hono15();
|
|
4615
4914
|
app.get("/api/search", async (c) => {
|
|
4616
4915
|
const url = new URL(c.req.url);
|
|
4617
4916
|
await deps.handleSearch(url, c.env.outgoing);
|
|
@@ -4621,11 +4920,11 @@ var createScannerRoutes = (deps) => {
|
|
|
4621
4920
|
};
|
|
4622
4921
|
|
|
4623
4922
|
// src/api/routes/sessions.routes.ts
|
|
4624
|
-
import { Hono as
|
|
4923
|
+
import { Hono as Hono16 } from "hono";
|
|
4625
4924
|
var ALREADY_HANDLED6 = 597;
|
|
4626
4925
|
var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
|
|
4627
4926
|
var createSessionRoutes = (deps) => {
|
|
4628
|
-
const app = new
|
|
4927
|
+
const app = new Hono16();
|
|
4629
4928
|
app.get("/count", (c) => {
|
|
4630
4929
|
deps.handleSessionsCount(c.env.outgoing);
|
|
4631
4930
|
return alreadyHandled6();
|
|
@@ -4700,9 +4999,9 @@ var createSessionRoutes = (deps) => {
|
|
|
4700
4999
|
};
|
|
4701
5000
|
|
|
4702
5001
|
// src/api/routes/ws.routes.ts
|
|
4703
|
-
import { Hono as
|
|
5002
|
+
import { Hono as Hono17 } from "hono";
|
|
4704
5003
|
var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
4705
|
-
const app = new
|
|
5004
|
+
const app = new Hono17();
|
|
4706
5005
|
app.get(
|
|
4707
5006
|
"/ws",
|
|
4708
5007
|
upgradeWebSocket(() => {
|
|
@@ -4728,7 +5027,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
4728
5027
|
|
|
4729
5028
|
// src/api/app.ts
|
|
4730
5029
|
var createHonoApp = (deps, upgradeWebSocket) => {
|
|
4731
|
-
const app = new
|
|
5030
|
+
const app = new Hono18();
|
|
4732
5031
|
const httpLog = getLogger("http");
|
|
4733
5032
|
app.use("*", async (c, next) => {
|
|
4734
5033
|
const start = Date.now();
|
|
@@ -4749,6 +5048,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4749
5048
|
app.use("*", authMiddleware(deps));
|
|
4750
5049
|
app.onError(errorMiddleware);
|
|
4751
5050
|
app.route("/healthz", createHealthRoutes(deps));
|
|
5051
|
+
app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
|
|
4752
5052
|
app.route("/", createMiscRoutes(deps));
|
|
4753
5053
|
app.route("/api/sessions", createSessionRoutes(deps));
|
|
4754
5054
|
app.route("/api/conversations", createConversationRoutes(deps));
|
|
@@ -4757,6 +5057,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
4757
5057
|
app.route("/api/projects", createProjectRoutes(deps));
|
|
4758
5058
|
app.route("/api/providers", createProviderRoutes());
|
|
4759
5059
|
app.route("/api/devices", createDeviceRoutes(deps));
|
|
5060
|
+
app.route("/api/backup", createBackupRoutes(deps));
|
|
4760
5061
|
app.route("/api/pair", createPairRoutes(deps));
|
|
4761
5062
|
app.route("/api", createBrowseRoutes(deps));
|
|
4762
5063
|
app.route("/", createScannerRoutes(deps));
|
|
@@ -4825,7 +5126,7 @@ import {
|
|
|
4825
5126
|
parseJsonlLine
|
|
4826
5127
|
} from "@threadbase-sh/scanner";
|
|
4827
5128
|
import Database from "better-sqlite3";
|
|
4828
|
-
import { closeSync as closeSync3, existsSync as
|
|
5129
|
+
import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
|
|
4829
5130
|
import { open as openAsync } from "fs/promises";
|
|
4830
5131
|
import { dirname as dirname7 } from "path";
|
|
4831
5132
|
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
@@ -6057,7 +6358,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6057
6358
|
* `handleGetConversation` can still serve the cached tail even when the
|
|
6058
6359
|
* JSONL has been deleted.
|
|
6059
6360
|
*/
|
|
6060
|
-
pruneGhostFiles(exists =
|
|
6361
|
+
pruneGhostFiles(exists = existsSync7) {
|
|
6061
6362
|
const rows = this.stmts.allFilePaths.all();
|
|
6062
6363
|
const ghosts = [];
|
|
6063
6364
|
const prune = this.db.transaction((ids) => {
|
|
@@ -6112,7 +6413,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6112
6413
|
* Returns the removed IDs.
|
|
6113
6414
|
*/
|
|
6114
6415
|
reconcileDeletions(livePaths, opts) {
|
|
6115
|
-
const exists = opts?.exists ??
|
|
6416
|
+
const exists = opts?.exists ?? existsSync7;
|
|
6116
6417
|
const rows = this.stmts.allFilePaths.all();
|
|
6117
6418
|
const removed = [];
|
|
6118
6419
|
const drop = this.db.transaction((ids) => {
|
|
@@ -6147,7 +6448,7 @@ var ConversationCache = class _ConversationCache {
|
|
|
6147
6448
|
* reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
|
|
6148
6449
|
* rows that still have cached history (which pruneGhostFiles would keep).
|
|
6149
6450
|
*/
|
|
6150
|
-
listMissingFiles(exists =
|
|
6451
|
+
listMissingFiles(exists = existsSync7) {
|
|
6151
6452
|
const rows = this.stmts.allFilePathsWithTitle.all();
|
|
6152
6453
|
const missing = [];
|
|
6153
6454
|
for (const row of rows) {
|
|
@@ -6648,7 +6949,7 @@ function setCacheMetadata(repo, key, value) {
|
|
|
6648
6949
|
|
|
6649
6950
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
6650
6951
|
import { createHash as createHash3 } from "crypto";
|
|
6651
|
-
import { existsSync as
|
|
6952
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6652
6953
|
|
|
6653
6954
|
// src/services/cache-integrity/alertStore.ts
|
|
6654
6955
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -6674,7 +6975,7 @@ function saveAlertState(state) {
|
|
|
6674
6975
|
}
|
|
6675
6976
|
|
|
6676
6977
|
// src/services/cache-integrity/backup.ts
|
|
6677
|
-
import { existsSync as
|
|
6978
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync5, unlinkSync } from "fs";
|
|
6678
6979
|
import { join as join15 } from "path";
|
|
6679
6980
|
var DEFAULT_RETAIN = 3;
|
|
6680
6981
|
function retainCount() {
|
|
@@ -6696,7 +6997,7 @@ async function backupCacheDb(db, cacheDir) {
|
|
|
6696
6997
|
return { full, mtime: statSync5(full).mtimeMs };
|
|
6697
6998
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6698
6999
|
for (const stale of backups.slice(retain)) {
|
|
6699
|
-
if (
|
|
7000
|
+
if (existsSync8(stale.full)) unlinkSync(stale.full);
|
|
6700
7001
|
}
|
|
6701
7002
|
return destPath;
|
|
6702
7003
|
}
|
|
@@ -6793,7 +7094,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6793
7094
|
* the pending record, back up on high severity, and broadcast the alert.
|
|
6794
7095
|
*/
|
|
6795
7096
|
async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6796
|
-
const all = this.cache.listMissingFiles(
|
|
7097
|
+
const all = this.cache.listMissingFiles(existsSync9);
|
|
6797
7098
|
const missing = all.filter((m) => !this.ignoredIds.has(m.id));
|
|
6798
7099
|
if (missing.length === 0) {
|
|
6799
7100
|
if (this._pending) {
|
|
@@ -6897,7 +7198,7 @@ var CacheIntegrityMonitor = class {
|
|
|
6897
7198
|
case "prune_all": {
|
|
6898
7199
|
await this.ensureBackup(pending);
|
|
6899
7200
|
const backupPath = pending.backupPath;
|
|
6900
|
-
const stillMissing = pending.missing.filter((m) => !
|
|
7201
|
+
const stillMissing = pending.missing.filter((m) => !existsSync9(m.filePath)).map((m) => m.id);
|
|
6901
7202
|
const pruned = this.cache.dropRowsById(stillMissing);
|
|
6902
7203
|
this.applyDeferredUnlinks();
|
|
6903
7204
|
this.clearPending();
|
|
@@ -7167,14 +7468,14 @@ function findSearchTarget(messages, query) {
|
|
|
7167
7468
|
}
|
|
7168
7469
|
|
|
7169
7470
|
// src/services/conversations/pruneAgentConversations.ts
|
|
7170
|
-
import { existsSync as
|
|
7471
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7171
7472
|
function pruneAgentConversations(cache) {
|
|
7172
7473
|
const db = cache.getDatabase();
|
|
7173
7474
|
const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
|
|
7174
7475
|
let pruned = 0;
|
|
7175
7476
|
let missing = 0;
|
|
7176
7477
|
for (const row of rows) {
|
|
7177
|
-
if (!
|
|
7478
|
+
if (!existsSync10(row.file_path)) {
|
|
7178
7479
|
missing += 1;
|
|
7179
7480
|
continue;
|
|
7180
7481
|
}
|
|
@@ -8051,6 +8352,88 @@ function resolveAnswer(pending, body) {
|
|
|
8051
8352
|
}
|
|
8052
8353
|
}
|
|
8053
8354
|
|
|
8355
|
+
// src/services/search/searchQuery.ts
|
|
8356
|
+
var DEFAULT_SEARCH_LIMIT = 50;
|
|
8357
|
+
var MAX_SEARCH_LIMIT = 200;
|
|
8358
|
+
var MAX_QUERY_LENGTH = 256;
|
|
8359
|
+
var SearchQueryError = class extends Error {
|
|
8360
|
+
constructor(message, code) {
|
|
8361
|
+
super(message);
|
|
8362
|
+
this.code = code;
|
|
8363
|
+
}
|
|
8364
|
+
code;
|
|
8365
|
+
};
|
|
8366
|
+
function intOr(raw, fallback) {
|
|
8367
|
+
if (raw === null) return fallback;
|
|
8368
|
+
const n = Number.parseInt(raw, 10);
|
|
8369
|
+
return Number.isFinite(n) ? n : fallback;
|
|
8370
|
+
}
|
|
8371
|
+
function parseSearchQuery(params) {
|
|
8372
|
+
const q = (params.get("q") ?? "").trim();
|
|
8373
|
+
if (!q) {
|
|
8374
|
+
throw new SearchQueryError("Missing query parameter: q", "invalid_query");
|
|
8375
|
+
}
|
|
8376
|
+
if (q.length > MAX_QUERY_LENGTH) {
|
|
8377
|
+
throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
|
|
8378
|
+
}
|
|
8379
|
+
const limit = Math.min(
|
|
8380
|
+
Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
|
|
8381
|
+
MAX_SEARCH_LIMIT
|
|
8382
|
+
);
|
|
8383
|
+
const offset = Math.max(intOr(params.get("offset"), 0), 0);
|
|
8384
|
+
const filters = {};
|
|
8385
|
+
const provider = params.get("provider");
|
|
8386
|
+
if (provider !== null) {
|
|
8387
|
+
if (!isProviderName(provider)) {
|
|
8388
|
+
throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
|
|
8389
|
+
}
|
|
8390
|
+
filters.provider = provider;
|
|
8391
|
+
}
|
|
8392
|
+
const projectPath = params.get("projectPath");
|
|
8393
|
+
if (projectPath) filters.projectPath = projectPath;
|
|
8394
|
+
const branch = params.get("branch");
|
|
8395
|
+
if (branch) filters.branch = branch;
|
|
8396
|
+
for (const [key, field] of [
|
|
8397
|
+
["since", "since"],
|
|
8398
|
+
["until", "until"]
|
|
8399
|
+
]) {
|
|
8400
|
+
const raw = params.get(key);
|
|
8401
|
+
if (raw === null) continue;
|
|
8402
|
+
const ms = Date.parse(raw);
|
|
8403
|
+
if (Number.isNaN(ms)) {
|
|
8404
|
+
throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
|
|
8405
|
+
}
|
|
8406
|
+
filters[field] = ms;
|
|
8407
|
+
}
|
|
8408
|
+
if (filters.since != null && filters.until != null && filters.since > filters.until) {
|
|
8409
|
+
throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
|
|
8410
|
+
}
|
|
8411
|
+
return { q, limit, offset, filters };
|
|
8412
|
+
}
|
|
8413
|
+
function applyFilters(results, filters) {
|
|
8414
|
+
return results.filter((r) => {
|
|
8415
|
+
if (filters.provider && r.provider !== filters.provider) return false;
|
|
8416
|
+
if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
|
|
8417
|
+
if (filters.branch && r.branch !== filters.branch) return false;
|
|
8418
|
+
if (filters.since != null || filters.until != null) {
|
|
8419
|
+
const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
|
|
8420
|
+
if (Number.isNaN(ts)) return false;
|
|
8421
|
+
if (filters.since != null && ts < filters.since) return false;
|
|
8422
|
+
if (filters.until != null && ts > filters.until) return false;
|
|
8423
|
+
}
|
|
8424
|
+
return true;
|
|
8425
|
+
});
|
|
8426
|
+
}
|
|
8427
|
+
function paginate(results, offset, limit) {
|
|
8428
|
+
const items = results.slice(offset, offset + limit);
|
|
8429
|
+
return {
|
|
8430
|
+
items,
|
|
8431
|
+
total: results.length,
|
|
8432
|
+
offset,
|
|
8433
|
+
hasMore: offset + items.length < results.length
|
|
8434
|
+
};
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8054
8437
|
// src/services/sessions/conversationBusy.ts
|
|
8055
8438
|
import { statSync as statSync8 } from "fs";
|
|
8056
8439
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
@@ -8753,6 +9136,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
|
8753
9136
|
var GRACE_MAX_DEFERS = 4;
|
|
8754
9137
|
var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
|
|
8755
9138
|
var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
|
|
9139
|
+
var SEARCH_OVERFETCH = 4;
|
|
9140
|
+
var SEARCH_MAX_SCAN = 1e3;
|
|
8756
9141
|
var RESUME_DISCOVERY_TIMEOUT_MS = 750;
|
|
8757
9142
|
var DISCOVERY_TTL_MS = 15e3;
|
|
8758
9143
|
var ADOPT_KILL_TIMEOUT_MS = 5e3;
|
|
@@ -8830,6 +9215,20 @@ var StreamerServer = class {
|
|
|
8830
9215
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
8831
9216
|
// a single rescan after the current one completes instead of restarting it.
|
|
8832
9217
|
scannerStale = false;
|
|
9218
|
+
// WHICH files scannerStale is about. A directory event names exactly one
|
|
9219
|
+
// JSONL, and the only correct response is refreshFile() on that one file —
|
|
9220
|
+
// but scannerStale alone carries no identity, so honoring it used to mean a
|
|
9221
|
+
// full-tree rescan. On the non-persistent scanner this server actually runs
|
|
9222
|
+
// (buildStatCache => persistent:false, see listen()), scan() opens by
|
|
9223
|
+
// clearing metadataCache AND conversationLRU — so one live session appending
|
|
9224
|
+
// to its own transcript threw away every OTHER conversation's parsed
|
|
9225
|
+
// snapshot, and the next full fetch of an unrelated conversation re-parsed
|
|
9226
|
+
// it from disk (745-2877ms on a 5MB/1112-message history) while the
|
|
9227
|
+
// per-file paginated path stayed at ~20ms throughout. Populated alongside
|
|
9228
|
+
// scannerStale and drained with it by takeStaleFiles(); an armed flag with
|
|
9229
|
+
// an EMPTY set means "stale, source unknown" and still falls back to the
|
|
9230
|
+
// full rescan.
|
|
9231
|
+
staleFiles = /* @__PURE__ */ new Set();
|
|
8833
9232
|
// Single-flight guard for the background disk reconcile: a burst of list
|
|
8834
9233
|
// polls during active session writes shares one rescan instead of queueing
|
|
8835
9234
|
// a full rescan per request.
|
|
@@ -8998,7 +9397,10 @@ var StreamerServer = class {
|
|
|
8998
9397
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
8999
9398
|
this.markScannerStaleDebounced = debounce(() => {
|
|
9000
9399
|
if (this.scannerReady) this.scannerStale = true;
|
|
9001
|
-
else
|
|
9400
|
+
else {
|
|
9401
|
+
this.scanner = null;
|
|
9402
|
+
this.staleFiles.clear();
|
|
9403
|
+
}
|
|
9002
9404
|
}, this.directoryDebounceMs);
|
|
9003
9405
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
9004
9406
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -9091,6 +9493,7 @@ var StreamerServer = class {
|
|
|
9091
9493
|
if (!tailed) this.maybeAttachExternalTail(filePath);
|
|
9092
9494
|
this.sweepIdleExternalTails();
|
|
9093
9495
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
9496
|
+
this.staleFiles.add(filePath);
|
|
9094
9497
|
this.markScannerStaleDebounced();
|
|
9095
9498
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
9096
9499
|
filePath,
|
|
@@ -9503,14 +9906,14 @@ var StreamerServer = class {
|
|
|
9503
9906
|
}
|
|
9504
9907
|
this.apnsClient = new ApnsClient(creds);
|
|
9505
9908
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
9506
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ??
|
|
9507
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId,
|
|
9909
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname3();
|
|
9910
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname3());
|
|
9508
9911
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
9509
9912
|
repo: pushRepo,
|
|
9510
9913
|
sender,
|
|
9511
9914
|
sessionStore: this.sessionStore,
|
|
9512
9915
|
serverId,
|
|
9513
|
-
serverLabel:
|
|
9916
|
+
serverLabel: hostname3()
|
|
9514
9917
|
});
|
|
9515
9918
|
this.liveActivityRenewal.start();
|
|
9516
9919
|
this.log.info("Live Activity push enabled", {
|
|
@@ -9849,7 +10252,7 @@ var StreamerServer = class {
|
|
|
9849
10252
|
this.fileWatcher.watchDirectory(dir);
|
|
9850
10253
|
}
|
|
9851
10254
|
for (const dir of this.codexRoots) {
|
|
9852
|
-
if (!
|
|
10255
|
+
if (!existsSync11(dir)) continue;
|
|
9853
10256
|
this.fileWatcher.watchDirectory(dir);
|
|
9854
10257
|
}
|
|
9855
10258
|
} catch (err) {
|
|
@@ -10122,7 +10525,7 @@ var StreamerServer = class {
|
|
|
10122
10525
|
}
|
|
10123
10526
|
let body;
|
|
10124
10527
|
try {
|
|
10125
|
-
body = await
|
|
10528
|
+
body = await readBody2(req);
|
|
10126
10529
|
} catch (err) {
|
|
10127
10530
|
const message = err instanceof Error ? err.message : "Invalid body";
|
|
10128
10531
|
json(res, 400, { error: message });
|
|
@@ -10168,7 +10571,7 @@ var StreamerServer = class {
|
|
|
10168
10571
|
nonce: sealed.nonce,
|
|
10169
10572
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
10170
10573
|
publicUrl: this.publicUrl,
|
|
10171
|
-
machineName:
|
|
10574
|
+
machineName: hostname3(),
|
|
10172
10575
|
...device && {
|
|
10173
10576
|
deviceId: device.deviceId,
|
|
10174
10577
|
deviceToken: device.deviceToken,
|
|
@@ -10337,21 +10740,46 @@ var StreamerServer = class {
|
|
|
10337
10740
|
// so a burst of list polls during active session writes shares one rescan
|
|
10338
10741
|
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
10339
10742
|
// in-flight cache write before shutting the DB.
|
|
10340
|
-
startBackgroundConversationReconcile() {
|
|
10743
|
+
startBackgroundConversationReconcile(mode = "full") {
|
|
10341
10744
|
if (this.conversationReconcileInFlight) return;
|
|
10342
|
-
const
|
|
10745
|
+
const paths = mode === "files" ? this.takeStaleFiles() : [];
|
|
10746
|
+
const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
|
|
10343
10747
|
this.conversationReconcileInFlight = null;
|
|
10344
10748
|
});
|
|
10345
10749
|
this.conversationReconcileInFlight = task;
|
|
10346
10750
|
this.trackCacheWrite(task);
|
|
10347
10751
|
}
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10752
|
+
// "files": a directory event named specific JSONLs, so refresh only those.
|
|
10753
|
+
// "full": disk drifted in ways a per-file refresh can't see (a project dir
|
|
10754
|
+
// appeared, rows vanished), so walk the tree. Order matters — the staleness
|
|
10755
|
+
// check short-circuits first so the HDD freshness probe stays off the hot
|
|
10756
|
+
// poll path, exactly as it did when this returned a boolean.
|
|
10757
|
+
conversationReconcileMode() {
|
|
10758
|
+
if (!this.cache) return null;
|
|
10759
|
+
if (this.scannerStale) return "files";
|
|
10760
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
|
|
10352
10761
|
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
10353
10762
|
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
10354
|
-
});
|
|
10763
|
+
}) ? "full" : null;
|
|
10764
|
+
}
|
|
10765
|
+
// The per-file half of reconcileConversationsCacheFromDisk: re-index just the
|
|
10766
|
+
// changed JSONLs and upsert their rows. No reconcileDeletions here — that
|
|
10767
|
+
// needs the whole live-path set, and deletions already have their own path
|
|
10768
|
+
// (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
|
|
10769
|
+
// HDD-freshness "full" mode.
|
|
10770
|
+
async reconcileStaleFilesFromDisk(paths) {
|
|
10771
|
+
if (!this.cache) return;
|
|
10772
|
+
const scanner = await this.getScanner(true);
|
|
10773
|
+
const metas = await this.refreshStaleFiles(scanner, paths);
|
|
10774
|
+
if (metas.length === 0) return;
|
|
10775
|
+
try {
|
|
10776
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
10777
|
+
} catch (err) {
|
|
10778
|
+
this.log.warn(
|
|
10779
|
+
`stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
10780
|
+
{ event: "conversations.reconcile_failed" }
|
|
10781
|
+
);
|
|
10782
|
+
}
|
|
10355
10783
|
}
|
|
10356
10784
|
async handleListConversations(url, res) {
|
|
10357
10785
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -10361,10 +10789,11 @@ var StreamerServer = class {
|
|
|
10361
10789
|
const project = url.searchParams.get("project") ?? void 0;
|
|
10362
10790
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
10363
10791
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
10364
|
-
|
|
10792
|
+
const reconcileMode = this.conversationReconcileMode();
|
|
10793
|
+
if (this.cache && (bustCache || reconcileMode)) {
|
|
10365
10794
|
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
10366
10795
|
if (canServeStale) {
|
|
10367
|
-
this.startBackgroundConversationReconcile();
|
|
10796
|
+
this.startBackgroundConversationReconcile(reconcileMode ?? "full");
|
|
10368
10797
|
} else {
|
|
10369
10798
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
10370
10799
|
await this.withWarmup(
|
|
@@ -10564,21 +10993,54 @@ var StreamerServer = class {
|
|
|
10564
10993
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
10565
10994
|
);
|
|
10566
10995
|
}
|
|
10996
|
+
// Drain the stale set and disarm the flag together. The caller owns the
|
|
10997
|
+
// returned paths: clearing before the refresh means events that land DURING
|
|
10998
|
+
// it re-arm the flag and get their own pass instead of being swallowed.
|
|
10999
|
+
takeStaleFiles() {
|
|
11000
|
+
const paths = [...this.staleFiles];
|
|
11001
|
+
this.staleFiles.clear();
|
|
11002
|
+
this.scannerStale = false;
|
|
11003
|
+
return paths;
|
|
11004
|
+
}
|
|
11005
|
+
// Reconcile exactly the JSONLs a directory event named. Failures are logged
|
|
11006
|
+
// and swallowed per file: one unreadable transcript must not abort the
|
|
11007
|
+
// others, and the file simply stays on its previous snapshot until the next
|
|
11008
|
+
// event — the same outcome the full rescan gave on a parse failure.
|
|
11009
|
+
async refreshStaleFiles(scanner, paths) {
|
|
11010
|
+
const metas = await Promise.all(
|
|
11011
|
+
paths.map(
|
|
11012
|
+
(filePath) => scanner.refreshFile(filePath).catch((err) => {
|
|
11013
|
+
this.log.warn("scanner.refreshFile: failed", {
|
|
11014
|
+
event: "scanner.refresh_failed",
|
|
11015
|
+
filePath,
|
|
11016
|
+
trigger: "directory-event",
|
|
11017
|
+
err
|
|
11018
|
+
});
|
|
11019
|
+
return null;
|
|
11020
|
+
})
|
|
11021
|
+
)
|
|
11022
|
+
);
|
|
11023
|
+
return metas.filter((m) => m !== null);
|
|
11024
|
+
}
|
|
10567
11025
|
async getScanner(skipStaleRescan = false) {
|
|
10568
11026
|
if (this.scannerReady) {
|
|
10569
11027
|
await this.scannerReady;
|
|
10570
11028
|
if (this.scanner) {
|
|
10571
11029
|
if (skipStaleRescan) return this.scanner;
|
|
10572
11030
|
if (this.scannerStale) {
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
11031
|
+
const paths = this.takeStaleFiles();
|
|
11032
|
+
if (paths.length === 0) {
|
|
11033
|
+
this.scanner = null;
|
|
11034
|
+
this.scannerReady = null;
|
|
11035
|
+
return this.getScanner();
|
|
11036
|
+
}
|
|
11037
|
+
await this.refreshStaleFiles(this.scanner, paths);
|
|
11038
|
+
return this.scanner ?? this.getScanner();
|
|
10577
11039
|
}
|
|
10578
11040
|
return this.scanner;
|
|
10579
11041
|
}
|
|
10580
11042
|
}
|
|
10581
|
-
this.
|
|
11043
|
+
this.takeStaleFiles();
|
|
10582
11044
|
const statCache = this.buildStatCache(this.scanner);
|
|
10583
11045
|
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
10584
11046
|
this.allScanners.add(this.scanner);
|
|
@@ -10612,7 +11074,7 @@ var StreamerServer = class {
|
|
|
10612
11074
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
10613
11075
|
async rescanForRefresh(onProgress) {
|
|
10614
11076
|
if (this.scannerReady) await this.scannerReady;
|
|
10615
|
-
this.
|
|
11077
|
+
this.takeStaleFiles();
|
|
10616
11078
|
if (!this.scanner) {
|
|
10617
11079
|
this.scanner = new ConversationScanner();
|
|
10618
11080
|
this.allScanners.add(this.scanner);
|
|
@@ -10644,15 +11106,15 @@ var StreamerServer = class {
|
|
|
10644
11106
|
findJsonlPath(uuid) {
|
|
10645
11107
|
const filename = `${uuid}.jsonl`;
|
|
10646
11108
|
for (const projectsDir of this.projectsDirs()) {
|
|
10647
|
-
if (!
|
|
11109
|
+
if (!existsSync11(projectsDir)) continue;
|
|
10648
11110
|
for (const dir of readdirSync6(projectsDir)) {
|
|
10649
11111
|
const fp = join18(projectsDir, dir, filename);
|
|
10650
|
-
if (
|
|
11112
|
+
if (existsSync11(fp)) return fp;
|
|
10651
11113
|
const projectDir = join18(projectsDir, dir);
|
|
10652
11114
|
try {
|
|
10653
11115
|
for (const sub of readdirSync6(projectDir)) {
|
|
10654
11116
|
const subagentPath = join18(projectDir, sub, "subagents", filename);
|
|
10655
|
-
if (
|
|
11117
|
+
if (existsSync11(subagentPath)) return subagentPath;
|
|
10656
11118
|
}
|
|
10657
11119
|
} catch {
|
|
10658
11120
|
}
|
|
@@ -11189,7 +11651,7 @@ var StreamerServer = class {
|
|
|
11189
11651
|
}
|
|
11190
11652
|
let body;
|
|
11191
11653
|
try {
|
|
11192
|
-
body = await
|
|
11654
|
+
body = await readBody2(req);
|
|
11193
11655
|
} catch {
|
|
11194
11656
|
res.setHeader("Accept-Query", "application/json");
|
|
11195
11657
|
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
@@ -11227,17 +11689,27 @@ var StreamerServer = class {
|
|
|
11227
11689
|
});
|
|
11228
11690
|
}
|
|
11229
11691
|
async handleSearch(url, res) {
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11692
|
+
let parsed;
|
|
11693
|
+
try {
|
|
11694
|
+
parsed = parseSearchQuery(url.searchParams);
|
|
11695
|
+
} catch (err) {
|
|
11696
|
+
if (err instanceof SearchQueryError) {
|
|
11697
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
11698
|
+
return;
|
|
11699
|
+
}
|
|
11700
|
+
throw err;
|
|
11234
11701
|
}
|
|
11235
|
-
const
|
|
11702
|
+
const { q, limit, offset, filters } = parsed;
|
|
11703
|
+
const startedAt = Date.now();
|
|
11236
11704
|
const scanner = await this.getScanner();
|
|
11237
11705
|
const results = await search(
|
|
11238
11706
|
q,
|
|
11239
11707
|
{
|
|
11240
|
-
|
|
11708
|
+
// Fetch beyond the requested page: filters below are applied AFTER the
|
|
11709
|
+
// scanner returns, so slicing at `limit` here would drop results that a
|
|
11710
|
+
// later page should contain. Bounded so a broad query cannot pull an
|
|
11711
|
+
// unbounded set into memory.
|
|
11712
|
+
limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
|
|
11241
11713
|
include: "conversations",
|
|
11242
11714
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
11243
11715
|
...this.codexScanOpts()
|
|
@@ -11261,13 +11733,24 @@ var StreamerServer = class {
|
|
|
11261
11733
|
lastActivity: r.meta.timestamp,
|
|
11262
11734
|
firstMessage: r.meta.firstMessage ?? void 0,
|
|
11263
11735
|
lastMessage: r.meta.lastMessage ?? void 0,
|
|
11264
|
-
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
|
|
11736
|
+
provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
|
|
11737
|
+
// The scanner already computes relevance and match snippets; the previous
|
|
11738
|
+
// adapter discarded both, so results arrived in an unexplained order with
|
|
11739
|
+
// no indication of WHY anything matched.
|
|
11740
|
+
score: r.score,
|
|
11741
|
+
matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
|
|
11742
|
+
field: m.field,
|
|
11743
|
+
snippet: m.snippet
|
|
11744
|
+
})) : []
|
|
11265
11745
|
}));
|
|
11746
|
+
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
11266
11747
|
json(res, 200, {
|
|
11267
|
-
conversations:
|
|
11268
|
-
hasMore:
|
|
11269
|
-
offset:
|
|
11270
|
-
total:
|
|
11748
|
+
conversations: page.items,
|
|
11749
|
+
hasMore: page.hasMore,
|
|
11750
|
+
offset: page.offset,
|
|
11751
|
+
total: page.total,
|
|
11752
|
+
// Query timing, so a slow search is diagnosable rather than merely felt.
|
|
11753
|
+
tookMs: Date.now() - startedAt
|
|
11271
11754
|
});
|
|
11272
11755
|
}
|
|
11273
11756
|
async handleListSessions(url, res) {
|
|
@@ -11313,7 +11796,7 @@ var StreamerServer = class {
|
|
|
11313
11796
|
if (this.rejectIfWarmingUp(res)) return;
|
|
11314
11797
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
11315
11798
|
if (session) {
|
|
11316
|
-
if (!
|
|
11799
|
+
if (!existsSync11(session.projectPath)) {
|
|
11317
11800
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
11318
11801
|
}
|
|
11319
11802
|
const reconciled = this.withReconciledLifecycle([session])[0];
|
|
@@ -11340,7 +11823,7 @@ var StreamerServer = class {
|
|
|
11340
11823
|
json(res, 404, { error: "Session not found" });
|
|
11341
11824
|
}
|
|
11342
11825
|
async handleResume(req, res) {
|
|
11343
|
-
const body = await
|
|
11826
|
+
const body = await readBody2(req);
|
|
11344
11827
|
const sessionId = body.sessionId ?? body.conversationId;
|
|
11345
11828
|
if (!sessionId) {
|
|
11346
11829
|
json(res, 400, { error: "Missing sessionId" });
|
|
@@ -11472,7 +11955,7 @@ var StreamerServer = class {
|
|
|
11472
11955
|
return;
|
|
11473
11956
|
}
|
|
11474
11957
|
if (this.agentConfig.enabled) {
|
|
11475
|
-
const body2 = await
|
|
11958
|
+
const body2 = await readBody2(req);
|
|
11476
11959
|
const cache = this.cache;
|
|
11477
11960
|
if (!cache) {
|
|
11478
11961
|
json(res, 503, {
|
|
@@ -11491,7 +11974,7 @@ var StreamerServer = class {
|
|
|
11491
11974
|
json(res, result.status, result.body);
|
|
11492
11975
|
return;
|
|
11493
11976
|
}
|
|
11494
|
-
const body = await
|
|
11977
|
+
const body = await readBody2(req);
|
|
11495
11978
|
const { input, keys } = body;
|
|
11496
11979
|
let idempotencyKey;
|
|
11497
11980
|
try {
|
|
@@ -11655,7 +12138,7 @@ var StreamerServer = class {
|
|
|
11655
12138
|
});
|
|
11656
12139
|
}
|
|
11657
12140
|
async handleSendAnswer(sessionId, req, res) {
|
|
11658
|
-
const body = await
|
|
12141
|
+
const body = await readBody2(req);
|
|
11659
12142
|
const pending = this.pendingQuestions.get(sessionId);
|
|
11660
12143
|
const resolution = resolveAnswer(pending, body);
|
|
11661
12144
|
if (!resolution.ok) {
|
|
@@ -11684,7 +12167,7 @@ var StreamerServer = class {
|
|
|
11684
12167
|
json(res, 400, { error: "Session has no project path" });
|
|
11685
12168
|
return;
|
|
11686
12169
|
}
|
|
11687
|
-
const body = await
|
|
12170
|
+
const body = await readBody2(req);
|
|
11688
12171
|
const { filename, mimeType, dataBase64 } = body ?? {};
|
|
11689
12172
|
if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
|
|
11690
12173
|
json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
|
|
@@ -11886,7 +12369,7 @@ var StreamerServer = class {
|
|
|
11886
12369
|
return;
|
|
11887
12370
|
}
|
|
11888
12371
|
if (this.agentConfig.enabled) {
|
|
11889
|
-
const body2 = await
|
|
12372
|
+
const body2 = await readBody2(req);
|
|
11890
12373
|
const result = await handleStartAgentSession(body2, {
|
|
11891
12374
|
sessionStore: this.sessionStore,
|
|
11892
12375
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
@@ -11900,7 +12383,7 @@ var StreamerServer = class {
|
|
|
11900
12383
|
}
|
|
11901
12384
|
return;
|
|
11902
12385
|
}
|
|
11903
|
-
const body = await
|
|
12386
|
+
const body = await readBody2(req);
|
|
11904
12387
|
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
11905
12388
|
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
11906
12389
|
json(res, 400, { error: "Invalid provider" });
|
|
@@ -12072,8 +12555,8 @@ var StreamerServer = class {
|
|
|
12072
12555
|
cleanup();
|
|
12073
12556
|
return;
|
|
12074
12557
|
}
|
|
12075
|
-
let resolvedFilePath =
|
|
12076
|
-
if (!resolvedFilePath &&
|
|
12558
|
+
let resolvedFilePath = existsSync11(filePath) ? filePath : null;
|
|
12559
|
+
if (!resolvedFilePath && existsSync11(projectsDir)) {
|
|
12077
12560
|
try {
|
|
12078
12561
|
const now = Date.now();
|
|
12079
12562
|
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
@@ -12169,7 +12652,7 @@ var StreamerServer = class {
|
|
|
12169
12652
|
);
|
|
12170
12653
|
for (const root of this.codexRoots) {
|
|
12171
12654
|
const sessionsDir = join18(root, dateDir);
|
|
12172
|
-
if (!
|
|
12655
|
+
if (!existsSync11(sessionsDir)) continue;
|
|
12173
12656
|
let candidateFiles;
|
|
12174
12657
|
try {
|
|
12175
12658
|
candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -12258,7 +12741,7 @@ var StreamerServer = class {
|
|
|
12258
12741
|
});
|
|
12259
12742
|
return;
|
|
12260
12743
|
}
|
|
12261
|
-
const body = await
|
|
12744
|
+
const body = await readBody2(req);
|
|
12262
12745
|
const { path: relativePath, name } = body;
|
|
12263
12746
|
if (!name || typeof name !== "string") {
|
|
12264
12747
|
json(res, 400, { error: "Missing name field" });
|
|
@@ -12288,7 +12771,7 @@ var StreamerServer = class {
|
|
|
12288
12771
|
}
|
|
12289
12772
|
let parsed;
|
|
12290
12773
|
try {
|
|
12291
|
-
parsed = await
|
|
12774
|
+
parsed = await readBody2(req);
|
|
12292
12775
|
} catch {
|
|
12293
12776
|
json(res, 400, { error: "Invalid JSON" });
|
|
12294
12777
|
return;
|
|
@@ -12345,7 +12828,7 @@ var StreamerServer = class {
|
|
|
12345
12828
|
}
|
|
12346
12829
|
let parsed;
|
|
12347
12830
|
try {
|
|
12348
|
-
parsed = await
|
|
12831
|
+
parsed = await readBody2(req);
|
|
12349
12832
|
} catch {
|
|
12350
12833
|
json(res, 400, { error: "Invalid JSON" });
|
|
12351
12834
|
return;
|
|
@@ -12404,7 +12887,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
12404
12887
|
}
|
|
12405
12888
|
function classifyResumability(cwd) {
|
|
12406
12889
|
if (!cwd) return { resumable: true };
|
|
12407
|
-
if (
|
|
12890
|
+
if (existsSync11(cwd)) return { resumable: true };
|
|
12408
12891
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
12409
12892
|
return {
|
|
12410
12893
|
resumable: false,
|
|
@@ -12522,7 +13005,7 @@ function parseSessionListQuery(url) {
|
|
|
12522
13005
|
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
12523
13006
|
return { query: { limit, sortBy, order, status, cursor } };
|
|
12524
13007
|
}
|
|
12525
|
-
function
|
|
13008
|
+
function readBody2(req) {
|
|
12526
13009
|
return new Promise((resolve2, reject) => {
|
|
12527
13010
|
const chunks = [];
|
|
12528
13011
|
req.on("data", (chunk) => chunks.push(chunk));
|