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