@threadbase-sh/streamer 1.39.1 → 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 +747 -341
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +575 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +518 -112
- 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;
|
|
@@ -9521,14 +9906,14 @@ var StreamerServer = class {
|
|
|
9521
9906
|
}
|
|
9522
9907
|
this.apnsClient = new ApnsClient(creds);
|
|
9523
9908
|
const sender = new LiveActivitySender(this.apnsClient, pushRepo);
|
|
9524
|
-
const serverId = process.env.THREADBASE_INSTANCE_ID ??
|
|
9525
|
-
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId,
|
|
9909
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname3();
|
|
9910
|
+
this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname3());
|
|
9526
9911
|
this.liveActivityRenewal = new LiveActivityRenewalScheduler({
|
|
9527
9912
|
repo: pushRepo,
|
|
9528
9913
|
sender,
|
|
9529
9914
|
sessionStore: this.sessionStore,
|
|
9530
9915
|
serverId,
|
|
9531
|
-
serverLabel:
|
|
9916
|
+
serverLabel: hostname3()
|
|
9532
9917
|
});
|
|
9533
9918
|
this.liveActivityRenewal.start();
|
|
9534
9919
|
this.log.info("Live Activity push enabled", {
|
|
@@ -9867,7 +10252,7 @@ var StreamerServer = class {
|
|
|
9867
10252
|
this.fileWatcher.watchDirectory(dir);
|
|
9868
10253
|
}
|
|
9869
10254
|
for (const dir of this.codexRoots) {
|
|
9870
|
-
if (!
|
|
10255
|
+
if (!existsSync11(dir)) continue;
|
|
9871
10256
|
this.fileWatcher.watchDirectory(dir);
|
|
9872
10257
|
}
|
|
9873
10258
|
} catch (err) {
|
|
@@ -10140,7 +10525,7 @@ var StreamerServer = class {
|
|
|
10140
10525
|
}
|
|
10141
10526
|
let body;
|
|
10142
10527
|
try {
|
|
10143
|
-
body = await
|
|
10528
|
+
body = await readBody2(req);
|
|
10144
10529
|
} catch (err) {
|
|
10145
10530
|
const message = err instanceof Error ? err.message : "Invalid body";
|
|
10146
10531
|
json(res, 400, { error: message });
|
|
@@ -10186,7 +10571,7 @@ var StreamerServer = class {
|
|
|
10186
10571
|
nonce: sealed.nonce,
|
|
10187
10572
|
ephemeralPublicKey: sealed.ephemeralPublicKey,
|
|
10188
10573
|
publicUrl: this.publicUrl,
|
|
10189
|
-
machineName:
|
|
10574
|
+
machineName: hostname3(),
|
|
10190
10575
|
...device && {
|
|
10191
10576
|
deviceId: device.deviceId,
|
|
10192
10577
|
deviceToken: device.deviceToken,
|
|
@@ -10721,15 +11106,15 @@ var StreamerServer = class {
|
|
|
10721
11106
|
findJsonlPath(uuid) {
|
|
10722
11107
|
const filename = `${uuid}.jsonl`;
|
|
10723
11108
|
for (const projectsDir of this.projectsDirs()) {
|
|
10724
|
-
if (!
|
|
11109
|
+
if (!existsSync11(projectsDir)) continue;
|
|
10725
11110
|
for (const dir of readdirSync6(projectsDir)) {
|
|
10726
11111
|
const fp = join18(projectsDir, dir, filename);
|
|
10727
|
-
if (
|
|
11112
|
+
if (existsSync11(fp)) return fp;
|
|
10728
11113
|
const projectDir = join18(projectsDir, dir);
|
|
10729
11114
|
try {
|
|
10730
11115
|
for (const sub of readdirSync6(projectDir)) {
|
|
10731
11116
|
const subagentPath = join18(projectDir, sub, "subagents", filename);
|
|
10732
|
-
if (
|
|
11117
|
+
if (existsSync11(subagentPath)) return subagentPath;
|
|
10733
11118
|
}
|
|
10734
11119
|
} catch {
|
|
10735
11120
|
}
|
|
@@ -11266,7 +11651,7 @@ var StreamerServer = class {
|
|
|
11266
11651
|
}
|
|
11267
11652
|
let body;
|
|
11268
11653
|
try {
|
|
11269
|
-
body = await
|
|
11654
|
+
body = await readBody2(req);
|
|
11270
11655
|
} catch {
|
|
11271
11656
|
res.setHeader("Accept-Query", "application/json");
|
|
11272
11657
|
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
@@ -11304,17 +11689,27 @@ var StreamerServer = class {
|
|
|
11304
11689
|
});
|
|
11305
11690
|
}
|
|
11306
11691
|
async handleSearch(url, res) {
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
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;
|
|
11311
11701
|
}
|
|
11312
|
-
const
|
|
11702
|
+
const { q, limit, offset, filters } = parsed;
|
|
11703
|
+
const startedAt = Date.now();
|
|
11313
11704
|
const scanner = await this.getScanner();
|
|
11314
11705
|
const results = await search(
|
|
11315
11706
|
q,
|
|
11316
11707
|
{
|
|
11317
|
-
|
|
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),
|
|
11318
11713
|
include: "conversations",
|
|
11319
11714
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
11320
11715
|
...this.codexScanOpts()
|
|
@@ -11338,13 +11733,24 @@ var StreamerServer = class {
|
|
|
11338
11733
|
lastActivity: r.meta.timestamp,
|
|
11339
11734
|
firstMessage: r.meta.firstMessage ?? void 0,
|
|
11340
11735
|
lastMessage: r.meta.lastMessage ?? void 0,
|
|
11341
|
-
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
|
+
})) : []
|
|
11342
11745
|
}));
|
|
11746
|
+
const page = paginate(applyFilters(adapted, filters), offset, limit);
|
|
11343
11747
|
json(res, 200, {
|
|
11344
|
-
conversations:
|
|
11345
|
-
hasMore:
|
|
11346
|
-
offset:
|
|
11347
|
-
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
|
|
11348
11754
|
});
|
|
11349
11755
|
}
|
|
11350
11756
|
async handleListSessions(url, res) {
|
|
@@ -11390,7 +11796,7 @@ var StreamerServer = class {
|
|
|
11390
11796
|
if (this.rejectIfWarmingUp(res)) return;
|
|
11391
11797
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
11392
11798
|
if (session) {
|
|
11393
|
-
if (!
|
|
11799
|
+
if (!existsSync11(session.projectPath)) {
|
|
11394
11800
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
11395
11801
|
}
|
|
11396
11802
|
const reconciled = this.withReconciledLifecycle([session])[0];
|
|
@@ -11417,7 +11823,7 @@ var StreamerServer = class {
|
|
|
11417
11823
|
json(res, 404, { error: "Session not found" });
|
|
11418
11824
|
}
|
|
11419
11825
|
async handleResume(req, res) {
|
|
11420
|
-
const body = await
|
|
11826
|
+
const body = await readBody2(req);
|
|
11421
11827
|
const sessionId = body.sessionId ?? body.conversationId;
|
|
11422
11828
|
if (!sessionId) {
|
|
11423
11829
|
json(res, 400, { error: "Missing sessionId" });
|
|
@@ -11549,7 +11955,7 @@ var StreamerServer = class {
|
|
|
11549
11955
|
return;
|
|
11550
11956
|
}
|
|
11551
11957
|
if (this.agentConfig.enabled) {
|
|
11552
|
-
const body2 = await
|
|
11958
|
+
const body2 = await readBody2(req);
|
|
11553
11959
|
const cache = this.cache;
|
|
11554
11960
|
if (!cache) {
|
|
11555
11961
|
json(res, 503, {
|
|
@@ -11568,7 +11974,7 @@ var StreamerServer = class {
|
|
|
11568
11974
|
json(res, result.status, result.body);
|
|
11569
11975
|
return;
|
|
11570
11976
|
}
|
|
11571
|
-
const body = await
|
|
11977
|
+
const body = await readBody2(req);
|
|
11572
11978
|
const { input, keys } = body;
|
|
11573
11979
|
let idempotencyKey;
|
|
11574
11980
|
try {
|
|
@@ -11732,7 +12138,7 @@ var StreamerServer = class {
|
|
|
11732
12138
|
});
|
|
11733
12139
|
}
|
|
11734
12140
|
async handleSendAnswer(sessionId, req, res) {
|
|
11735
|
-
const body = await
|
|
12141
|
+
const body = await readBody2(req);
|
|
11736
12142
|
const pending = this.pendingQuestions.get(sessionId);
|
|
11737
12143
|
const resolution = resolveAnswer(pending, body);
|
|
11738
12144
|
if (!resolution.ok) {
|
|
@@ -11761,7 +12167,7 @@ var StreamerServer = class {
|
|
|
11761
12167
|
json(res, 400, { error: "Session has no project path" });
|
|
11762
12168
|
return;
|
|
11763
12169
|
}
|
|
11764
|
-
const body = await
|
|
12170
|
+
const body = await readBody2(req);
|
|
11765
12171
|
const { filename, mimeType, dataBase64 } = body ?? {};
|
|
11766
12172
|
if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
|
|
11767
12173
|
json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
|
|
@@ -11963,7 +12369,7 @@ var StreamerServer = class {
|
|
|
11963
12369
|
return;
|
|
11964
12370
|
}
|
|
11965
12371
|
if (this.agentConfig.enabled) {
|
|
11966
|
-
const body2 = await
|
|
12372
|
+
const body2 = await readBody2(req);
|
|
11967
12373
|
const result = await handleStartAgentSession(body2, {
|
|
11968
12374
|
sessionStore: this.sessionStore,
|
|
11969
12375
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
@@ -11977,7 +12383,7 @@ var StreamerServer = class {
|
|
|
11977
12383
|
}
|
|
11978
12384
|
return;
|
|
11979
12385
|
}
|
|
11980
|
-
const body = await
|
|
12386
|
+
const body = await readBody2(req);
|
|
11981
12387
|
const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
|
|
11982
12388
|
if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
|
|
11983
12389
|
json(res, 400, { error: "Invalid provider" });
|
|
@@ -12149,8 +12555,8 @@ var StreamerServer = class {
|
|
|
12149
12555
|
cleanup();
|
|
12150
12556
|
return;
|
|
12151
12557
|
}
|
|
12152
|
-
let resolvedFilePath =
|
|
12153
|
-
if (!resolvedFilePath &&
|
|
12558
|
+
let resolvedFilePath = existsSync11(filePath) ? filePath : null;
|
|
12559
|
+
if (!resolvedFilePath && existsSync11(projectsDir)) {
|
|
12154
12560
|
try {
|
|
12155
12561
|
const now = Date.now();
|
|
12156
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(
|
|
@@ -12246,7 +12652,7 @@ var StreamerServer = class {
|
|
|
12246
12652
|
);
|
|
12247
12653
|
for (const root of this.codexRoots) {
|
|
12248
12654
|
const sessionsDir = join18(root, dateDir);
|
|
12249
|
-
if (!
|
|
12655
|
+
if (!existsSync11(sessionsDir)) continue;
|
|
12250
12656
|
let candidateFiles;
|
|
12251
12657
|
try {
|
|
12252
12658
|
candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
@@ -12335,7 +12741,7 @@ var StreamerServer = class {
|
|
|
12335
12741
|
});
|
|
12336
12742
|
return;
|
|
12337
12743
|
}
|
|
12338
|
-
const body = await
|
|
12744
|
+
const body = await readBody2(req);
|
|
12339
12745
|
const { path: relativePath, name } = body;
|
|
12340
12746
|
if (!name || typeof name !== "string") {
|
|
12341
12747
|
json(res, 400, { error: "Missing name field" });
|
|
@@ -12365,7 +12771,7 @@ var StreamerServer = class {
|
|
|
12365
12771
|
}
|
|
12366
12772
|
let parsed;
|
|
12367
12773
|
try {
|
|
12368
|
-
parsed = await
|
|
12774
|
+
parsed = await readBody2(req);
|
|
12369
12775
|
} catch {
|
|
12370
12776
|
json(res, 400, { error: "Invalid JSON" });
|
|
12371
12777
|
return;
|
|
@@ -12422,7 +12828,7 @@ var StreamerServer = class {
|
|
|
12422
12828
|
}
|
|
12423
12829
|
let parsed;
|
|
12424
12830
|
try {
|
|
12425
|
-
parsed = await
|
|
12831
|
+
parsed = await readBody2(req);
|
|
12426
12832
|
} catch {
|
|
12427
12833
|
json(res, 400, { error: "Invalid JSON" });
|
|
12428
12834
|
return;
|
|
@@ -12481,7 +12887,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
12481
12887
|
}
|
|
12482
12888
|
function classifyResumability(cwd) {
|
|
12483
12889
|
if (!cwd) return { resumable: true };
|
|
12484
|
-
if (
|
|
12890
|
+
if (existsSync11(cwd)) return { resumable: true };
|
|
12485
12891
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
12486
12892
|
return {
|
|
12487
12893
|
resumable: false,
|
|
@@ -12599,7 +13005,7 @@ function parseSessionListQuery(url) {
|
|
|
12599
13005
|
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
12600
13006
|
return { query: { limit, sortBy, order, status, cursor } };
|
|
12601
13007
|
}
|
|
12602
|
-
function
|
|
13008
|
+
function readBody2(req) {
|
|
12603
13009
|
return new Promise((resolve2, reject) => {
|
|
12604
13010
|
const chunks = [];
|
|
12605
13011
|
req.on("data", (chunk) => chunks.push(chunk));
|