@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.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
|
@@ -673,6 +673,9 @@ var CODEX_CLI_PROVIDER = "codex-cli";
|
|
|
673
673
|
function isProviderName(value) {
|
|
674
674
|
return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
|
|
675
675
|
}
|
|
676
|
+
function coerceProviderForRunner(value) {
|
|
677
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
|
|
678
|
+
}
|
|
676
679
|
function isProviderResumable(_provider, availabilityResumable) {
|
|
677
680
|
return availabilityResumable;
|
|
678
681
|
}
|
|
@@ -2615,17 +2618,17 @@ var corsMiddleware = (configValue) => {
|
|
|
2615
2618
|
const raw = c.env.outgoing;
|
|
2616
2619
|
raw.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
2617
2620
|
raw.setHeader("Vary", "Origin");
|
|
2618
|
-
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2621
|
+
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2619
2622
|
raw.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
2620
|
-
raw.setHeader("Access-Control-Expose-Headers", "ETag");
|
|
2623
|
+
raw.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2621
2624
|
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
2622
2625
|
c.res.headers.set("Vary", "Origin");
|
|
2623
|
-
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
2626
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2624
2627
|
c.res.headers.set(
|
|
2625
2628
|
"Access-Control-Allow-Headers",
|
|
2626
2629
|
"Authorization, Content-Type, If-None-Match"
|
|
2627
2630
|
);
|
|
2628
|
-
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
2631
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2629
2632
|
}
|
|
2630
2633
|
if (c.req.method === "OPTIONS") {
|
|
2631
2634
|
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
@@ -2669,6 +2672,11 @@ var createConversationRoutes = (deps) => {
|
|
|
2669
2672
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
2670
2673
|
return alreadyHandled2();
|
|
2671
2674
|
});
|
|
2675
|
+
app.on("QUERY", "/:id{.+}/search-target", async (c) => {
|
|
2676
|
+
const id = c.req.param("id");
|
|
2677
|
+
await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);
|
|
2678
|
+
return alreadyHandled2();
|
|
2679
|
+
});
|
|
2672
2680
|
app.get("/:id{.+}", async (c) => {
|
|
2673
2681
|
const id = c.req.param("id");
|
|
2674
2682
|
const url = new URL(c.req.url);
|
|
@@ -4486,6 +4494,55 @@ var ConversationWatcher = class {
|
|
|
4486
4494
|
}
|
|
4487
4495
|
};
|
|
4488
4496
|
|
|
4497
|
+
// src/services/conversations/findSearchTarget.ts
|
|
4498
|
+
var SNIPPET_CONTEXT = 60;
|
|
4499
|
+
var MAX_MATCH_INDEXES = 1e3;
|
|
4500
|
+
function buildSnippet(source, matchStart, matchLength) {
|
|
4501
|
+
const start = Math.max(0, matchStart - SNIPPET_CONTEXT);
|
|
4502
|
+
const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);
|
|
4503
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
4504
|
+
const suffix = end < source.length ? "\u2026" : "";
|
|
4505
|
+
return `${prefix}${source.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
4506
|
+
}
|
|
4507
|
+
function extendedBody(m) {
|
|
4508
|
+
const parts = [];
|
|
4509
|
+
if (m.isThinking && m.thinkingContent) parts.push(m.thinkingContent);
|
|
4510
|
+
for (const b of m.metadata?.toolUseBlocks ?? []) {
|
|
4511
|
+
if (b.input !== void 0) parts.push(JSON.stringify(b.input));
|
|
4512
|
+
}
|
|
4513
|
+
for (const r of m.metadata?.toolResults ?? []) {
|
|
4514
|
+
if (r.content !== void 0) parts.push(JSON.stringify(r.content));
|
|
4515
|
+
}
|
|
4516
|
+
return parts.join("\n");
|
|
4517
|
+
}
|
|
4518
|
+
function collectMatches(messages, needle, body) {
|
|
4519
|
+
const indexes = [];
|
|
4520
|
+
for (let i = 0; i < messages.length; i++) {
|
|
4521
|
+
if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);
|
|
4522
|
+
}
|
|
4523
|
+
return indexes;
|
|
4524
|
+
}
|
|
4525
|
+
function findSearchTarget(messages, query) {
|
|
4526
|
+
const needle = query.toLowerCase();
|
|
4527
|
+
let body = (m) => m.text ?? "";
|
|
4528
|
+
let matches = collectMatches(messages, needle, body);
|
|
4529
|
+
if (matches.length === 0) {
|
|
4530
|
+
body = extendedBody;
|
|
4531
|
+
matches = collectMatches(messages, needle, body);
|
|
4532
|
+
}
|
|
4533
|
+
if (matches.length === 0) return null;
|
|
4534
|
+
const anchorIndex = matches[matches.length - 1];
|
|
4535
|
+
const anchorBody = body(messages[anchorIndex]);
|
|
4536
|
+
const at = anchorBody.toLowerCase().indexOf(needle);
|
|
4537
|
+
return {
|
|
4538
|
+
messageIndex: anchorIndex,
|
|
4539
|
+
uuid: messages[anchorIndex].uuid ?? null,
|
|
4540
|
+
snippet: buildSnippet(anchorBody, at, query.length),
|
|
4541
|
+
matchIndexes: matches.slice(-MAX_MATCH_INDEXES),
|
|
4542
|
+
totalMatches: matches.length
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4489
4546
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4490
4547
|
import { existsSync as existsSync6 } from "fs";
|
|
4491
4548
|
function pruneAgentConversations(cache) {
|
|
@@ -5371,6 +5428,7 @@ var StreamerServer = class {
|
|
|
5371
5428
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
5372
5429
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
5373
5430
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
5431
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
5374
5432
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
5375
5433
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
5376
5434
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -6271,7 +6329,7 @@ var StreamerServer = class {
|
|
|
6271
6329
|
messageCount: etagSource.messageCount,
|
|
6272
6330
|
timestamp: etagSource.timestamp
|
|
6273
6331
|
});
|
|
6274
|
-
const isFirstPage = !url.searchParams.has("before_index");
|
|
6332
|
+
const isFirstPage = !url.searchParams.has("before_index") && !url.searchParams.has("anchor_index") && !url.searchParams.has("after_index");
|
|
6275
6333
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
6276
6334
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
6277
6335
|
res.end();
|
|
@@ -6279,29 +6337,53 @@ var StreamerServer = class {
|
|
|
6279
6337
|
}
|
|
6280
6338
|
const filtered = conversation.messages;
|
|
6281
6339
|
const total = filtered.length;
|
|
6282
|
-
const
|
|
6340
|
+
const hasAnchor = url.searchParams.has("anchor_index");
|
|
6341
|
+
const hasAfter = url.searchParams.has("after_index");
|
|
6342
|
+
const usePaging = url.searchParams.has("msg_limit") || url.searchParams.has("before_index") || hasAnchor || hasAfter;
|
|
6283
6343
|
let slice = filtered;
|
|
6284
6344
|
let fromIdx = 0;
|
|
6285
6345
|
let messagePagination;
|
|
6286
6346
|
if (usePaging) {
|
|
6287
6347
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6288
6348
|
let beforeIndex = total;
|
|
6349
|
+
let scanLimit = limit;
|
|
6350
|
+
let anchorIndex = null;
|
|
6351
|
+
let newerPaging = false;
|
|
6289
6352
|
if (url.searchParams.has("before_index")) {
|
|
6290
6353
|
beforeIndex = intParam(url, "before_index", total);
|
|
6291
6354
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
6355
|
+
} else if (hasAfter) {
|
|
6356
|
+
const from = Math.min(Math.max(intParam(url, "after_index", 0), 0), total);
|
|
6357
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6358
|
+
scanLimit = beforeIndex - from;
|
|
6359
|
+
newerPaging = true;
|
|
6360
|
+
} else if (hasAnchor) {
|
|
6361
|
+
anchorIndex = Math.min(
|
|
6362
|
+
Math.max(intParam(url, "anchor_index", 0), 0),
|
|
6363
|
+
Math.max(0, total - 1)
|
|
6364
|
+
);
|
|
6365
|
+
const from = Math.max(0, anchorIndex - Math.floor(limit / 2));
|
|
6366
|
+
beforeIndex = Math.min(total, from + limit);
|
|
6367
|
+
newerPaging = true;
|
|
6292
6368
|
}
|
|
6293
6369
|
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 -
|
|
6370
|
+
const page = scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null;
|
|
6371
|
+
const start = page?.fromIndex ?? Math.max(0, beforeIndex - scanLimit);
|
|
6296
6372
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6297
6373
|
fromIdx = start;
|
|
6374
|
+
const effectiveTotal = page?.total ?? total;
|
|
6298
6375
|
messagePagination = {
|
|
6299
|
-
total:
|
|
6376
|
+
total: effectiveTotal,
|
|
6300
6377
|
before_index: beforeIndex,
|
|
6301
6378
|
from_index: start,
|
|
6302
6379
|
has_more_older: start > 0,
|
|
6303
6380
|
next_before_index: start > 0 ? start : null
|
|
6304
6381
|
};
|
|
6382
|
+
if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;
|
|
6383
|
+
if (newerPaging) {
|
|
6384
|
+
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
6385
|
+
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
6386
|
+
}
|
|
6305
6387
|
}
|
|
6306
6388
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6307
6389
|
const content = [];
|
|
@@ -6341,7 +6423,7 @@ var StreamerServer = class {
|
|
|
6341
6423
|
});
|
|
6342
6424
|
const conv = conversation;
|
|
6343
6425
|
const cachedConvMeta = this.cache?.getMetaById(id);
|
|
6344
|
-
const convProvider = conv.provider ?? cachedConvMeta?.provider
|
|
6426
|
+
const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);
|
|
6345
6427
|
const availability = classifyResumability(conv.projectPath);
|
|
6346
6428
|
const body = {
|
|
6347
6429
|
meta: {
|
|
@@ -6376,6 +6458,65 @@ var StreamerServer = class {
|
|
|
6376
6458
|
});
|
|
6377
6459
|
res.end(JSON.stringify(body));
|
|
6378
6460
|
}
|
|
6461
|
+
// Resolves an active search query to the message a client should anchor to
|
|
6462
|
+
// inside one conversation. Matching is body-only (text first, then
|
|
6463
|
+
// thinking/tool payloads) — a metadata-only search hit (project path, title)
|
|
6464
|
+
// has no scroll target and returns 404 search_target_not_found.
|
|
6465
|
+
//
|
|
6466
|
+
// Implements HTTP QUERY (RFC 10008): the search query travels in a JSON
|
|
6467
|
+
// request body instead of a URL query param — QUERY is safe + idempotent +
|
|
6468
|
+
// cacheable like GET, but (like POST) can carry a body, which fits this
|
|
6469
|
+
// endpoint's single-string input exactly. `Accept-Query` advertises the
|
|
6470
|
+
// supported request media type per the spec.
|
|
6471
|
+
async handleSearchTarget(id, req, res) {
|
|
6472
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0].trim();
|
|
6473
|
+
if (contentType && contentType !== "application/json") {
|
|
6474
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6475
|
+
json(res, 415, {
|
|
6476
|
+
error: "Unsupported Content-Type; expected application/json",
|
|
6477
|
+
code: "unsupported_media_type"
|
|
6478
|
+
});
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
let body;
|
|
6482
|
+
try {
|
|
6483
|
+
body = await readBody(req);
|
|
6484
|
+
} catch {
|
|
6485
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6486
|
+
json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
const q = typeof body?.q === "string" ? body.q.trim() : "";
|
|
6490
|
+
if (!q) {
|
|
6491
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6492
|
+
json(res, 422, { error: "Missing or empty query field: q", code: "invalid_query" });
|
|
6493
|
+
return;
|
|
6494
|
+
}
|
|
6495
|
+
if (q.length > 256) {
|
|
6496
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6497
|
+
json(res, 422, { error: "Query too long (max 256 characters)", code: "invalid_query" });
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6500
|
+
const conversation = await this.findConversationByUuid(id);
|
|
6501
|
+
if (!conversation) {
|
|
6502
|
+
json(res, 404, { error: "Conversation not found", code: "not_found" });
|
|
6503
|
+
return;
|
|
6504
|
+
}
|
|
6505
|
+
const target = findSearchTarget(conversation.messages, q);
|
|
6506
|
+
if (!target) {
|
|
6507
|
+
json(res, 404, { error: "No message body matches query", code: "search_target_not_found" });
|
|
6508
|
+
return;
|
|
6509
|
+
}
|
|
6510
|
+
res.setHeader("Accept-Query", "application/json");
|
|
6511
|
+
json(res, 200, {
|
|
6512
|
+
query: q,
|
|
6513
|
+
message_index: target.messageIndex,
|
|
6514
|
+
uuid: target.uuid,
|
|
6515
|
+
snippet: target.snippet,
|
|
6516
|
+
match_indexes: target.matchIndexes,
|
|
6517
|
+
total_matches: target.totalMatches
|
|
6518
|
+
});
|
|
6519
|
+
}
|
|
6379
6520
|
async handleSearch(url, res) {
|
|
6380
6521
|
const q = url.searchParams.get("q") ?? "";
|
|
6381
6522
|
if (!q) {
|
|
@@ -6395,7 +6536,11 @@ var StreamerServer = class {
|
|
|
6395
6536
|
scanner
|
|
6396
6537
|
);
|
|
6397
6538
|
const adapted = results.map((r) => ({
|
|
6398
|
-
id
|
|
6539
|
+
// Use sessionId so the id matches /api/conversations and resolves via
|
|
6540
|
+
// findConversationByUuid — a client can round-trip a search result into
|
|
6541
|
+
// GET /api/conversations/:id or the search-target QUERY. The old
|
|
6542
|
+
// filename-stem derivation produced an id no other endpoint recognized.
|
|
6543
|
+
id: r.meta.sessionId || r.meta.id,
|
|
6399
6544
|
title: r.meta.projectName,
|
|
6400
6545
|
sessionName: r.meta.sessionName || void 0,
|
|
6401
6546
|
filePath: r.meta.filePath,
|
|
@@ -6492,7 +6637,7 @@ var StreamerServer = class {
|
|
|
6492
6637
|
return;
|
|
6493
6638
|
}
|
|
6494
6639
|
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
6495
|
-
const provider = conv?.provider ?? cachedConvMeta?.provider
|
|
6640
|
+
const provider = coerceProviderForRunner(conv?.provider ?? cachedConvMeta?.provider);
|
|
6496
6641
|
const session = await this.ptyManager.start(sessionId, {
|
|
6497
6642
|
provider,
|
|
6498
6643
|
projectPath,
|