@threadbase-sh/streamer 1.25.0 → 1.27.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 +207 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +207 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +207 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -307,6 +307,7 @@ interface ServerConfig {
|
|
|
307
307
|
logMenubarRequests?: boolean;
|
|
308
308
|
browseRoot?: string;
|
|
309
309
|
publicUrl?: string;
|
|
310
|
+
browserCors?: string;
|
|
310
311
|
disableDb?: boolean;
|
|
311
312
|
scanProfiles?: Array<{
|
|
312
313
|
id: string;
|
|
@@ -717,6 +718,7 @@ type ApiDeps = {
|
|
|
717
718
|
};
|
|
718
719
|
publicUrl: string | null;
|
|
719
720
|
browseRoot: string | null;
|
|
721
|
+
browserCors: string | undefined;
|
|
720
722
|
ptyManager: LiveSessionManager;
|
|
721
723
|
sessionStore: SessionStore;
|
|
722
724
|
wsHub: WSHub;
|
|
@@ -745,6 +747,7 @@ type ApiDeps = {
|
|
|
745
747
|
handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
|
|
746
748
|
handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
|
|
747
749
|
handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
|
|
750
|
+
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
748
751
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
749
752
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
750
753
|
handlePairStart: (res: ServerResponse) => void;
|
|
@@ -893,6 +896,7 @@ declare class StreamerServer {
|
|
|
893
896
|
private disableDb;
|
|
894
897
|
private browseRoot;
|
|
895
898
|
private publicUrl;
|
|
899
|
+
private browserCors;
|
|
896
900
|
private pairTokens;
|
|
897
901
|
private exchangeAttempts;
|
|
898
902
|
private sessionStartAttempts;
|
|
@@ -967,6 +971,7 @@ declare class StreamerServer {
|
|
|
967
971
|
private findConversationByUuid;
|
|
968
972
|
private isConversationSnapshotStale;
|
|
969
973
|
private handleGetConversation;
|
|
974
|
+
private handleSearchTarget;
|
|
970
975
|
private handleSearch;
|
|
971
976
|
private handleListSessions;
|
|
972
977
|
private handleGetSession;
|
package/dist/index.d.ts
CHANGED
|
@@ -307,6 +307,7 @@ interface ServerConfig {
|
|
|
307
307
|
logMenubarRequests?: boolean;
|
|
308
308
|
browseRoot?: string;
|
|
309
309
|
publicUrl?: string;
|
|
310
|
+
browserCors?: string;
|
|
310
311
|
disableDb?: boolean;
|
|
311
312
|
scanProfiles?: Array<{
|
|
312
313
|
id: string;
|
|
@@ -717,6 +718,7 @@ type ApiDeps = {
|
|
|
717
718
|
};
|
|
718
719
|
publicUrl: string | null;
|
|
719
720
|
browseRoot: string | null;
|
|
721
|
+
browserCors: string | undefined;
|
|
720
722
|
ptyManager: LiveSessionManager;
|
|
721
723
|
sessionStore: SessionStore;
|
|
722
724
|
wsHub: WSHub;
|
|
@@ -745,6 +747,7 @@ type ApiDeps = {
|
|
|
745
747
|
handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
|
|
746
748
|
handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
|
|
747
749
|
handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
|
|
750
|
+
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
748
751
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
749
752
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
750
753
|
handlePairStart: (res: ServerResponse) => void;
|
|
@@ -893,6 +896,7 @@ declare class StreamerServer {
|
|
|
893
896
|
private disableDb;
|
|
894
897
|
private browseRoot;
|
|
895
898
|
private publicUrl;
|
|
899
|
+
private browserCors;
|
|
896
900
|
private pairTokens;
|
|
897
901
|
private exchangeAttempts;
|
|
898
902
|
private sessionStartAttempts;
|
|
@@ -967,6 +971,7 @@ declare class StreamerServer {
|
|
|
967
971
|
private findConversationByUuid;
|
|
968
972
|
private isConversationSnapshotStale;
|
|
969
973
|
private handleGetConversation;
|
|
974
|
+
private handleSearchTarget;
|
|
970
975
|
private handleSearch;
|
|
971
976
|
private handleListSessions;
|
|
972
977
|
private handleGetSession;
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,15 @@ function loadPublicUrl() {
|
|
|
324
324
|
}
|
|
325
325
|
return void 0;
|
|
326
326
|
}
|
|
327
|
+
function loadBrowserCors() {
|
|
328
|
+
try {
|
|
329
|
+
const content = readFileSync(configFile(), "utf-8");
|
|
330
|
+
const match = content.match(/browser_cors:\s*(.+)/);
|
|
331
|
+
if (match?.[1]) return match[1].trim();
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
327
336
|
function loadCacheDir() {
|
|
328
337
|
try {
|
|
329
338
|
const content = readFileSync(configFile(), "utf-8");
|
|
@@ -2574,25 +2583,55 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
2574
2583
|
};
|
|
2575
2584
|
|
|
2576
2585
|
// src/api/middleware/cors.middleware.ts
|
|
2577
|
-
var
|
|
2586
|
+
var DEFAULT_DEV_ORIGINS = [
|
|
2578
2587
|
"http://localhost:8081",
|
|
2579
2588
|
"http://localhost:19006",
|
|
2580
2589
|
"http://localhost:3000"
|
|
2581
|
-
]
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2590
|
+
];
|
|
2591
|
+
function resolveAllowedOrigins(raw) {
|
|
2592
|
+
if (!raw) return null;
|
|
2593
|
+
const trimmed = raw.trim();
|
|
2594
|
+
const lower = trimmed.toLowerCase();
|
|
2595
|
+
if (lower === "0" || lower === "false" || lower === "no" || lower === "off" || trimmed === "") {
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2598
|
+
const origins = new Set(DEFAULT_DEV_ORIGINS);
|
|
2599
|
+
if (!["1", "true", "yes", "on"].includes(lower)) {
|
|
2600
|
+
for (const o of trimmed.split(",")) {
|
|
2601
|
+
const origin = o.trim();
|
|
2602
|
+
if (origin) origins.add(origin);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return origins;
|
|
2606
|
+
}
|
|
2607
|
+
var corsMiddleware = (configValue) => {
|
|
2608
|
+
const allowedOrigins = resolveAllowedOrigins(
|
|
2609
|
+
process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue
|
|
2610
|
+
);
|
|
2611
|
+
return async (c, next) => {
|
|
2612
|
+
const origin = c.req.header("origin");
|
|
2613
|
+
const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;
|
|
2614
|
+
if (allowedOrigin) {
|
|
2615
|
+
const raw = c.env.outgoing;
|
|
2616
|
+
raw.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
2617
|
+
raw.setHeader("Vary", "Origin");
|
|
2618
|
+
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2619
|
+
raw.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
2620
|
+
raw.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2621
|
+
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
2622
|
+
c.res.headers.set("Vary", "Origin");
|
|
2623
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2624
|
+
c.res.headers.set(
|
|
2625
|
+
"Access-Control-Allow-Headers",
|
|
2626
|
+
"Authorization, Content-Type, If-None-Match"
|
|
2627
|
+
);
|
|
2628
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2629
|
+
}
|
|
2630
|
+
if (c.req.method === "OPTIONS") {
|
|
2631
|
+
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
2632
|
+
}
|
|
2633
|
+
await next();
|
|
2634
|
+
};
|
|
2596
2635
|
};
|
|
2597
2636
|
|
|
2598
2637
|
// src/api/middleware/error.middleware.ts
|
|
@@ -2630,6 +2669,11 @@ var createConversationRoutes = (deps) => {
|
|
|
2630
2669
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
2631
2670
|
return alreadyHandled2();
|
|
2632
2671
|
});
|
|
2672
|
+
app.on("QUERY", "/:id{.+}/search-target", async (c) => {
|
|
2673
|
+
const id = c.req.param("id");
|
|
2674
|
+
await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);
|
|
2675
|
+
return alreadyHandled2();
|
|
2676
|
+
});
|
|
2633
2677
|
app.get("/:id{.+}", async (c) => {
|
|
2634
2678
|
const id = c.req.param("id");
|
|
2635
2679
|
const url = new URL(c.req.url);
|
|
@@ -3007,7 +3051,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3007
3051
|
event: "http.request"
|
|
3008
3052
|
});
|
|
3009
3053
|
});
|
|
3010
|
-
app.use("*", corsMiddleware());
|
|
3054
|
+
app.use("*", corsMiddleware(deps.browserCors));
|
|
3011
3055
|
app.use("*", authMiddleware(deps));
|
|
3012
3056
|
app.onError(errorMiddleware);
|
|
3013
3057
|
app.route("/healthz", createHealthRoutes());
|
|
@@ -4447,6 +4491,55 @@ var ConversationWatcher = class {
|
|
|
4447
4491
|
}
|
|
4448
4492
|
};
|
|
4449
4493
|
|
|
4494
|
+
// src/services/conversations/findSearchTarget.ts
|
|
4495
|
+
var SNIPPET_CONTEXT = 60;
|
|
4496
|
+
var MAX_MATCH_INDEXES = 1e3;
|
|
4497
|
+
function buildSnippet(source, matchStart, matchLength) {
|
|
4498
|
+
const start = Math.max(0, matchStart - SNIPPET_CONTEXT);
|
|
4499
|
+
const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);
|
|
4500
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
4501
|
+
const suffix = end < source.length ? "\u2026" : "";
|
|
4502
|
+
return `${prefix}${source.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
4503
|
+
}
|
|
4504
|
+
function extendedBody(m) {
|
|
4505
|
+
const parts = [];
|
|
4506
|
+
if (m.isThinking && m.thinkingContent) parts.push(m.thinkingContent);
|
|
4507
|
+
for (const b of m.metadata?.toolUseBlocks ?? []) {
|
|
4508
|
+
if (b.input !== void 0) parts.push(JSON.stringify(b.input));
|
|
4509
|
+
}
|
|
4510
|
+
for (const r of m.metadata?.toolResults ?? []) {
|
|
4511
|
+
if (r.content !== void 0) parts.push(JSON.stringify(r.content));
|
|
4512
|
+
}
|
|
4513
|
+
return parts.join("\n");
|
|
4514
|
+
}
|
|
4515
|
+
function collectMatches(messages, needle, body) {
|
|
4516
|
+
const indexes = [];
|
|
4517
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4518
|
+
if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);
|
|
4519
|
+
}
|
|
4520
|
+
return indexes;
|
|
4521
|
+
}
|
|
4522
|
+
function findSearchTarget(messages, query) {
|
|
4523
|
+
const needle = query.toLowerCase();
|
|
4524
|
+
let body = (m) => m.text ?? "";
|
|
4525
|
+
let matches = collectMatches(messages, needle, body);
|
|
4526
|
+
if (matches.length === 0) {
|
|
4527
|
+
body = extendedBody;
|
|
4528
|
+
matches = collectMatches(messages, needle, body);
|
|
4529
|
+
}
|
|
4530
|
+
if (matches.length === 0) return null;
|
|
4531
|
+
const anchorIndex = matches[matches.length - 1];
|
|
4532
|
+
const anchorBody = body(messages[anchorIndex]);
|
|
4533
|
+
const at = anchorBody.toLowerCase().indexOf(needle);
|
|
4534
|
+
return {
|
|
4535
|
+
messageIndex: anchorIndex,
|
|
4536
|
+
uuid: messages[anchorIndex].uuid ?? null,
|
|
4537
|
+
snippet: buildSnippet(anchorBody, at, query.length),
|
|
4538
|
+
matchIndexes: matches.slice(-MAX_MATCH_INDEXES),
|
|
4539
|
+
totalMatches: matches.length
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4450
4543
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4451
4544
|
import { existsSync as existsSync6 } from "fs";
|
|
4452
4545
|
function pruneAgentConversations(cache) {
|
|
@@ -5062,6 +5155,7 @@ var StreamerServer = class {
|
|
|
5062
5155
|
disableDb = false;
|
|
5063
5156
|
browseRoot = null;
|
|
5064
5157
|
publicUrl = null;
|
|
5158
|
+
browserCors;
|
|
5065
5159
|
pairTokens = new PairTokenStore();
|
|
5066
5160
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
5067
5161
|
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
@@ -5147,6 +5241,7 @@ var StreamerServer = class {
|
|
|
5147
5241
|
this.log.warn(`Warning: ${result.error}`, { error: result.error });
|
|
5148
5242
|
}
|
|
5149
5243
|
}
|
|
5244
|
+
this.browserCors = config.browserCors ?? loadBrowserCors();
|
|
5150
5245
|
this.sessionStore = new SessionStore();
|
|
5151
5246
|
this.wsHub = new WSHub();
|
|
5152
5247
|
this.fileWatcher = new ConversationWatcher({
|
|
@@ -5301,6 +5396,7 @@ var StreamerServer = class {
|
|
|
5301
5396
|
rotateApiKey: () => this.rotateApiKey(),
|
|
5302
5397
|
publicUrl: this.publicUrl,
|
|
5303
5398
|
browseRoot: this.browseRoot,
|
|
5399
|
+
browserCors: this.browserCors,
|
|
5304
5400
|
ptyManager: this.ptyManager,
|
|
5305
5401
|
sessionStore: this.sessionStore,
|
|
5306
5402
|
wsHub: this.wsHub,
|
|
@@ -5329,6 +5425,7 @@ var StreamerServer = class {
|
|
|
5329
5425
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
5330
5426
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
5331
5427
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
5428
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
5332
5429
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
5333
5430
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
5334
5431
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -6229,7 +6326,7 @@ var StreamerServer = class {
|
|
|
6229
6326
|
messageCount: etagSource.messageCount,
|
|
6230
6327
|
timestamp: etagSource.timestamp
|
|
6231
6328
|
});
|
|
6232
|
-
const isFirstPage = !url.searchParams.has("before_index");
|
|
6329
|
+
const isFirstPage = !url.searchParams.has("before_index") && !url.searchParams.has("anchor_index") && !url.searchParams.has("after_index");
|
|
6233
6330
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
6234
6331
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
6235
6332
|
res.end();
|
|
@@ -6237,29 +6334,53 @@ var StreamerServer = class {
|
|
|
6237
6334
|
}
|
|
6238
6335
|
const filtered = conversation.messages;
|
|
6239
6336
|
const total = filtered.length;
|
|
6240
|
-
const
|
|
6337
|
+
const hasAnchor = url.searchParams.has("anchor_index");
|
|
6338
|
+
const hasAfter = url.searchParams.has("after_index");
|
|
6339
|
+
const usePaging = url.searchParams.has("msg_limit") || url.searchParams.has("before_index") || hasAnchor || hasAfter;
|
|
6241
6340
|
let slice = filtered;
|
|
6242
6341
|
let fromIdx = 0;
|
|
6243
6342
|
let messagePagination;
|
|
6244
6343
|
if (usePaging) {
|
|
6245
6344
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6246
6345
|
let beforeIndex = total;
|
|
6346
|
+
let scanLimit = limit;
|
|
6347
|
+
let anchorIndex = null;
|
|
6348
|
+
let newerPaging = false;
|
|
6247
6349
|
if (url.searchParams.has("before_index")) {
|
|
6248
6350
|
beforeIndex = intParam(url, "before_index", total);
|
|
6249
6351
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
6352
|
+
} else if (hasAfter) {
|
|
6353
|
+
const from = Math.min(Math.max(intParam(url, "after_index", 0), 0), total);
|
|
6354
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6355
|
+
scanLimit = beforeIndex - from;
|
|
6356
|
+
newerPaging = true;
|
|
6357
|
+
} else if (hasAnchor) {
|
|
6358
|
+
anchorIndex = Math.min(
|
|
6359
|
+
Math.max(intParam(url, "anchor_index", 0), 0),
|
|
6360
|
+
Math.max(0, total - 1)
|
|
6361
|
+
);
|
|
6362
|
+
const from = Math.max(0, anchorIndex - Math.floor(limit / 2));
|
|
6363
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6364
|
+
newerPaging = true;
|
|
6250
6365
|
}
|
|
6251
6366
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
6252
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
6253
|
-
const start = page?.fromIndex ?? Math.max(0, beforeIndex -
|
|
6367
|
+
const page = scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null;
|
|
6368
|
+
const start = page?.fromIndex ?? Math.max(0, beforeIndex - scanLimit);
|
|
6254
6369
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6255
6370
|
fromIdx = start;
|
|
6371
|
+
const effectiveTotal = page?.total ?? total;
|
|
6256
6372
|
messagePagination = {
|
|
6257
|
-
total:
|
|
6373
|
+
total: effectiveTotal,
|
|
6258
6374
|
before_index: beforeIndex,
|
|
6259
6375
|
from_index: start,
|
|
6260
6376
|
has_more_older: start > 0,
|
|
6261
6377
|
next_before_index: start > 0 ? start : null
|
|
6262
6378
|
};
|
|
6379
|
+
if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;
|
|
6380
|
+
if (newerPaging) {
|
|
6381
|
+
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
6382
|
+
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
6383
|
+
}
|
|
6263
6384
|
}
|
|
6264
6385
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6265
6386
|
const content = [];
|
|
@@ -6334,6 +6455,65 @@ var StreamerServer = class {
|
|
|
6334
6455
|
});
|
|
6335
6456
|
res.end(JSON.stringify(body));
|
|
6336
6457
|
}
|
|
6458
|
+
// Resolves an active search query to the message a client should anchor to
|
|
6459
|
+
// inside one conversation. Matching is body-only (text first, then
|
|
6460
|
+
// thinking/tool payloads) — a metadata-only search hit (project path, title)
|
|
6461
|
+
// has no scroll target and returns 404 search_target_not_found.
|
|
6462
|
+
//
|
|
6463
|
+
// Implements HTTP QUERY (RFC 10008): the search query travels in a JSON
|
|
6464
|
+
// request body instead of a URL query param — QUERY is safe + idempotent +
|
|
6465
|
+
// cacheable like GET, but (like POST) can carry a body, which fits this
|
|
6466
|
+
// endpoint's single-string input exactly. `Accept-Query` advertises the
|
|
6467
|
+
// supported request media type per the spec.
|
|
6468
|
+
async handleSearchTarget(id, req, res) {
|
|
6469
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0].trim();
|
|
6470
|
+
if (contentType && contentType !== "application/json") {
|
|
6471
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6472
|
+
json(res, 415, {
|
|
6473
|
+
error: "Unsupported Content-Type; expected application/json",
|
|
6474
|
+
code: "unsupported_media_type"
|
|
6475
|
+
});
|
|
6476
|
+
return;
|
|
6477
|
+
}
|
|
6478
|
+
let body;
|
|
6479
|
+
try {
|
|
6480
|
+
body = await readBody(req);
|
|
6481
|
+
} catch {
|
|
6482
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6483
|
+
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
6484
|
+
return;
|
|
6485
|
+
}
|
|
6486
|
+
const q = typeof body?.q === "string" ? body.q.trim() : "";
|
|
6487
|
+
if (!q) {
|
|
6488
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6489
|
+
json(res, 422, { error: "Missing or empty query field: q", code: "invalid_query" });
|
|
6490
|
+
return;
|
|
6491
|
+
}
|
|
6492
|
+
if (q.length > 256) {
|
|
6493
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6494
|
+
json(res, 422, { error: "Query too long (max 256 characters)", code: "invalid_query" });
|
|
6495
|
+
return;
|
|
6496
|
+
}
|
|
6497
|
+
const conversation = await this.findConversationByUuid(id);
|
|
6498
|
+
if (!conversation) {
|
|
6499
|
+
json(res, 404, { error: "Conversation not found", code: "not_found" });
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
const target = findSearchTarget(conversation.messages, q);
|
|
6503
|
+
if (!target) {
|
|
6504
|
+
json(res, 404, { error: "No message body matches query", code: "search_target_not_found" });
|
|
6505
|
+
return;
|
|
6506
|
+
}
|
|
6507
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6508
|
+
json(res, 200, {
|
|
6509
|
+
query: q,
|
|
6510
|
+
message_index: target.messageIndex,
|
|
6511
|
+
uuid: target.uuid,
|
|
6512
|
+
snippet: target.snippet,
|
|
6513
|
+
match_indexes: target.matchIndexes,
|
|
6514
|
+
total_matches: target.totalMatches
|
|
6515
|
+
});
|
|
6516
|
+
}
|
|
6337
6517
|
async handleSearch(url, res) {
|
|
6338
6518
|
const q = url.searchParams.get("q") ?? "";
|
|
6339
6519
|
if (!q) {
|
|
@@ -6353,7 +6533,11 @@ var StreamerServer = class {
|
|
|
6353
6533
|
scanner
|
|
6354
6534
|
);
|
|
6355
6535
|
const adapted = results.map((r) => ({
|
|
6356
|
-
id
|
|
6536
|
+
// Use sessionId so the id matches /api/conversations and resolves via
|
|
6537
|
+
// findConversationByUuid — a client can round-trip a search result into
|
|
6538
|
+
// GET /api/conversations/:id or the search-target QUERY. The old
|
|
6539
|
+
// filename-stem derivation produced an id no other endpoint recognized.
|
|
6540
|
+
id: r.meta.sessionId || r.meta.id,
|
|
6357
6541
|
title: r.meta.projectName,
|
|
6358
6542
|
sessionName: r.meta.sessionName || void 0,
|
|
6359
6543
|
filePath: r.meta.filePath,
|