@threadbase-sh/streamer 1.60.0 → 1.61.1
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 +631 -169
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +10 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +26 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -31270,7 +31270,7 @@ var require_websocket = __commonJS({
|
|
|
31270
31270
|
var http = require("http");
|
|
31271
31271
|
var net = require("net");
|
|
31272
31272
|
var tls = require("tls");
|
|
31273
|
-
var { randomBytes: randomBytes5, createHash:
|
|
31273
|
+
var { randomBytes: randomBytes5, createHash: createHash9 } = require("crypto");
|
|
31274
31274
|
var { Duplex, Readable: Readable2 } = require("stream");
|
|
31275
31275
|
var { URL: URL2 } = require("url");
|
|
31276
31276
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -31938,7 +31938,7 @@ var require_websocket = __commonJS({
|
|
|
31938
31938
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
31939
31939
|
return;
|
|
31940
31940
|
}
|
|
31941
|
-
const digest =
|
|
31941
|
+
const digest = createHash9("sha1").update(key + GUID).digest("base64");
|
|
31942
31942
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
31943
31943
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
31944
31944
|
return;
|
|
@@ -32307,7 +32307,7 @@ var require_websocket_server = __commonJS({
|
|
|
32307
32307
|
var EventEmitter5 = require("events");
|
|
32308
32308
|
var http = require("http");
|
|
32309
32309
|
var { Duplex } = require("stream");
|
|
32310
|
-
var { createHash:
|
|
32310
|
+
var { createHash: createHash9 } = require("crypto");
|
|
32311
32311
|
var extension2 = require_extension();
|
|
32312
32312
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
32313
32313
|
var subprotocol2 = require_subprotocol();
|
|
@@ -32614,7 +32614,7 @@ var require_websocket_server = __commonJS({
|
|
|
32614
32614
|
);
|
|
32615
32615
|
}
|
|
32616
32616
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
32617
|
-
const digest =
|
|
32617
|
+
const digest = createHash9("sha1").update(key + GUID).digest("base64");
|
|
32618
32618
|
const headers = [
|
|
32619
32619
|
"HTTP/1.1 101 Switching Protocols",
|
|
32620
32620
|
"Upgrade: websocket",
|
|
@@ -97360,6 +97360,81 @@ var init_platform = __esm({
|
|
|
97360
97360
|
}
|
|
97361
97361
|
});
|
|
97362
97362
|
|
|
97363
|
+
// src/server-identity.ts
|
|
97364
|
+
function serverIdentityKeyPath() {
|
|
97365
|
+
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path3.join)((0, import_os5.homedir)(), ".threadbase");
|
|
97366
|
+
return (0, import_path3.join)(dir, "keys", "server-identity.key");
|
|
97367
|
+
}
|
|
97368
|
+
function loadOrCreateServerIdentity() {
|
|
97369
|
+
const path2 = serverIdentityKeyPath();
|
|
97370
|
+
let raw2;
|
|
97371
|
+
try {
|
|
97372
|
+
raw2 = (0, import_fs4.readFileSync)(path2, "utf-8");
|
|
97373
|
+
} catch (err) {
|
|
97374
|
+
if (err.code !== "ENOENT") throw err;
|
|
97375
|
+
return generateIdentity(path2);
|
|
97376
|
+
}
|
|
97377
|
+
let privateKey;
|
|
97378
|
+
try {
|
|
97379
|
+
privateKey = (0, import_crypto4.createPrivateKey)({ key: JSON.parse(raw2).key, format: "jwk" });
|
|
97380
|
+
} catch {
|
|
97381
|
+
throw new Error(
|
|
97382
|
+
`Server identity key at ${path2} could not be read. Refusing to generate a new one \u2014 that would invalidate every paired device. Repair or delete the file deliberately.`
|
|
97383
|
+
);
|
|
97384
|
+
}
|
|
97385
|
+
return { publicKey: publicKeyOf(privateKey), privateKey };
|
|
97386
|
+
}
|
|
97387
|
+
function serverIdentityPublicKey() {
|
|
97388
|
+
return loadOrCreateServerIdentity().publicKey;
|
|
97389
|
+
}
|
|
97390
|
+
function serverIdentityFingerprint(publicKeyBase64url) {
|
|
97391
|
+
const raw2 = Buffer.from(publicKeyBase64url, "base64url");
|
|
97392
|
+
const hex3 = (0, import_crypto4.createHash)("sha256").update(raw2).digest().subarray(0, 16).toString("hex");
|
|
97393
|
+
return hex3.match(/.{4}/g)?.join(" ") ?? hex3;
|
|
97394
|
+
}
|
|
97395
|
+
function currentServerIdentityFingerprint() {
|
|
97396
|
+
return serverIdentityFingerprint(serverIdentityPublicKey());
|
|
97397
|
+
}
|
|
97398
|
+
function generateIdentity(path2) {
|
|
97399
|
+
const { privateKey } = (0, import_crypto4.generateKeyPairSync)("x25519");
|
|
97400
|
+
const file2 = {
|
|
97401
|
+
v: IDENTITY_FILE_VERSION,
|
|
97402
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97403
|
+
key: privateKey.export({ format: "jwk" })
|
|
97404
|
+
};
|
|
97405
|
+
(0, import_fs4.mkdirSync)((0, import_path3.dirname)(path2), { recursive: true, mode: 448 });
|
|
97406
|
+
const tmp = `${path2}.tmp`;
|
|
97407
|
+
(0, import_fs4.writeFileSync)(tmp, `${JSON.stringify(file2)}
|
|
97408
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
97409
|
+
(0, import_fs4.chmodSync)(tmp, 384);
|
|
97410
|
+
(0, import_fs4.renameSync)(tmp, path2);
|
|
97411
|
+
const publicKey = publicKeyOf(privateKey);
|
|
97412
|
+
getLogger("identity").info(`Generated server identity key ${publicKey}`, {
|
|
97413
|
+
event: "identity.key_generated",
|
|
97414
|
+
path: path2
|
|
97415
|
+
});
|
|
97416
|
+
return { publicKey, privateKey };
|
|
97417
|
+
}
|
|
97418
|
+
function publicKeyOf(privateKey) {
|
|
97419
|
+
const jwk = (0, import_crypto4.createPublicKey)(privateKey).export({ format: "jwk" });
|
|
97420
|
+
if (jwk.crv !== "X25519" || typeof jwk.x !== "string") {
|
|
97421
|
+
throw new Error(`Server identity key at ${serverIdentityKeyPath()} is not an X25519 key`);
|
|
97422
|
+
}
|
|
97423
|
+
return jwk.x;
|
|
97424
|
+
}
|
|
97425
|
+
var import_crypto4, import_fs4, import_os5, import_path3, IDENTITY_FILE_VERSION;
|
|
97426
|
+
var init_server_identity = __esm({
|
|
97427
|
+
"src/server-identity.ts"() {
|
|
97428
|
+
"use strict";
|
|
97429
|
+
import_crypto4 = require("crypto");
|
|
97430
|
+
import_fs4 = require("fs");
|
|
97431
|
+
import_os5 = require("os");
|
|
97432
|
+
import_path3 = require("path");
|
|
97433
|
+
init_logger();
|
|
97434
|
+
IDENTITY_FILE_VERSION = 1;
|
|
97435
|
+
}
|
|
97436
|
+
});
|
|
97437
|
+
|
|
97363
97438
|
// node_modules/fast-glob/out/utils/array.js
|
|
97364
97439
|
var require_array = __commonJS({
|
|
97365
97440
|
"node_modules/fast-glob/out/utils/array.js"(exports2) {
|
|
@@ -128273,6 +128348,43 @@ var init_repo = __esm({
|
|
|
128273
128348
|
}
|
|
128274
128349
|
});
|
|
128275
128350
|
|
|
128351
|
+
// cli/identity.ts
|
|
128352
|
+
var identity_exports = {};
|
|
128353
|
+
__export(identity_exports, {
|
|
128354
|
+
formatIdentityBanner: () => formatIdentityBanner,
|
|
128355
|
+
runIdentity: () => runIdentity
|
|
128356
|
+
});
|
|
128357
|
+
function runIdentity(deps) {
|
|
128358
|
+
const keyPath = (deps.keyPath ?? serverIdentityKeyPath)();
|
|
128359
|
+
const fingerprint2 = deps.fingerprint ?? currentServerIdentityFingerprint;
|
|
128360
|
+
let value;
|
|
128361
|
+
try {
|
|
128362
|
+
value = fingerprint2();
|
|
128363
|
+
} catch (err) {
|
|
128364
|
+
deps.log.error(
|
|
128365
|
+
`Could not read the server identity key: ${err instanceof Error ? err.message : String(err)}`
|
|
128366
|
+
);
|
|
128367
|
+
return 1;
|
|
128368
|
+
}
|
|
128369
|
+
deps.log.info(formatIdentityBanner(value, keyPath));
|
|
128370
|
+
return 0;
|
|
128371
|
+
}
|
|
128372
|
+
function formatIdentityBanner(fingerprint2, keyPath) {
|
|
128373
|
+
return [
|
|
128374
|
+
`Server identity (X25519, ${keyPath})`,
|
|
128375
|
+
"",
|
|
128376
|
+
` ${fingerprint2}`,
|
|
128377
|
+
"",
|
|
128378
|
+
"Compare this with the fingerprint your phone shows when you add this server."
|
|
128379
|
+
].join("\n");
|
|
128380
|
+
}
|
|
128381
|
+
var init_identity = __esm({
|
|
128382
|
+
"cli/identity.ts"() {
|
|
128383
|
+
"use strict";
|
|
128384
|
+
init_server_identity();
|
|
128385
|
+
}
|
|
128386
|
+
});
|
|
128387
|
+
|
|
128276
128388
|
// cli/setKey.ts
|
|
128277
128389
|
var setKey_exports = {};
|
|
128278
128390
|
__export(setKey_exports, {
|
|
@@ -135627,67 +135739,7 @@ var PushRepository = class {
|
|
|
135627
135739
|
|
|
135628
135740
|
// src/api/routes/misc.routes.ts
|
|
135629
135741
|
init_logger();
|
|
135630
|
-
|
|
135631
|
-
// src/server-identity.ts
|
|
135632
|
-
var import_crypto4 = require("crypto");
|
|
135633
|
-
var import_fs4 = require("fs");
|
|
135634
|
-
var import_os5 = require("os");
|
|
135635
|
-
var import_path3 = require("path");
|
|
135636
|
-
init_logger();
|
|
135637
|
-
var IDENTITY_FILE_VERSION = 1;
|
|
135638
|
-
function serverIdentityKeyPath() {
|
|
135639
|
-
const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path3.join)((0, import_os5.homedir)(), ".threadbase");
|
|
135640
|
-
return (0, import_path3.join)(dir, "keys", "server-identity.key");
|
|
135641
|
-
}
|
|
135642
|
-
function loadOrCreateServerIdentity() {
|
|
135643
|
-
const path2 = serverIdentityKeyPath();
|
|
135644
|
-
let raw2;
|
|
135645
|
-
try {
|
|
135646
|
-
raw2 = (0, import_fs4.readFileSync)(path2, "utf-8");
|
|
135647
|
-
} catch (err) {
|
|
135648
|
-
if (err.code !== "ENOENT") throw err;
|
|
135649
|
-
return generateIdentity(path2);
|
|
135650
|
-
}
|
|
135651
|
-
let privateKey;
|
|
135652
|
-
try {
|
|
135653
|
-
privateKey = (0, import_crypto4.createPrivateKey)({ key: JSON.parse(raw2).key, format: "jwk" });
|
|
135654
|
-
} catch {
|
|
135655
|
-
throw new Error(
|
|
135656
|
-
`Server identity key at ${path2} could not be read. Refusing to generate a new one \u2014 that would invalidate every paired device. Repair or delete the file deliberately.`
|
|
135657
|
-
);
|
|
135658
|
-
}
|
|
135659
|
-
return { publicKey: publicKeyOf(privateKey), privateKey };
|
|
135660
|
-
}
|
|
135661
|
-
function serverIdentityPublicKey() {
|
|
135662
|
-
return loadOrCreateServerIdentity().publicKey;
|
|
135663
|
-
}
|
|
135664
|
-
function generateIdentity(path2) {
|
|
135665
|
-
const { privateKey } = (0, import_crypto4.generateKeyPairSync)("x25519");
|
|
135666
|
-
const file2 = {
|
|
135667
|
-
v: IDENTITY_FILE_VERSION,
|
|
135668
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
135669
|
-
key: privateKey.export({ format: "jwk" })
|
|
135670
|
-
};
|
|
135671
|
-
(0, import_fs4.mkdirSync)((0, import_path3.dirname)(path2), { recursive: true, mode: 448 });
|
|
135672
|
-
const tmp = `${path2}.tmp`;
|
|
135673
|
-
(0, import_fs4.writeFileSync)(tmp, `${JSON.stringify(file2)}
|
|
135674
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
135675
|
-
(0, import_fs4.chmodSync)(tmp, 384);
|
|
135676
|
-
(0, import_fs4.renameSync)(tmp, path2);
|
|
135677
|
-
const publicKey = publicKeyOf(privateKey);
|
|
135678
|
-
getLogger("identity").info(`Generated server identity key ${publicKey}`, {
|
|
135679
|
-
event: "identity.key_generated",
|
|
135680
|
-
path: path2
|
|
135681
|
-
});
|
|
135682
|
-
return { publicKey, privateKey };
|
|
135683
|
-
}
|
|
135684
|
-
function publicKeyOf(privateKey) {
|
|
135685
|
-
const jwk = (0, import_crypto4.createPublicKey)(privateKey).export({ format: "jwk" });
|
|
135686
|
-
if (jwk.crv !== "X25519" || typeof jwk.x !== "string") {
|
|
135687
|
-
throw new Error(`Server identity key at ${serverIdentityKeyPath()} is not an X25519 key`);
|
|
135688
|
-
}
|
|
135689
|
-
return jwk.x;
|
|
135690
|
-
}
|
|
135742
|
+
init_server_identity();
|
|
135691
135743
|
|
|
135692
135744
|
// src/services/push/apnsClient.ts
|
|
135693
135745
|
var import_node_crypto = require("crypto");
|
|
@@ -140776,18 +140828,43 @@ function readGitBranch(projectPath) {
|
|
|
140776
140828
|
log15.trace({ projectPath }, "git: no .git found within depth");
|
|
140777
140829
|
return null;
|
|
140778
140830
|
}
|
|
140831
|
+
var FTS_HIT_OPEN = "";
|
|
140832
|
+
var FTS_HIT_CLOSE = "";
|
|
140833
|
+
var FTS_SNIPPET_TOKENS = 16;
|
|
140834
|
+
var FTS_ELLIPSIS = "\u2026";
|
|
140835
|
+
var CONTENT_FIELD = "content";
|
|
140836
|
+
function parseFtsSnippet(raw2) {
|
|
140837
|
+
if (!raw2?.includes(FTS_HIT_OPEN)) return null;
|
|
140838
|
+
const highlights = [];
|
|
140839
|
+
let snippet = "";
|
|
140840
|
+
let openAt = -1;
|
|
140841
|
+
for (const ch of raw2) {
|
|
140842
|
+
if (ch === FTS_HIT_OPEN) {
|
|
140843
|
+
openAt = snippet.length;
|
|
140844
|
+
continue;
|
|
140845
|
+
}
|
|
140846
|
+
if (ch === FTS_HIT_CLOSE) {
|
|
140847
|
+
if (openAt >= 0 && snippet.length > openAt) {
|
|
140848
|
+
highlights.push({ start: openAt, end: snippet.length });
|
|
140849
|
+
}
|
|
140850
|
+
openAt = -1;
|
|
140851
|
+
continue;
|
|
140852
|
+
}
|
|
140853
|
+
snippet += ch;
|
|
140854
|
+
}
|
|
140855
|
+
return highlights.length > 0 ? { snippet, highlights } : null;
|
|
140856
|
+
}
|
|
140779
140857
|
function generateMatches(meta3, query) {
|
|
140780
140858
|
const matches = [];
|
|
140781
140859
|
const lowerQuery = query.toLowerCase();
|
|
140782
140860
|
const fields = [
|
|
140783
|
-
["contentSnippet", meta3.contentSnippet],
|
|
140784
|
-
["projectName", meta3.projectName],
|
|
140785
|
-
["sessionId", meta3.sessionId],
|
|
140786
140861
|
["sessionName", meta3.sessionName],
|
|
140787
|
-
["
|
|
140788
|
-
["model", meta3.model || ""],
|
|
140862
|
+
["projectName", meta3.projectName],
|
|
140789
140863
|
["gitBranch", meta3.gitBranch || ""],
|
|
140790
|
-
["toolNames", meta3.toolNames.join(" ")]
|
|
140864
|
+
["toolNames", meta3.toolNames.join(" ")],
|
|
140865
|
+
["model", meta3.model || ""],
|
|
140866
|
+
["account", meta3.account],
|
|
140867
|
+
["sessionId", meta3.sessionId]
|
|
140791
140868
|
];
|
|
140792
140869
|
for (const [field, value] of fields) {
|
|
140793
140870
|
const idx = value.toLowerCase().indexOf(lowerQuery);
|
|
@@ -140802,11 +140879,29 @@ function generateMatches(meta3, query) {
|
|
|
140802
140879
|
}
|
|
140803
140880
|
return matches.length > 0 ? matches : [{ field: "preview", snippet: meta3.preview }];
|
|
140804
140881
|
}
|
|
140882
|
+
function buildContentMatch(searchContent, query) {
|
|
140883
|
+
if (!searchContent || !query.trim()) return null;
|
|
140884
|
+
const idx = searchContent.toLowerCase().indexOf(query.toLowerCase());
|
|
140885
|
+
if (idx === -1) return null;
|
|
140886
|
+
const start = Math.max(0, idx - 80);
|
|
140887
|
+
const end = Math.min(searchContent.length, idx + query.length + 120);
|
|
140888
|
+
const body = searchContent.slice(start, end).replace(/\s+/g, " ").trim();
|
|
140889
|
+
const hitAt = body.toLowerCase().indexOf(query.toLowerCase());
|
|
140890
|
+
const prefix = start > 0 ? FTS_ELLIPSIS : "";
|
|
140891
|
+
const suffix = end < searchContent.length ? FTS_ELLIPSIS : "";
|
|
140892
|
+
const snippet = `${prefix}${body}${suffix}`;
|
|
140893
|
+
const highlights = hitAt === -1 ? [] : [{ start: hitAt + prefix.length, end: hitAt + prefix.length + query.length }];
|
|
140894
|
+
return { field: CONTENT_FIELD, snippet, highlights };
|
|
140895
|
+
}
|
|
140805
140896
|
var FlexSearch = flexsearch_bundle_module_min_default.default ?? flexsearch_bundle_module_min_default;
|
|
140806
140897
|
var SearchIndexer = class {
|
|
140807
140898
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140808
140899
|
index;
|
|
140809
140900
|
documents = /* @__PURE__ */ new Map();
|
|
140901
|
+
// The indexed body per conversation, kept so a hit can produce a real excerpt
|
|
140902
|
+
// instead of falling back to an unrelated preview. Sized by the caller (the
|
|
140903
|
+
// scanner tail-caps it to the content tier) — see the note on addDocument.
|
|
140904
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
140810
140905
|
constructor() {
|
|
140811
140906
|
this.index = this.createIndex();
|
|
140812
140907
|
}
|
|
@@ -140832,25 +140927,25 @@ var SearchIndexer = class {
|
|
|
140832
140927
|
cache: 100
|
|
140833
140928
|
});
|
|
140834
140929
|
}
|
|
140835
|
-
|
|
140930
|
+
// `searchContent` is the combined search document (text + thinking + tools),
|
|
140931
|
+
// NOT meta.contentSnippet.
|
|
140932
|
+
//
|
|
140933
|
+
// It arrives pre-capped. This index is `tokenize: "forward"` at resolution 9,
|
|
140934
|
+
// which stores every prefix of every token, so it is a resident-memory
|
|
140935
|
+
// structure whose cost is very different from the on-disk FTS index. Feeding
|
|
140936
|
+
// it the full ~128 KB budget across a few hundred conversations would be a
|
|
140937
|
+
// multi-hundred-MB-to-GB index. The scanner therefore caps this to the active
|
|
140938
|
+
// tier's snippetMax and accepts that `persistent: false` has lower recall than
|
|
140939
|
+
// SQLite — an honest, documented divergence rather than a silent one.
|
|
140940
|
+
addDocument(meta3, searchContent = "") {
|
|
140836
140941
|
this.documents.set(meta3.id, meta3);
|
|
140837
|
-
this.
|
|
140838
|
-
|
|
140839
|
-
content: meta3.contentSnippet,
|
|
140840
|
-
projectName: meta3.projectName,
|
|
140841
|
-
projectPath: meta3.projectPath,
|
|
140842
|
-
sessionId: meta3.sessionId,
|
|
140843
|
-
sessionName: meta3.sessionName,
|
|
140844
|
-
account: meta3.account,
|
|
140845
|
-
model: meta3.model || "",
|
|
140846
|
-
gitBranch: meta3.gitBranch || "",
|
|
140847
|
-
toolNames: meta3.toolNames.join(" ")
|
|
140848
|
-
});
|
|
140942
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
140943
|
+
this.index.add(toIndexDoc(meta3, searchContent));
|
|
140849
140944
|
}
|
|
140850
|
-
buildIndex(metas) {
|
|
140945
|
+
buildIndex(metas, searchContents) {
|
|
140851
140946
|
this.clear();
|
|
140852
140947
|
for (const meta3 of metas) {
|
|
140853
|
-
this.addDocument(meta3);
|
|
140948
|
+
this.addDocument(meta3, searchContents?.get(meta3.id) ?? "");
|
|
140854
140949
|
}
|
|
140855
140950
|
getLogger2().debug({ docCount: metas.length }, "indexer: built");
|
|
140856
140951
|
}
|
|
@@ -140870,14 +140965,26 @@ var SearchIndexer = class {
|
|
|
140870
140965
|
seen.add(id);
|
|
140871
140966
|
const meta3 = this.documents.get(id);
|
|
140872
140967
|
if (!meta3) continue;
|
|
140873
|
-
|
|
140874
|
-
|
|
140968
|
+
searchResults.push({
|
|
140969
|
+
meta: meta3,
|
|
140970
|
+
score: 1,
|
|
140971
|
+
matches: this.matchesFor(meta3, query)
|
|
140972
|
+
});
|
|
140875
140973
|
if (searchResults.length >= limit) break;
|
|
140876
140974
|
}
|
|
140877
140975
|
if (searchResults.length >= limit) break;
|
|
140878
140976
|
}
|
|
140879
140977
|
return searchResults;
|
|
140880
140978
|
}
|
|
140979
|
+
// Body context first (that is what explains why the result appeared), then any
|
|
140980
|
+
// metadata matches. Only when neither hits does generateMatches' preview
|
|
140981
|
+
// fallback stand in.
|
|
140982
|
+
matchesFor(meta3, query) {
|
|
140983
|
+
const contentMatch = buildContentMatch(this.searchContents.get(meta3.id) ?? "", query);
|
|
140984
|
+
const metaMatches = generateMatches(meta3, query);
|
|
140985
|
+
if (!contentMatch) return metaMatches;
|
|
140986
|
+
return [contentMatch, ...metaMatches.filter((m2) => m2.field !== "preview")];
|
|
140987
|
+
}
|
|
140881
140988
|
getRecent(limit) {
|
|
140882
140989
|
return Array.from(this.documents.values()).sort((a, b2) => b2.timestamp.localeCompare(a.timestamp)).slice(0, limit).map((meta3) => ({
|
|
140883
140990
|
meta: meta3,
|
|
@@ -140891,31 +140998,37 @@ var SearchIndexer = class {
|
|
|
140891
140998
|
// Replace an already-indexed document in place. FlexSearch's `add` does not
|
|
140892
140999
|
// overwrite an existing id, so a single-file refresh must go through
|
|
140893
141000
|
// `update` to avoid stale matches lingering in the index.
|
|
140894
|
-
updateDocument(meta3) {
|
|
141001
|
+
updateDocument(meta3, searchContent = "") {
|
|
140895
141002
|
this.documents.set(meta3.id, meta3);
|
|
140896
|
-
this.
|
|
140897
|
-
|
|
140898
|
-
content: meta3.contentSnippet,
|
|
140899
|
-
projectName: meta3.projectName,
|
|
140900
|
-
projectPath: meta3.projectPath,
|
|
140901
|
-
sessionId: meta3.sessionId,
|
|
140902
|
-
sessionName: meta3.sessionName,
|
|
140903
|
-
account: meta3.account,
|
|
140904
|
-
model: meta3.model || "",
|
|
140905
|
-
gitBranch: meta3.gitBranch || "",
|
|
140906
|
-
toolNames: meta3.toolNames.join(" ")
|
|
140907
|
-
});
|
|
141003
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
141004
|
+
this.index.update(toIndexDoc(meta3, searchContent));
|
|
140908
141005
|
}
|
|
140909
141006
|
removeDocument(id) {
|
|
140910
141007
|
this.documents.delete(id);
|
|
141008
|
+
this.searchContents.delete(id);
|
|
140911
141009
|
this.index.remove(id);
|
|
140912
141010
|
}
|
|
140913
141011
|
clear() {
|
|
140914
141012
|
this.documents.clear();
|
|
141013
|
+
this.searchContents.clear();
|
|
140915
141014
|
this.index = this.createIndex();
|
|
140916
141015
|
getLogger2().trace("indexer: cleared");
|
|
140917
141016
|
}
|
|
140918
141017
|
};
|
|
141018
|
+
function toIndexDoc(meta3, searchContent) {
|
|
141019
|
+
return {
|
|
141020
|
+
id: meta3.id,
|
|
141021
|
+
content: searchContent,
|
|
141022
|
+
projectName: meta3.projectName,
|
|
141023
|
+
projectPath: meta3.projectPath,
|
|
141024
|
+
sessionId: meta3.sessionId,
|
|
141025
|
+
sessionName: meta3.sessionName,
|
|
141026
|
+
account: meta3.account,
|
|
141027
|
+
model: meta3.model || "",
|
|
141028
|
+
gitBranch: meta3.gitBranch || "",
|
|
141029
|
+
toolNames: meta3.toolNames.join(" ")
|
|
141030
|
+
};
|
|
141031
|
+
}
|
|
140919
141032
|
var CLAUDE_CODE_PROVIDER2 = "claude-code";
|
|
140920
141033
|
var CODEX_CLI_PROVIDER2 = "codex-cli";
|
|
140921
141034
|
function initialReducerState() {
|
|
@@ -141065,7 +141178,7 @@ var SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join("|")})[^>]*>[\\s\\S]*?<\\/\
|
|
|
141065
141178
|
function cleanSystemTags(text) {
|
|
141066
141179
|
return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
141067
141180
|
}
|
|
141068
|
-
async function parseMeta(filePath, account, tier) {
|
|
141181
|
+
async function parseMeta(filePath, account, tier, onEntry) {
|
|
141069
141182
|
const log15 = getLogger2();
|
|
141070
141183
|
log15.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
|
|
141071
141184
|
const state = initialReducerState();
|
|
@@ -141082,6 +141195,7 @@ async function parseMeta(filePath, account, tier) {
|
|
|
141082
141195
|
continue;
|
|
141083
141196
|
}
|
|
141084
141197
|
reduceLine(state, entry, tier);
|
|
141198
|
+
onEntry?.(entry);
|
|
141085
141199
|
}
|
|
141086
141200
|
} catch (err) {
|
|
141087
141201
|
log15.warn({ filePath, err }, "parseMeta: read failed");
|
|
@@ -141776,7 +141890,7 @@ var LRUCache = class {
|
|
|
141776
141890
|
}
|
|
141777
141891
|
};
|
|
141778
141892
|
var YIELD_EVERY_LINES = 500;
|
|
141779
|
-
async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
141893
|
+
async function tailReduce(filePath, startOffset, startLine, state, tier, onEntry) {
|
|
141780
141894
|
const stream = (0, import_fs11.createReadStream)(filePath, { start: startOffset, encoding: "utf8" });
|
|
141781
141895
|
let buffer = "";
|
|
141782
141896
|
let offset = startOffset;
|
|
@@ -141792,7 +141906,9 @@ async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
|
141792
141906
|
buffer = buffer.slice(nl + 1);
|
|
141793
141907
|
if (text.length > 0) {
|
|
141794
141908
|
try {
|
|
141795
|
-
|
|
141909
|
+
const entry = JSON.parse(text);
|
|
141910
|
+
reduceLine(state, entry, tier);
|
|
141911
|
+
onEntry?.(entry);
|
|
141796
141912
|
} catch {
|
|
141797
141913
|
state.badJsonLines++;
|
|
141798
141914
|
}
|
|
@@ -142013,7 +142129,7 @@ function classify(filePath, existing) {
|
|
|
142013
142129
|
}
|
|
142014
142130
|
return { change: "reindex", stat: stat42 };
|
|
142015
142131
|
}
|
|
142016
|
-
async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
142132
|
+
async function parseMetaWithProvider(provider, filePath, account, tier, onEntry) {
|
|
142017
142133
|
const log15 = getLogger2();
|
|
142018
142134
|
const acc = provider.createEmptyAccumulator();
|
|
142019
142135
|
const rl = (0, import_readline3.createInterface)({
|
|
@@ -142031,6 +142147,7 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142031
142147
|
}
|
|
142032
142148
|
try {
|
|
142033
142149
|
provider.reduceEntry(acc, entry, tier);
|
|
142150
|
+
onEntry?.(entry);
|
|
142034
142151
|
} catch (err) {
|
|
142035
142152
|
log15.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
|
|
142036
142153
|
}
|
|
@@ -142041,6 +142158,149 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142041
142158
|
}
|
|
142042
142159
|
return provider.finalize(acc, filePath, account, tier);
|
|
142043
142160
|
}
|
|
142161
|
+
var SEARCH_BUDGET = {
|
|
142162
|
+
textMax: 64 * 1024,
|
|
142163
|
+
thinkingMax: 32 * 1024,
|
|
142164
|
+
toolsMax: 32 * 1024,
|
|
142165
|
+
toolPayloadMax: 4 * 1024
|
|
142166
|
+
};
|
|
142167
|
+
var SEP = "\n\n";
|
|
142168
|
+
function emptySearchDocument() {
|
|
142169
|
+
return { text: "", thinking: "", tools: "" };
|
|
142170
|
+
}
|
|
142171
|
+
function appendSearchDelta(doc, delta) {
|
|
142172
|
+
return {
|
|
142173
|
+
text: tailAppend(doc.text, delta.text, SEARCH_BUDGET.textMax),
|
|
142174
|
+
thinking: tailAppend(doc.thinking, delta.thinking, SEARCH_BUDGET.thinkingMax),
|
|
142175
|
+
tools: tailAppend(doc.tools, delta.tools, SEARCH_BUDGET.toolsMax)
|
|
142176
|
+
};
|
|
142177
|
+
}
|
|
142178
|
+
function tailAppend(current, incoming, max) {
|
|
142179
|
+
if (!incoming) return current;
|
|
142180
|
+
const joined = current ? current + SEP + incoming : incoming;
|
|
142181
|
+
return joined.length <= max ? joined : joined.slice(-max);
|
|
142182
|
+
}
|
|
142183
|
+
function combineSearchContent(doc) {
|
|
142184
|
+
return [doc.text, doc.thinking, doc.tools].filter(Boolean).join(SEP);
|
|
142185
|
+
}
|
|
142186
|
+
function capToolPayload(value) {
|
|
142187
|
+
const raw2 = stringifyPayload(value);
|
|
142188
|
+
if (!raw2) return "";
|
|
142189
|
+
return raw2.length > SEARCH_BUDGET.toolPayloadMax ? raw2.slice(0, SEARCH_BUDGET.toolPayloadMax) : raw2;
|
|
142190
|
+
}
|
|
142191
|
+
function stringifyPayload(value) {
|
|
142192
|
+
if (value === null || value === void 0) return "";
|
|
142193
|
+
if (typeof value === "string") return value;
|
|
142194
|
+
try {
|
|
142195
|
+
return JSON.stringify(value) ?? "";
|
|
142196
|
+
} catch {
|
|
142197
|
+
return "";
|
|
142198
|
+
}
|
|
142199
|
+
}
|
|
142200
|
+
function extractSearchDelta(entry) {
|
|
142201
|
+
if (entry.type === "response_item" || entry.type === "session_meta") {
|
|
142202
|
+
return extractCodexDelta(entry);
|
|
142203
|
+
}
|
|
142204
|
+
return extractClaudeDelta(entry);
|
|
142205
|
+
}
|
|
142206
|
+
function extractClaudeDelta(entry) {
|
|
142207
|
+
const type = entry.type;
|
|
142208
|
+
if (type !== "user" && type !== "assistant") return emptySearchDocument();
|
|
142209
|
+
if (entry.isMeta) return emptySearchDocument();
|
|
142210
|
+
const msg = entry.message;
|
|
142211
|
+
const content = msg?.content;
|
|
142212
|
+
const tools = [
|
|
142213
|
+
extractClaudeToolContent(content),
|
|
142214
|
+
// Claude stores the rich/structured tool result at the JSONL entry's top
|
|
142215
|
+
// level, not inside message.content — indexing only message.content would
|
|
142216
|
+
// miss most real tool output (file reads, command stdout).
|
|
142217
|
+
capToolPayload(entry.toolUseResult)
|
|
142218
|
+
].filter(Boolean).join(SEP);
|
|
142219
|
+
return {
|
|
142220
|
+
text: extractClaudeText(content),
|
|
142221
|
+
thinking: type === "assistant" ? extractThinking(content).content : "",
|
|
142222
|
+
tools
|
|
142223
|
+
};
|
|
142224
|
+
}
|
|
142225
|
+
function extractClaudeText(content) {
|
|
142226
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142227
|
+
if (!Array.isArray(content)) return "";
|
|
142228
|
+
const parts = [];
|
|
142229
|
+
for (const item of content) {
|
|
142230
|
+
if (typeof item === "string") {
|
|
142231
|
+
const cleaned = cleanSystemTags(item);
|
|
142232
|
+
if (cleaned) parts.push(cleaned);
|
|
142233
|
+
} else if (item?.type === "text" && typeof item.text === "string") {
|
|
142234
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142235
|
+
if (cleaned) parts.push(cleaned);
|
|
142236
|
+
}
|
|
142237
|
+
}
|
|
142238
|
+
return parts.join(SEP);
|
|
142239
|
+
}
|
|
142240
|
+
function extractClaudeToolContent(content) {
|
|
142241
|
+
if (!Array.isArray(content)) return "";
|
|
142242
|
+
const parts = [];
|
|
142243
|
+
for (const item of content) {
|
|
142244
|
+
if (item?.type === "tool_use") {
|
|
142245
|
+
const capped = capToolPayload(item.input);
|
|
142246
|
+
if (capped) parts.push(capped);
|
|
142247
|
+
} else if (item?.type === "tool_result") {
|
|
142248
|
+
const capped = capToolPayload(item.content);
|
|
142249
|
+
if (capped) parts.push(capped);
|
|
142250
|
+
}
|
|
142251
|
+
}
|
|
142252
|
+
return parts.join(SEP);
|
|
142253
|
+
}
|
|
142254
|
+
function extractCodexDelta(entry) {
|
|
142255
|
+
const payload = entry.payload;
|
|
142256
|
+
if (!payload || typeof payload !== "object") return emptySearchDocument();
|
|
142257
|
+
const ptype = payload.type;
|
|
142258
|
+
if (ptype === "function_call" || ptype === "custom_tool_call") {
|
|
142259
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.arguments) };
|
|
142260
|
+
}
|
|
142261
|
+
if (ptype === "function_call_output" || ptype === "custom_tool_call_output") {
|
|
142262
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.output) };
|
|
142263
|
+
}
|
|
142264
|
+
if (ptype === "reasoning") {
|
|
142265
|
+
return { text: "", thinking: extractCodexReasoning(payload), tools: "" };
|
|
142266
|
+
}
|
|
142267
|
+
if (ptype === "message") {
|
|
142268
|
+
const role = payload.role;
|
|
142269
|
+
if (role !== "user" && role !== "assistant") return emptySearchDocument();
|
|
142270
|
+
return { text: extractCodexText2(payload.content), thinking: "", tools: "" };
|
|
142271
|
+
}
|
|
142272
|
+
return emptySearchDocument();
|
|
142273
|
+
}
|
|
142274
|
+
function extractCodexReasoning(payload) {
|
|
142275
|
+
const parts = [];
|
|
142276
|
+
for (const key of ["summary", "content"]) {
|
|
142277
|
+
const blocks = payload[key];
|
|
142278
|
+
if (!Array.isArray(blocks)) continue;
|
|
142279
|
+
for (const block of blocks) {
|
|
142280
|
+
if (typeof block === "string") parts.push(block);
|
|
142281
|
+
else if (typeof block?.text === "string") parts.push(block.text);
|
|
142282
|
+
}
|
|
142283
|
+
}
|
|
142284
|
+
return parts.filter(Boolean).join(SEP);
|
|
142285
|
+
}
|
|
142286
|
+
function extractCodexText2(content) {
|
|
142287
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142288
|
+
if (!Array.isArray(content)) return "";
|
|
142289
|
+
const parts = [];
|
|
142290
|
+
for (const item of content) {
|
|
142291
|
+
if (typeof item === "string") {
|
|
142292
|
+
const cleaned = cleanSystemTags(item);
|
|
142293
|
+
if (cleaned) parts.push(cleaned);
|
|
142294
|
+
continue;
|
|
142295
|
+
}
|
|
142296
|
+
const t = item?.type;
|
|
142297
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof item.text === "string") {
|
|
142298
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142299
|
+
if (cleaned) parts.push(cleaned);
|
|
142300
|
+
}
|
|
142301
|
+
}
|
|
142302
|
+
return parts.join(SEP);
|
|
142303
|
+
}
|
|
142044
142304
|
var DEFAULT_TIERS = {
|
|
142045
142305
|
standard: { name: "standard", previewMax: 200, snippetMax: 5e3 },
|
|
142046
142306
|
full: { name: "full", previewMax: 1200, snippetMax: 5e4 }
|
|
@@ -142054,7 +142314,7 @@ function resolveTier(tierName, customTiers) {
|
|
|
142054
142314
|
}
|
|
142055
142315
|
return tier;
|
|
142056
142316
|
}
|
|
142057
|
-
var SCHEMA_VERSION =
|
|
142317
|
+
var SCHEMA_VERSION = 5;
|
|
142058
142318
|
var SCHEMA_SQL = `
|
|
142059
142319
|
CREATE TABLE IF NOT EXISTS conversation_files (
|
|
142060
142320
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -142154,13 +142414,27 @@ CREATE INDEX IF NOT EXISTS idx_conversations_account_recent ON conversations(acc
|
|
|
142154
142414
|
CREATE INDEX IF NOT EXISTS idx_conversations_subagent_recent ON conversations(is_subagent, timestamp DESC);
|
|
142155
142415
|
CREATE INDEX IF NOT EXISTS idx_conversations_team_recent ON conversations(team_name, timestamp DESC);
|
|
142156
142416
|
|
|
142157
|
-
-- Full-text search index over conversation
|
|
142158
|
-
--
|
|
142159
|
-
--
|
|
142160
|
-
--
|
|
142417
|
+
-- Full-text search index over conversation body + metadata. Kept separate from
|
|
142418
|
+
-- the metadata tables so list-screen queries stay small and fast. One FTS row
|
|
142419
|
+
-- per conversation, replaced on each upsert.
|
|
142420
|
+
--
|
|
142421
|
+
-- The row's rowid IS conversation_files.id. That is load-bearing, not cosmetic:
|
|
142422
|
+
-- FTS5's query planner only handles MATCH, rowid and rank, so any other
|
|
142423
|
+
-- constraint (including a source_path equality on an UNINDEXED column) has no
|
|
142424
|
+
-- index and linear-scans the whole table. At ~128 KB of body per row that would
|
|
142425
|
+
-- mean scanning the entire corpus on every append. Look rows up by rowid.
|
|
142426
|
+
-- source_path stays stored-but-UNINDEXED so a search hit can resolve back to a
|
|
142427
|
+
-- conversations row.
|
|
142428
|
+
--
|
|
142429
|
+
-- Body is three columns, not one: text > thinking > tools priority has to
|
|
142430
|
+
-- survive an append (a new user message belongs in the text column, not after
|
|
142431
|
+
-- the tool output already written). They are deliberately NOT concatenated into
|
|
142432
|
+
-- a fourth column - that would double-weight body hits and inflate bm25 length.
|
|
142161
142433
|
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts USING fts5(
|
|
142162
142434
|
source_path UNINDEXED,
|
|
142163
|
-
|
|
142435
|
+
text,
|
|
142436
|
+
thinking,
|
|
142437
|
+
tools,
|
|
142164
142438
|
project_name,
|
|
142165
142439
|
session_id,
|
|
142166
142440
|
session_name,
|
|
@@ -142228,6 +142502,24 @@ function runMigrations(db) {
|
|
|
142228
142502
|
if (current >= 1 && current < 3 && tableExists(db, "conversations")) {
|
|
142229
142503
|
db.exec("UPDATE conversations SET provider = 'claude-code' WHERE provider = 'threadbase'");
|
|
142230
142504
|
}
|
|
142505
|
+
if (current >= 1 && current < 5) {
|
|
142506
|
+
db.exec("DROP TABLE IF EXISTS conversation_messages_fts");
|
|
142507
|
+
if (tableExists(db, "conversation_files")) {
|
|
142508
|
+
const assignments = [];
|
|
142509
|
+
if (hasColumn(db, "conversation_files", "last_indexed_offset")) {
|
|
142510
|
+
assignments.push("last_indexed_offset = 0");
|
|
142511
|
+
}
|
|
142512
|
+
if (hasColumn(db, "conversation_files", "last_indexed_line")) {
|
|
142513
|
+
assignments.push("last_indexed_line = 0");
|
|
142514
|
+
}
|
|
142515
|
+
if (hasColumn(db, "conversation_files", "reducer_state")) {
|
|
142516
|
+
assignments.push("reducer_state = NULL");
|
|
142517
|
+
}
|
|
142518
|
+
if (assignments.length > 0) {
|
|
142519
|
+
db.exec(`UPDATE conversation_files SET ${assignments.join(", ")}`);
|
|
142520
|
+
}
|
|
142521
|
+
}
|
|
142522
|
+
}
|
|
142231
142523
|
db.exec(SCHEMA_SQL);
|
|
142232
142524
|
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
142233
142525
|
}
|
|
@@ -142247,6 +142539,7 @@ function openDatabase(dbPath) {
|
|
|
142247
142539
|
db.pragma("synchronous = NORMAL");
|
|
142248
142540
|
db.pragma("temp_store = MEMORY");
|
|
142249
142541
|
db.pragma("foreign_keys = ON");
|
|
142542
|
+
db.pragma("busy_timeout = 5000");
|
|
142250
142543
|
runMigrations(db);
|
|
142251
142544
|
getLogger2().debug({ dbPath }, "db: opened");
|
|
142252
142545
|
return db;
|
|
@@ -142681,22 +142974,31 @@ var ConversationsRepo = class {
|
|
|
142681
142974
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversations WHERE status = 'active'").get().n;
|
|
142682
142975
|
}
|
|
142683
142976
|
};
|
|
142977
|
+
var BODY_COLUMNS = [
|
|
142978
|
+
{ name: "text", index: 1 },
|
|
142979
|
+
{ name: "thinking", index: 2 },
|
|
142980
|
+
{ name: "tools", index: 3 }
|
|
142981
|
+
];
|
|
142684
142982
|
var FtsRepo = class {
|
|
142685
142983
|
constructor(db) {
|
|
142686
142984
|
this.db = db;
|
|
142687
142985
|
}
|
|
142688
142986
|
db;
|
|
142689
|
-
upsert(meta3) {
|
|
142987
|
+
upsert(rowId, meta3, doc) {
|
|
142690
142988
|
const sourcePath = canonicalPath(meta3.id);
|
|
142691
142989
|
const tx = this.db.transaction(() => {
|
|
142692
|
-
this.db.prepare("DELETE FROM conversation_messages_fts WHERE
|
|
142990
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
142693
142991
|
this.db.prepare(
|
|
142694
142992
|
`INSERT INTO conversation_messages_fts
|
|
142695
|
-
(
|
|
142696
|
-
|
|
142993
|
+
(rowid, source_path, text, thinking, tools,
|
|
142994
|
+
project_name, session_id, session_name, account, model, branch, tool_names)
|
|
142995
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
142697
142996
|
).run(
|
|
142997
|
+
rowId,
|
|
142698
142998
|
sourcePath,
|
|
142699
|
-
|
|
142999
|
+
doc.text,
|
|
143000
|
+
doc.thinking,
|
|
143001
|
+
doc.tools,
|
|
142700
143002
|
meta3.projectName ?? "",
|
|
142701
143003
|
meta3.sessionId ?? "",
|
|
142702
143004
|
meta3.sessionName ?? "",
|
|
@@ -142708,26 +143010,94 @@ var FtsRepo = class {
|
|
|
142708
143010
|
});
|
|
142709
143011
|
tx();
|
|
142710
143012
|
}
|
|
142711
|
-
|
|
142712
|
-
|
|
143013
|
+
// Current durable buckets for a conversation, so an append can tail-extend
|
|
143014
|
+
// them without reparsing the file. Returns null when no row exists yet.
|
|
143015
|
+
readDocument(rowId) {
|
|
143016
|
+
const row = this.db.prepare("SELECT text, thinking, tools FROM conversation_messages_fts WHERE rowid = ?").get(rowId);
|
|
143017
|
+
if (!row) return null;
|
|
143018
|
+
return { text: row.text ?? "", thinking: row.thinking ?? "", tools: row.tools ?? "" };
|
|
142713
143019
|
}
|
|
142714
|
-
//
|
|
142715
|
-
//
|
|
142716
|
-
|
|
143020
|
+
// True when the stored row already equals what we would write, so an append
|
|
143021
|
+
// can skip re-tokenizing ~128 KB of body for nothing.
|
|
143022
|
+
//
|
|
143023
|
+
// The check covers the metadata columns too, not just the buckets: a
|
|
143024
|
+
// newly-seen tool name or a late-resolved session_name changes metadata while
|
|
143025
|
+
// the body is byte-identical, and nothing else ever rewrites this row — so
|
|
143026
|
+
// skipping on "body unchanged" alone would strand that stale value forever.
|
|
143027
|
+
isCurrent(rowId, meta3, doc) {
|
|
143028
|
+
const row = this.db.prepare(
|
|
143029
|
+
`SELECT text, thinking, tools,
|
|
143030
|
+
project_name, session_id, session_name, account, model, branch, tool_names
|
|
143031
|
+
FROM conversation_messages_fts WHERE rowid = ?`
|
|
143032
|
+
).get(rowId);
|
|
143033
|
+
if (!row) return false;
|
|
143034
|
+
return row.text === doc.text && row.thinking === doc.thinking && row.tools === doc.tools && row.project_name === (meta3.projectName ?? "") && row.session_id === (meta3.sessionId ?? "") && row.session_name === (meta3.sessionName ?? "") && row.account === (meta3.account ?? "") && row.model === (meta3.model ?? "") && row.branch === (meta3.gitBranch ?? "") && row.tool_names === meta3.toolNames.join(" ");
|
|
143035
|
+
}
|
|
143036
|
+
remove(rowId) {
|
|
143037
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
143038
|
+
}
|
|
143039
|
+
// Ranked hits matching the query, best first. Filters run in SQL BEFORE LIMIT:
|
|
143040
|
+
// with a wide corpus a popular term matches far more conversations than one
|
|
143041
|
+
// page, so post-filtering an already-truncated list would report "no results"
|
|
143042
|
+
// for queries that do have them.
|
|
143043
|
+
search(query, limit, filters = {}) {
|
|
142717
143044
|
const match2 = toMatchQuery(query);
|
|
142718
143045
|
if (!match2) return [];
|
|
143046
|
+
const snippetSelects = BODY_COLUMNS.map(
|
|
143047
|
+
(c) => `snippet(conversation_messages_fts, ${c.index}, ?, ?, ?, ?) AS snippet_${c.name}`
|
|
143048
|
+
).join(", ");
|
|
143049
|
+
const params = [];
|
|
143050
|
+
for (const _col of BODY_COLUMNS) {
|
|
143051
|
+
params.push(FTS_HIT_OPEN, FTS_HIT_CLOSE, FTS_ELLIPSIS, FTS_SNIPPET_TOKENS);
|
|
143052
|
+
}
|
|
143053
|
+
params.push(match2);
|
|
143054
|
+
const where = ["conversation_messages_fts MATCH ?", "c.status = 'active'"];
|
|
143055
|
+
if (filters.account) {
|
|
143056
|
+
where.push("c.account = ?");
|
|
143057
|
+
params.push(filters.account);
|
|
143058
|
+
}
|
|
143059
|
+
if (filters.provider) {
|
|
143060
|
+
where.push("c.provider = ?");
|
|
143061
|
+
params.push(filters.provider);
|
|
143062
|
+
}
|
|
143063
|
+
if (filters.project) {
|
|
143064
|
+
where.push("(lower(c.project_path) LIKE ? OR lower(c.project_name) LIKE ?)");
|
|
143065
|
+
const like = `%${filters.project.toLowerCase()}%`;
|
|
143066
|
+
params.push(like, like);
|
|
143067
|
+
}
|
|
143068
|
+
if (filters.since) {
|
|
143069
|
+
where.push("c.timestamp >= ?");
|
|
143070
|
+
params.push(filters.since.toISOString());
|
|
143071
|
+
}
|
|
143072
|
+
if (filters.include === "conversations") {
|
|
143073
|
+
where.push("c.is_subagent = 0 AND c.is_teammate = 0");
|
|
143074
|
+
} else if (filters.include === "subagents") {
|
|
143075
|
+
where.push("c.is_subagent = 1");
|
|
143076
|
+
} else if (filters.include === "teammates") {
|
|
143077
|
+
where.push("c.is_teammate = 1");
|
|
143078
|
+
}
|
|
143079
|
+
params.push(limit);
|
|
142719
143080
|
const rows = this.db.prepare(
|
|
142720
|
-
`SELECT source_path
|
|
142721
|
-
|
|
143081
|
+
`SELECT conversation_messages_fts.source_path AS source_path, ${snippetSelects}
|
|
143082
|
+
FROM conversation_messages_fts
|
|
143083
|
+
JOIN conversations c ON c.source_path = conversation_messages_fts.source_path
|
|
143084
|
+
WHERE ${where.join(" AND ")}
|
|
142722
143085
|
ORDER BY rank
|
|
142723
143086
|
LIMIT ?`
|
|
142724
|
-
).all(
|
|
142725
|
-
return rows.map((
|
|
143087
|
+
).all(...params);
|
|
143088
|
+
return rows.map((row) => ({ sourcePath: row.source_path, body: pickBodySnippet(row) }));
|
|
142726
143089
|
}
|
|
142727
143090
|
count() {
|
|
142728
143091
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversation_messages_fts").get().n;
|
|
142729
143092
|
}
|
|
142730
143093
|
};
|
|
143094
|
+
function pickBodySnippet(row) {
|
|
143095
|
+
for (const col of BODY_COLUMNS) {
|
|
143096
|
+
const parsed = parseFtsSnippet(row[`snippet_${col.name}`]);
|
|
143097
|
+
if (parsed) return parsed;
|
|
143098
|
+
}
|
|
143099
|
+
return null;
|
|
143100
|
+
}
|
|
142731
143101
|
function toMatchQuery(query) {
|
|
142732
143102
|
const terms = query.trim().split(/\s+/).map((t) => t.replace(/"/g, "").trim()).filter(Boolean);
|
|
142733
143103
|
if (terms.length === 0) return "";
|
|
@@ -142903,9 +143273,13 @@ var PersistentEngine = class {
|
|
|
142903
143273
|
const state = resume ? JSON.parse(existing.reducer_state) : initialReducerState();
|
|
142904
143274
|
const startOffset = resume ? existing.last_indexed_offset : 0;
|
|
142905
143275
|
const startLine = resume ? existing.last_indexed_line : 0;
|
|
143276
|
+
let searchDelta = emptySearchDocument();
|
|
143277
|
+
const collectSearch = (entry) => {
|
|
143278
|
+
searchDelta = appendSearchDelta(searchDelta, extractSearchDelta(entry));
|
|
143279
|
+
};
|
|
142906
143280
|
let result;
|
|
142907
143281
|
try {
|
|
142908
|
-
result = await tailReduce(filePath, startOffset, startLine, state, tier);
|
|
143282
|
+
result = await tailReduce(filePath, startOffset, startLine, state, tier, collectSearch);
|
|
142909
143283
|
} catch (err) {
|
|
142910
143284
|
log15.warn({ filePath, err }, "persistent: tail read failed");
|
|
142911
143285
|
return { meta: null, change };
|
|
@@ -142918,9 +143292,13 @@ var PersistentEngine = class {
|
|
|
142918
143292
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
142919
143293
|
const fp = stat42.size > 0 ? fingerprint(filePath, stat42.size) : null;
|
|
142920
143294
|
const fileId = this.files.ensure(filePath, account);
|
|
143295
|
+
const base = resume ? this.fts.readDocument(fileId) ?? emptySearchDocument() : emptySearchDocument();
|
|
143296
|
+
const searchDoc = appendSearchDelta(base, searchDelta);
|
|
142921
143297
|
const upsert = this.db.transaction(() => {
|
|
142922
143298
|
this.conversations.upsert(fileId, meta3, state.pageMessageCount);
|
|
142923
|
-
this.fts.
|
|
143299
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143300
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143301
|
+
}
|
|
142924
143302
|
if (!resume) this.checkpoints.remove(filePath);
|
|
142925
143303
|
this.files.updateCursor(fileId, {
|
|
142926
143304
|
sizeBytes: stat42.size,
|
|
@@ -142963,7 +143341,10 @@ var PersistentEngine = class {
|
|
|
142963
143341
|
// any change reparses from 0 again. No reducer_state is persisted.
|
|
142964
143342
|
async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
|
|
142965
143343
|
const log15 = getLogger2();
|
|
142966
|
-
|
|
143344
|
+
let searchDoc = emptySearchDocument();
|
|
143345
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143346
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
143347
|
+
});
|
|
142967
143348
|
if (!meta3) {
|
|
142968
143349
|
this.markDeleted(filePath);
|
|
142969
143350
|
return null;
|
|
@@ -142975,7 +143356,9 @@ var PersistentEngine = class {
|
|
|
142975
143356
|
const fileId = this.files.ensure(filePath, account);
|
|
142976
143357
|
const upsert = this.db.transaction(() => {
|
|
142977
143358
|
this.conversations.upsert(fileId, meta3, meta3.messageCount);
|
|
142978
|
-
this.fts.
|
|
143359
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143360
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143361
|
+
}
|
|
142979
143362
|
this.checkpoints.remove(filePath);
|
|
142980
143363
|
this.files.updateCursor(fileId, {
|
|
142981
143364
|
sizeBytes: stat42.size,
|
|
@@ -143001,7 +143384,7 @@ var PersistentEngine = class {
|
|
|
143001
143384
|
if (!existing) return;
|
|
143002
143385
|
const tx = this.db.transaction(() => {
|
|
143003
143386
|
this.conversations.deleteByFileId(existing.id);
|
|
143004
|
-
this.fts.remove(
|
|
143387
|
+
this.fts.remove(existing.id);
|
|
143005
143388
|
this.checkpoints.remove(filePath);
|
|
143006
143389
|
this.files.setStatus(existing.id, "deleted");
|
|
143007
143390
|
});
|
|
@@ -143017,20 +143400,24 @@ var PersistentEngine = class {
|
|
|
143017
143400
|
getAllBySessionId(sessionId) {
|
|
143018
143401
|
return this.conversations.getAllBySessionId(sessionId);
|
|
143019
143402
|
}
|
|
143020
|
-
// Ranked
|
|
143021
|
-
//
|
|
143022
|
-
//
|
|
143023
|
-
|
|
143024
|
-
|
|
143025
|
-
|
|
143026
|
-
|
|
143027
|
-
|
|
143028
|
-
const
|
|
143029
|
-
for (const
|
|
143030
|
-
const meta3 = this.conversations.getBySourcePath(
|
|
143031
|
-
if (meta3)
|
|
143403
|
+
// Ranked hits matching the FTS query, best first, each already resolved to its
|
|
143404
|
+
// active conversation row and carrying the body excerpt when the match was in
|
|
143405
|
+
// the conversation body.
|
|
143406
|
+
//
|
|
143407
|
+
// Filters are passed down into SQL rather than applied to the result: with a
|
|
143408
|
+
// wide corpus, filtering an already-LIMITed list drops conversations that
|
|
143409
|
+
// would have matched.
|
|
143410
|
+
searchHits(query, limit, filters = {}) {
|
|
143411
|
+
const hits = [];
|
|
143412
|
+
for (const hit of this.fts.search(query, limit, filters)) {
|
|
143413
|
+
const meta3 = this.conversations.getBySourcePath(hit.sourcePath);
|
|
143414
|
+
if (meta3) hits.push({ meta: meta3, body: hit.body });
|
|
143032
143415
|
}
|
|
143033
|
-
return
|
|
143416
|
+
return hits;
|
|
143417
|
+
}
|
|
143418
|
+
// Empty-query listing, mirroring the in-memory indexer's behavior.
|
|
143419
|
+
recentMetas(limit) {
|
|
143420
|
+
return this.conversations.recent(limit);
|
|
143034
143421
|
}
|
|
143035
143422
|
getProjects() {
|
|
143036
143423
|
return this.conversations.distinctProjects();
|
|
@@ -143184,6 +143571,10 @@ var DEFAULT_CONFIG_PATH2 = "~/.config/threadbase-scanner";
|
|
|
143184
143571
|
function defaultDbPath() {
|
|
143185
143572
|
return process.env.TB_SCANNER_DB ?? (0, import_path10.join)((0, import_os8.homedir)(), ".config", "threadbase-scanner", "index.db");
|
|
143186
143573
|
}
|
|
143574
|
+
function capForMemory(doc, max) {
|
|
143575
|
+
const combined = combineSearchContent(doc);
|
|
143576
|
+
return combined.length > max ? combined.slice(-max) : combined;
|
|
143577
|
+
}
|
|
143187
143578
|
var ConversationScanner = class {
|
|
143188
143579
|
metadataCache = /* @__PURE__ */ new Map();
|
|
143189
143580
|
// Parsed conversations plus (persistent claude-code entries only) the resume
|
|
@@ -143195,6 +143586,10 @@ var ConversationScanner = class {
|
|
|
143195
143586
|
sessionIdIndex = /* @__PURE__ */ new Map();
|
|
143196
143587
|
projects = /* @__PURE__ */ new Set();
|
|
143197
143588
|
indexer = new SearchIndexer();
|
|
143589
|
+
// Search body per conversation for the in-memory path, already tail-capped to
|
|
143590
|
+
// the content tier. Survives scan() so a statCache hit — which skips the parse
|
|
143591
|
+
// entirely — can still index a body rather than an empty string.
|
|
143592
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
143198
143593
|
// Tier the most recent scan() ran with, so refreshFile() re-parses a single
|
|
143199
143594
|
// file at the same content depth. Defaults to the standard tier.
|
|
143200
143595
|
lastTier = resolveTier("standard");
|
|
@@ -143355,34 +143750,42 @@ var ConversationScanner = class {
|
|
|
143355
143750
|
try {
|
|
143356
143751
|
const s3 = (0, import_fs9.statSync)(filePath);
|
|
143357
143752
|
if (s3.mtimeMs === cached4.stat.mtimeMs && s3.size === cached4.stat.size) {
|
|
143358
|
-
return
|
|
143753
|
+
return {
|
|
143754
|
+
meta: cached4.meta,
|
|
143755
|
+
searchContent: this.searchContents.get(cached4.meta.id)
|
|
143756
|
+
};
|
|
143359
143757
|
}
|
|
143360
143758
|
} catch {
|
|
143361
143759
|
}
|
|
143362
143760
|
}
|
|
143363
143761
|
}
|
|
143364
143762
|
try {
|
|
143365
|
-
|
|
143763
|
+
let doc = emptySearchDocument();
|
|
143764
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143765
|
+
doc = appendSearchDelta(doc, extractSearchDelta(entry));
|
|
143766
|
+
});
|
|
143366
143767
|
if (meta3 && meta3.gitBranch === null && meta3.projectPath) {
|
|
143367
143768
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
143368
143769
|
}
|
|
143369
|
-
return meta3;
|
|
143770
|
+
return { meta: meta3, searchContent: capForMemory(doc, tier.snippetMax) };
|
|
143370
143771
|
} catch (err) {
|
|
143371
143772
|
parseFailures++;
|
|
143372
143773
|
log15.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
|
|
143373
|
-
return null;
|
|
143774
|
+
return { meta: null, searchContent: void 0 };
|
|
143374
143775
|
}
|
|
143375
143776
|
})
|
|
143376
143777
|
);
|
|
143377
143778
|
const batchMetas = [];
|
|
143378
|
-
for (const meta3 of results) {
|
|
143779
|
+
for (const { meta: meta3, searchContent } of results) {
|
|
143379
143780
|
if (meta3 && meta3.messageCount > 0) {
|
|
143380
143781
|
this.metadataCache.set(meta3.id, meta3);
|
|
143381
143782
|
this.addToSessionIndex(meta3);
|
|
143382
143783
|
this.projects.add(meta3.projectPath);
|
|
143383
143784
|
allMetas.push(meta3);
|
|
143384
143785
|
batchMetas.push(meta3);
|
|
143385
|
-
this.
|
|
143786
|
+
const content = searchContent ?? this.searchContents.get(meta3.id) ?? "";
|
|
143787
|
+
this.searchContents.set(meta3.id, content);
|
|
143788
|
+
this.indexer.addDocument(meta3, content);
|
|
143386
143789
|
}
|
|
143387
143790
|
}
|
|
143388
143791
|
if (batchMetas.length > 0) {
|
|
@@ -143419,12 +143822,29 @@ var ConversationScanner = class {
|
|
|
143419
143822
|
const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
|
|
143420
143823
|
await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
|
|
143421
143824
|
}
|
|
143422
|
-
|
|
143423
|
-
|
|
143424
|
-
|
|
143425
|
-
|
|
143426
|
-
|
|
143427
|
-
|
|
143825
|
+
if (query.trim()) {
|
|
143826
|
+
const want = (options.limit ?? 50) + (options.offset ?? 0);
|
|
143827
|
+
results = engine.searchHits(query, want, {
|
|
143828
|
+
account: options.account,
|
|
143829
|
+
provider: options.provider,
|
|
143830
|
+
project: options.project,
|
|
143831
|
+
since: options.since ? parseSinceCutoff(options.since) : void 0,
|
|
143832
|
+
include: options.include
|
|
143833
|
+
}).map(({ meta: meta3, body }) => ({
|
|
143834
|
+
meta: meta3,
|
|
143835
|
+
score: 1,
|
|
143836
|
+
matches: body ? [
|
|
143837
|
+
{ field: CONTENT_FIELD, snippet: body.snippet, highlights: body.highlights },
|
|
143838
|
+
...generateMatches(meta3, query).filter((m2) => m2.field !== "preview")
|
|
143839
|
+
] : generateMatches(meta3, query)
|
|
143840
|
+
}));
|
|
143841
|
+
} else {
|
|
143842
|
+
results = engine.recentMetas((options.limit ?? 50) + (options.offset ?? 0)).map((meta3) => ({
|
|
143843
|
+
meta: meta3,
|
|
143844
|
+
score: 1,
|
|
143845
|
+
matches: [{ field: "timestamp", snippet: meta3.preview }]
|
|
143846
|
+
}));
|
|
143847
|
+
}
|
|
143428
143848
|
} else {
|
|
143429
143849
|
if (this.indexer.getDocumentCount() === 0) {
|
|
143430
143850
|
log15.debug("search: index empty, triggering scan");
|
|
@@ -143618,8 +144038,11 @@ var ConversationScanner = class {
|
|
|
143618
144038
|
const previous = this.metadataCache.get(filePath);
|
|
143619
144039
|
const resolvedAccount = account ?? previous?.account ?? "default";
|
|
143620
144040
|
let meta3 = null;
|
|
144041
|
+
let searchDoc = emptySearchDocument();
|
|
143621
144042
|
try {
|
|
143622
|
-
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier)
|
|
144043
|
+
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier, (entry) => {
|
|
144044
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
144045
|
+
});
|
|
143623
144046
|
} catch (err) {
|
|
143624
144047
|
log15.warn({ filePath, err }, "refreshFile: parseMeta threw");
|
|
143625
144048
|
meta3 = null;
|
|
@@ -143636,6 +144059,7 @@ var ConversationScanner = class {
|
|
|
143636
144059
|
this.metadataCache.delete(previous.id);
|
|
143637
144060
|
this.removeFromSessionIndex(previous);
|
|
143638
144061
|
this.indexer.removeDocument(previous.id);
|
|
144062
|
+
this.searchContents.delete(previous.id);
|
|
143639
144063
|
}
|
|
143640
144064
|
log15.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
|
|
143641
144065
|
return null;
|
|
@@ -143645,10 +144069,12 @@ var ConversationScanner = class {
|
|
|
143645
144069
|
this.metadataCache.set(meta3.id, meta3);
|
|
143646
144070
|
this.addToSessionIndex(meta3);
|
|
143647
144071
|
this.projects.add(meta3.projectPath);
|
|
144072
|
+
const searchContent = capForMemory(searchDoc, this.lastTier.snippetMax);
|
|
144073
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
143648
144074
|
if (previous) {
|
|
143649
|
-
this.indexer.updateDocument(meta3);
|
|
144075
|
+
this.indexer.updateDocument(meta3, searchContent);
|
|
143650
144076
|
} else {
|
|
143651
|
-
this.indexer.addDocument(meta3);
|
|
144077
|
+
this.indexer.addDocument(meta3, searchContent);
|
|
143652
144078
|
}
|
|
143653
144079
|
log15.debug(
|
|
143654
144080
|
{ filePath, messageCount: meta3.messageCount },
|
|
@@ -145498,7 +145924,7 @@ function paginate(results, offset, limit) {
|
|
|
145498
145924
|
}
|
|
145499
145925
|
|
|
145500
145926
|
// src/utils/codexConversationLine.ts
|
|
145501
|
-
function
|
|
145927
|
+
function extractCodexText3(content) {
|
|
145502
145928
|
if (typeof content === "string") return content.trim();
|
|
145503
145929
|
if (!Array.isArray(content)) return "";
|
|
145504
145930
|
return content.map((item) => {
|
|
@@ -145537,7 +145963,7 @@ function classifyCodexLine(line) {
|
|
|
145537
145963
|
if (role !== "user" && role !== "assistant") {
|
|
145538
145964
|
return { kind: "ignored", reason: `role ${String(role)} is not rendered` };
|
|
145539
145965
|
}
|
|
145540
|
-
const text =
|
|
145966
|
+
const text = extractCodexText3(payload.content);
|
|
145541
145967
|
if (!text) {
|
|
145542
145968
|
return { kind: "ignored", reason: "message has no extractable text" };
|
|
145543
145969
|
}
|
|
@@ -150263,6 +150689,9 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
150263
150689
|
};
|
|
150264
150690
|
}
|
|
150265
150691
|
|
|
150692
|
+
// src/server.ts
|
|
150693
|
+
init_server_identity();
|
|
150694
|
+
|
|
150266
150695
|
// src/server-wiring.ts
|
|
150267
150696
|
var import_fs31 = require("fs");
|
|
150268
150697
|
|
|
@@ -154474,13 +154903,16 @@ var StreamerServer = class {
|
|
|
154474
154903
|
json2(res, 400, { error: "Missing token or clientPublicKey" });
|
|
154475
154904
|
return;
|
|
154476
154905
|
}
|
|
154477
|
-
|
|
154478
|
-
|
|
154479
|
-
|
|
154480
|
-
|
|
154481
|
-
|
|
154482
|
-
|
|
154483
|
-
|
|
154906
|
+
const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;
|
|
154907
|
+
let e2eeRequest = null;
|
|
154908
|
+
if (e2eeEnabled) {
|
|
154909
|
+
try {
|
|
154910
|
+
e2eeRequest = parseE2eeRequest(body?.e2ee);
|
|
154911
|
+
} catch (err) {
|
|
154912
|
+
const e = err;
|
|
154913
|
+
json2(res, 400, { error: e.message, code: e.code });
|
|
154914
|
+
return;
|
|
154915
|
+
}
|
|
154484
154916
|
}
|
|
154485
154917
|
const precheck = this.pairTokens.wouldConsume(token);
|
|
154486
154918
|
if (!precheck.ok) {
|
|
@@ -155134,6 +155566,9 @@ function parseDirScanDebounceEnv(raw2) {
|
|
|
155134
155566
|
return Number.isNaN(parsed) || parsed < 0 ? void 0 : parsed;
|
|
155135
155567
|
}
|
|
155136
155568
|
|
|
155569
|
+
// cli/index.ts
|
|
155570
|
+
init_server_identity();
|
|
155571
|
+
|
|
155137
155572
|
// src/updater/check-update.ts
|
|
155138
155573
|
var import_semver = __toESM(require_semver2(), 1);
|
|
155139
155574
|
|
|
@@ -158775,6 +159210,7 @@ init_platform2();
|
|
|
158775
159210
|
init_logger();
|
|
158776
159211
|
init_protocol();
|
|
158777
159212
|
init_socket();
|
|
159213
|
+
init_server_identity();
|
|
158778
159214
|
var log11 = getLogger("prod");
|
|
158779
159215
|
function clearSupervisorLogs() {
|
|
158780
159216
|
let paths;
|
|
@@ -158827,6 +159263,7 @@ async function runProdStatus() {
|
|
|
158827
159263
|
async function runProdDoctor(opts, deps = {}) {
|
|
158828
159264
|
const findings = [];
|
|
158829
159265
|
const repairs = [];
|
|
159266
|
+
let identity;
|
|
158830
159267
|
let ptyHost;
|
|
158831
159268
|
const marker = readMarker();
|
|
158832
159269
|
if (marker && !marker.userHeld && !isPidAlive(marker.devPid)) {
|
|
@@ -158839,6 +159276,15 @@ async function runProdDoctor(opts, deps = {}) {
|
|
|
158839
159276
|
if (!getSupervisor().isAgentLoaded()) {
|
|
158840
159277
|
findings.push("launchd agent is not loaded \u2014 prod is fully down");
|
|
158841
159278
|
}
|
|
159279
|
+
if (deps.serverIdentityFingerprint) {
|
|
159280
|
+
try {
|
|
159281
|
+
identity = deps.serverIdentityFingerprint();
|
|
159282
|
+
} catch (err) {
|
|
159283
|
+
findings.push(
|
|
159284
|
+
`server identity key at ${serverIdentityKeyPath()} is unreadable: ` + (err instanceof Error ? err.message : String(err))
|
|
159285
|
+
);
|
|
159286
|
+
}
|
|
159287
|
+
}
|
|
158842
159288
|
const conflicts = detectConflictingAgents();
|
|
158843
159289
|
for (const c of conflicts) {
|
|
158844
159290
|
if (c.resolution === "uninstall-homebrew") {
|
|
@@ -158867,7 +159313,7 @@ async function runProdDoctor(opts, deps = {}) {
|
|
|
158867
159313
|
);
|
|
158868
159314
|
}
|
|
158869
159315
|
}
|
|
158870
|
-
return { findings, repairs, ...ptyHost ? { ptyHost } : {} };
|
|
159316
|
+
return { findings, repairs, ...identity ? { identity } : {}, ...ptyHost ? { ptyHost } : {} };
|
|
158871
159317
|
}
|
|
158872
159318
|
function toPowerShellLiteral(value) {
|
|
158873
159319
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -159016,7 +159462,13 @@ function registerProdCommands(program3) {
|
|
|
159016
159462
|
log11.info(what, void 0, "console");
|
|
159017
159463
|
});
|
|
159018
159464
|
prod.command("doctor").description("Detect stale markers, missing agents, and pty-host health").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
|
|
159019
|
-
const r = await runProdDoctor(
|
|
159465
|
+
const r = await runProdDoctor(
|
|
159466
|
+
{ fix: opts.fix === true },
|
|
159467
|
+
{ serverIdentityFingerprint: currentServerIdentityFingerprint }
|
|
159468
|
+
);
|
|
159469
|
+
if (r.identity) {
|
|
159470
|
+
log11.info(`identity: ${r.identity}`, void 0, "console");
|
|
159471
|
+
}
|
|
159020
159472
|
if (r.ptyHost) {
|
|
159021
159473
|
log11.info(
|
|
159022
159474
|
r.ptyHost.reachable ? `pty-host: reachable, protocol=${r.ptyHost.protocolVersion}, sessions=${r.ptyHost.sessionCount}` : `pty-host: unreachable (${r.ptyHost.error})`,
|
|
@@ -159408,6 +159860,16 @@ program2.command("pair").description("Print a pairing QR code (server must alrea
|
|
|
159408
159860
|
const publicUrl = loadPublicUrl() ?? null;
|
|
159409
159861
|
await printServerBanner({ port, apiKey, publicUrl, includeQr: true });
|
|
159410
159862
|
});
|
|
159863
|
+
program2.command("identity").description("Print this server's identity fingerprint for out-of-band verification").action(async () => {
|
|
159864
|
+
const { runIdentity: runIdentity2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
|
|
159865
|
+
const code = runIdentity2({
|
|
159866
|
+
log: {
|
|
159867
|
+
info: (msg) => log14.info(msg, void 0, "console"),
|
|
159868
|
+
error: (msg) => log14.error(msg, void 0, "console")
|
|
159869
|
+
}
|
|
159870
|
+
});
|
|
159871
|
+
process.exit(code);
|
|
159872
|
+
});
|
|
159411
159873
|
program2.command("set-key [key]").description("Set the streamer API key in ~/.threadbase/server.yaml").action(async (key) => {
|
|
159412
159874
|
const { runSetKey: runSetKey2 } = await Promise.resolve().then(() => (init_setKey(), setKey_exports));
|
|
159413
159875
|
const code = await runSetKey2(
|