@threadbase-sh/streamer 1.26.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 +152 -10
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +152 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +152 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -747,6 +747,7 @@ type ApiDeps = {
|
|
|
747
747
|
handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
|
|
748
748
|
handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
|
|
749
749
|
handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
|
|
750
|
+
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
750
751
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
751
752
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
752
753
|
handlePairStart: (res: ServerResponse) => void;
|
|
@@ -970,6 +971,7 @@ declare class StreamerServer {
|
|
|
970
971
|
private findConversationByUuid;
|
|
971
972
|
private isConversationSnapshotStale;
|
|
972
973
|
private handleGetConversation;
|
|
974
|
+
private handleSearchTarget;
|
|
973
975
|
private handleSearch;
|
|
974
976
|
private handleListSessions;
|
|
975
977
|
private handleGetSession;
|
package/dist/index.d.ts
CHANGED
|
@@ -747,6 +747,7 @@ type ApiDeps = {
|
|
|
747
747
|
handleConversationsCount: (url: URL, res: ServerResponse) => Promise<void>;
|
|
748
748
|
handleGetConversation: (id: string, url: URL, res: ServerResponse, ifNoneMatch?: string) => Promise<void>;
|
|
749
749
|
handleSearch: (url: URL, res: ServerResponse) => Promise<void>;
|
|
750
|
+
handleSearchTarget: (id: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
750
751
|
handleListProjects: (url: URL, res: ServerResponse) => void;
|
|
751
752
|
handleGetPopularProjects: (url: URL, res: ServerResponse) => void;
|
|
752
753
|
handlePairStart: (res: ServerResponse) => void;
|
|
@@ -970,6 +971,7 @@ declare class StreamerServer {
|
|
|
970
971
|
private findConversationByUuid;
|
|
971
972
|
private isConversationSnapshotStale;
|
|
972
973
|
private handleGetConversation;
|
|
974
|
+
private handleSearchTarget;
|
|
973
975
|
private handleSearch;
|
|
974
976
|
private handleListSessions;
|
|
975
977
|
private handleGetSession;
|
package/dist/index.js
CHANGED
|
@@ -2615,17 +2615,17 @@ var corsMiddleware = (configValue) => {
|
|
|
2615
2615
|
const raw = c.env.outgoing;
|
|
2616
2616
|
raw.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
2617
2617
|
raw.setHeader("Vary", "Origin");
|
|
2618
|
-
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2618
|
+
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2619
2619
|
raw.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
2620
|
-
raw.setHeader("Access-Control-Expose-Headers", "ETag");
|
|
2620
|
+
raw.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2621
2621
|
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
2622
2622
|
c.res.headers.set("Vary", "Origin");
|
|
2623
|
-
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2623
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2624
2624
|
c.res.headers.set(
|
|
2625
2625
|
"Access-Control-Allow-Headers",
|
|
2626
2626
|
"Authorization, Content-Type, If-None-Match"
|
|
2627
2627
|
);
|
|
2628
|
-
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
2628
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2629
2629
|
}
|
|
2630
2630
|
if (c.req.method === "OPTIONS") {
|
|
2631
2631
|
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
@@ -2669,6 +2669,11 @@ var createConversationRoutes = (deps) => {
|
|
|
2669
2669
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
2670
2670
|
return alreadyHandled2();
|
|
2671
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
|
+
});
|
|
2672
2677
|
app.get("/:id{.+}", async (c) => {
|
|
2673
2678
|
const id = c.req.param("id");
|
|
2674
2679
|
const url = new URL(c.req.url);
|
|
@@ -4486,6 +4491,55 @@ var ConversationWatcher = class {
|
|
|
4486
4491
|
}
|
|
4487
4492
|
};
|
|
4488
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
|
+
|
|
4489
4543
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4490
4544
|
import { existsSync as existsSync6 } from "fs";
|
|
4491
4545
|
function pruneAgentConversations(cache) {
|
|
@@ -5371,6 +5425,7 @@ var StreamerServer = class {
|
|
|
5371
5425
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
5372
5426
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
5373
5427
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
5428
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
5374
5429
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
5375
5430
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
5376
5431
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -6271,7 +6326,7 @@ var StreamerServer = class {
|
|
|
6271
6326
|
messageCount: etagSource.messageCount,
|
|
6272
6327
|
timestamp: etagSource.timestamp
|
|
6273
6328
|
});
|
|
6274
|
-
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");
|
|
6275
6330
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
6276
6331
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
6277
6332
|
res.end();
|
|
@@ -6279,29 +6334,53 @@ var StreamerServer = class {
|
|
|
6279
6334
|
}
|
|
6280
6335
|
const filtered = conversation.messages;
|
|
6281
6336
|
const total = filtered.length;
|
|
6282
|
-
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;
|
|
6283
6340
|
let slice = filtered;
|
|
6284
6341
|
let fromIdx = 0;
|
|
6285
6342
|
let messagePagination;
|
|
6286
6343
|
if (usePaging) {
|
|
6287
6344
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6288
6345
|
let beforeIndex = total;
|
|
6346
|
+
let scanLimit = limit;
|
|
6347
|
+
let anchorIndex = null;
|
|
6348
|
+
let newerPaging = false;
|
|
6289
6349
|
if (url.searchParams.has("before_index")) {
|
|
6290
6350
|
beforeIndex = intParam(url, "before_index", total);
|
|
6291
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;
|
|
6292
6365
|
}
|
|
6293
6366
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
6294
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
6295
|
-
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);
|
|
6296
6369
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6297
6370
|
fromIdx = start;
|
|
6371
|
+
const effectiveTotal = page?.total ?? total;
|
|
6298
6372
|
messagePagination = {
|
|
6299
|
-
total:
|
|
6373
|
+
total: effectiveTotal,
|
|
6300
6374
|
before_index: beforeIndex,
|
|
6301
6375
|
from_index: start,
|
|
6302
6376
|
has_more_older: start > 0,
|
|
6303
6377
|
next_before_index: start > 0 ? start : null
|
|
6304
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
|
+
}
|
|
6305
6384
|
}
|
|
6306
6385
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6307
6386
|
const content = [];
|
|
@@ -6376,6 +6455,65 @@ var StreamerServer = class {
|
|
|
6376
6455
|
});
|
|
6377
6456
|
res.end(JSON.stringify(body));
|
|
6378
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
|
+
}
|
|
6379
6517
|
async handleSearch(url, res) {
|
|
6380
6518
|
const q = url.searchParams.get("q") ?? "";
|
|
6381
6519
|
if (!q) {
|
|
@@ -6395,7 +6533,11 @@ var StreamerServer = class {
|
|
|
6395
6533
|
scanner
|
|
6396
6534
|
);
|
|
6397
6535
|
const adapted = results.map((r) => ({
|
|
6398
|
-
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,
|
|
6399
6541
|
title: r.meta.projectName,
|
|
6400
6542
|
sessionName: r.meta.sessionName || void 0,
|
|
6401
6543
|
filePath: r.meta.filePath,
|