@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.cjs
CHANGED
|
@@ -2653,17 +2653,17 @@ var corsMiddleware = (configValue) => {
|
|
|
2653
2653
|
const raw = c.env.outgoing;
|
|
2654
2654
|
raw.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
2655
2655
|
raw.setHeader("Vary", "Origin");
|
|
2656
|
-
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2656
|
+
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2657
2657
|
raw.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
2658
|
-
raw.setHeader("Access-Control-Expose-Headers", "ETag");
|
|
2658
|
+
raw.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2659
2659
|
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
2660
2660
|
c.res.headers.set("Vary", "Origin");
|
|
2661
|
-
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2661
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2662
2662
|
c.res.headers.set(
|
|
2663
2663
|
"Access-Control-Allow-Headers",
|
|
2664
2664
|
"Authorization, Content-Type, If-None-Match"
|
|
2665
2665
|
);
|
|
2666
|
-
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
2666
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2667
2667
|
}
|
|
2668
2668
|
if (c.req.method === "OPTIONS") {
|
|
2669
2669
|
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
@@ -2707,6 +2707,11 @@ var createConversationRoutes = (deps) => {
|
|
|
2707
2707
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
2708
2708
|
return alreadyHandled2();
|
|
2709
2709
|
});
|
|
2710
|
+
app.on("QUERY", "/:id{.+}/search-target", async (c) => {
|
|
2711
|
+
const id = c.req.param("id");
|
|
2712
|
+
await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);
|
|
2713
|
+
return alreadyHandled2();
|
|
2714
|
+
});
|
|
2710
2715
|
app.get("/:id{.+}", async (c) => {
|
|
2711
2716
|
const id = c.req.param("id");
|
|
2712
2717
|
const url = new URL(c.req.url);
|
|
@@ -4525,6 +4530,55 @@ var ConversationWatcher = class {
|
|
|
4525
4530
|
}
|
|
4526
4531
|
};
|
|
4527
4532
|
|
|
4533
|
+
// src/services/conversations/findSearchTarget.ts
|
|
4534
|
+
var SNIPPET_CONTEXT = 60;
|
|
4535
|
+
var MAX_MATCH_INDEXES = 1e3;
|
|
4536
|
+
function buildSnippet(source, matchStart, matchLength) {
|
|
4537
|
+
const start = Math.max(0, matchStart - SNIPPET_CONTEXT);
|
|
4538
|
+
const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);
|
|
4539
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
4540
|
+
const suffix = end < source.length ? "\u2026" : "";
|
|
4541
|
+
return `${prefix}${source.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
4542
|
+
}
|
|
4543
|
+
function extendedBody(m) {
|
|
4544
|
+
const parts = [];
|
|
4545
|
+
if (m.isThinking && m.thinkingContent) parts.push(m.thinkingContent);
|
|
4546
|
+
for (const b of m.metadata?.toolUseBlocks ?? []) {
|
|
4547
|
+
if (b.input !== void 0) parts.push(JSON.stringify(b.input));
|
|
4548
|
+
}
|
|
4549
|
+
for (const r of m.metadata?.toolResults ?? []) {
|
|
4550
|
+
if (r.content !== void 0) parts.push(JSON.stringify(r.content));
|
|
4551
|
+
}
|
|
4552
|
+
return parts.join("\n");
|
|
4553
|
+
}
|
|
4554
|
+
function collectMatches(messages, needle, body) {
|
|
4555
|
+
const indexes = [];
|
|
4556
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4557
|
+
if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);
|
|
4558
|
+
}
|
|
4559
|
+
return indexes;
|
|
4560
|
+
}
|
|
4561
|
+
function findSearchTarget(messages, query) {
|
|
4562
|
+
const needle = query.toLowerCase();
|
|
4563
|
+
let body = (m) => m.text ?? "";
|
|
4564
|
+
let matches = collectMatches(messages, needle, body);
|
|
4565
|
+
if (matches.length === 0) {
|
|
4566
|
+
body = extendedBody;
|
|
4567
|
+
matches = collectMatches(messages, needle, body);
|
|
4568
|
+
}
|
|
4569
|
+
if (matches.length === 0) return null;
|
|
4570
|
+
const anchorIndex = matches[matches.length - 1];
|
|
4571
|
+
const anchorBody = body(messages[anchorIndex]);
|
|
4572
|
+
const at = anchorBody.toLowerCase().indexOf(needle);
|
|
4573
|
+
return {
|
|
4574
|
+
messageIndex: anchorIndex,
|
|
4575
|
+
uuid: messages[anchorIndex].uuid ?? null,
|
|
4576
|
+
snippet: buildSnippet(anchorBody, at, query.length),
|
|
4577
|
+
matchIndexes: matches.slice(-MAX_MATCH_INDEXES),
|
|
4578
|
+
totalMatches: matches.length
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4528
4582
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4529
4583
|
var import_fs11 = require("fs");
|
|
4530
4584
|
function pruneAgentConversations(cache) {
|
|
@@ -5410,6 +5464,7 @@ var StreamerServer = class {
|
|
|
5410
5464
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
5411
5465
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
5412
5466
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
5467
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
5413
5468
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
5414
5469
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
5415
5470
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -6310,7 +6365,7 @@ var StreamerServer = class {
|
|
|
6310
6365
|
messageCount: etagSource.messageCount,
|
|
6311
6366
|
timestamp: etagSource.timestamp
|
|
6312
6367
|
});
|
|
6313
|
-
const isFirstPage = !url.searchParams.has("before_index");
|
|
6368
|
+
const isFirstPage = !url.searchParams.has("before_index") && !url.searchParams.has("anchor_index") && !url.searchParams.has("after_index");
|
|
6314
6369
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
6315
6370
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
6316
6371
|
res.end();
|
|
@@ -6318,29 +6373,53 @@ var StreamerServer = class {
|
|
|
6318
6373
|
}
|
|
6319
6374
|
const filtered = conversation.messages;
|
|
6320
6375
|
const total = filtered.length;
|
|
6321
|
-
const
|
|
6376
|
+
const hasAnchor = url.searchParams.has("anchor_index");
|
|
6377
|
+
const hasAfter = url.searchParams.has("after_index");
|
|
6378
|
+
const usePaging = url.searchParams.has("msg_limit") || url.searchParams.has("before_index") || hasAnchor || hasAfter;
|
|
6322
6379
|
let slice = filtered;
|
|
6323
6380
|
let fromIdx = 0;
|
|
6324
6381
|
let messagePagination;
|
|
6325
6382
|
if (usePaging) {
|
|
6326
6383
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6327
6384
|
let beforeIndex = total;
|
|
6385
|
+
let scanLimit = limit;
|
|
6386
|
+
let anchorIndex = null;
|
|
6387
|
+
let newerPaging = false;
|
|
6328
6388
|
if (url.searchParams.has("before_index")) {
|
|
6329
6389
|
beforeIndex = intParam(url, "before_index", total);
|
|
6330
6390
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
6391
|
+
} else if (hasAfter) {
|
|
6392
|
+
const from = Math.min(Math.max(intParam(url, "after_index", 0), 0), total);
|
|
6393
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6394
|
+
scanLimit = beforeIndex - from;
|
|
6395
|
+
newerPaging = true;
|
|
6396
|
+
} else if (hasAnchor) {
|
|
6397
|
+
anchorIndex = Math.min(
|
|
6398
|
+
Math.max(intParam(url, "anchor_index", 0), 0),
|
|
6399
|
+
Math.max(0, total - 1)
|
|
6400
|
+
);
|
|
6401
|
+
const from = Math.max(0, anchorIndex - Math.floor(limit / 2));
|
|
6402
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6403
|
+
newerPaging = true;
|
|
6331
6404
|
}
|
|
6332
6405
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
6333
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
6334
|
-
const start = page?.fromIndex ?? Math.max(0, beforeIndex -
|
|
6406
|
+
const page = scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null;
|
|
6407
|
+
const start = page?.fromIndex ?? Math.max(0, beforeIndex - scanLimit);
|
|
6335
6408
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6336
6409
|
fromIdx = start;
|
|
6410
|
+
const effectiveTotal = page?.total ?? total;
|
|
6337
6411
|
messagePagination = {
|
|
6338
|
-
total:
|
|
6412
|
+
total: effectiveTotal,
|
|
6339
6413
|
before_index: beforeIndex,
|
|
6340
6414
|
from_index: start,
|
|
6341
6415
|
has_more_older: start > 0,
|
|
6342
6416
|
next_before_index: start > 0 ? start : null
|
|
6343
6417
|
};
|
|
6418
|
+
if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;
|
|
6419
|
+
if (newerPaging) {
|
|
6420
|
+
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
6421
|
+
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
6422
|
+
}
|
|
6344
6423
|
}
|
|
6345
6424
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6346
6425
|
const content = [];
|
|
@@ -6415,6 +6494,65 @@ var StreamerServer = class {
|
|
|
6415
6494
|
});
|
|
6416
6495
|
res.end(JSON.stringify(body));
|
|
6417
6496
|
}
|
|
6497
|
+
// Resolves an active search query to the message a client should anchor to
|
|
6498
|
+
// inside one conversation. Matching is body-only (text first, then
|
|
6499
|
+
// thinking/tool payloads) — a metadata-only search hit (project path, title)
|
|
6500
|
+
// has no scroll target and returns 404 search_target_not_found.
|
|
6501
|
+
//
|
|
6502
|
+
// Implements HTTP QUERY (RFC 10008): the search query travels in a JSON
|
|
6503
|
+
// request body instead of a URL query param — QUERY is safe + idempotent +
|
|
6504
|
+
// cacheable like GET, but (like POST) can carry a body, which fits this
|
|
6505
|
+
// endpoint's single-string input exactly. `Accept-Query` advertises the
|
|
6506
|
+
// supported request media type per the spec.
|
|
6507
|
+
async handleSearchTarget(id, req, res) {
|
|
6508
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0].trim();
|
|
6509
|
+
if (contentType && contentType !== "application/json") {
|
|
6510
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6511
|
+
json(res, 415, {
|
|
6512
|
+
error: "Unsupported Content-Type; expected application/json",
|
|
6513
|
+
code: "unsupported_media_type"
|
|
6514
|
+
});
|
|
6515
|
+
return;
|
|
6516
|
+
}
|
|
6517
|
+
let body;
|
|
6518
|
+
try {
|
|
6519
|
+
body = await readBody(req);
|
|
6520
|
+
} catch {
|
|
6521
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6522
|
+
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
6525
|
+
const q = typeof body?.q === "string" ? body.q.trim() : "";
|
|
6526
|
+
if (!q) {
|
|
6527
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6528
|
+
json(res, 422, { error: "Missing or empty query field: q", code: "invalid_query" });
|
|
6529
|
+
return;
|
|
6530
|
+
}
|
|
6531
|
+
if (q.length > 256) {
|
|
6532
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6533
|
+
json(res, 422, { error: "Query too long (max 256 characters)", code: "invalid_query" });
|
|
6534
|
+
return;
|
|
6535
|
+
}
|
|
6536
|
+
const conversation = await this.findConversationByUuid(id);
|
|
6537
|
+
if (!conversation) {
|
|
6538
|
+
json(res, 404, { error: "Conversation not found", code: "not_found" });
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
const target = findSearchTarget(conversation.messages, q);
|
|
6542
|
+
if (!target) {
|
|
6543
|
+
json(res, 404, { error: "No message body matches query", code: "search_target_not_found" });
|
|
6544
|
+
return;
|
|
6545
|
+
}
|
|
6546
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6547
|
+
json(res, 200, {
|
|
6548
|
+
query: q,
|
|
6549
|
+
message_index: target.messageIndex,
|
|
6550
|
+
uuid: target.uuid,
|
|
6551
|
+
snippet: target.snippet,
|
|
6552
|
+
match_indexes: target.matchIndexes,
|
|
6553
|
+
total_matches: target.totalMatches
|
|
6554
|
+
});
|
|
6555
|
+
}
|
|
6418
6556
|
async handleSearch(url, res) {
|
|
6419
6557
|
const q = url.searchParams.get("q") ?? "";
|
|
6420
6558
|
if (!q) {
|
|
@@ -6434,7 +6572,11 @@ var StreamerServer = class {
|
|
|
6434
6572
|
scanner
|
|
6435
6573
|
);
|
|
6436
6574
|
const adapted = results.map((r) => ({
|
|
6437
|
-
id
|
|
6575
|
+
// Use sessionId so the id matches /api/conversations and resolves via
|
|
6576
|
+
// findConversationByUuid — a client can round-trip a search result into
|
|
6577
|
+
// GET /api/conversations/:id or the search-target QUERY. The old
|
|
6578
|
+
// filename-stem derivation produced an id no other endpoint recognized.
|
|
6579
|
+
id: r.meta.sessionId || r.meta.id,
|
|
6438
6580
|
title: r.meta.projectName,
|
|
6439
6581
|
sessionName: r.meta.sessionName || void 0,
|
|
6440
6582
|
filePath: r.meta.filePath,
|