@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/cli.cjs
CHANGED
|
@@ -135843,17 +135843,17 @@ var corsMiddleware = (configValue) => {
|
|
|
135843
135843
|
const raw2 = c.env.outgoing;
|
|
135844
135844
|
raw2.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
135845
135845
|
raw2.setHeader("Vary", "Origin");
|
|
135846
|
-
raw2.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
135846
|
+
raw2.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
135847
135847
|
raw2.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
135848
|
-
raw2.setHeader("Access-Control-Expose-Headers", "ETag");
|
|
135848
|
+
raw2.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
135849
135849
|
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
135850
135850
|
c.res.headers.set("Vary", "Origin");
|
|
135851
|
-
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
135851
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
135852
135852
|
c.res.headers.set(
|
|
135853
135853
|
"Access-Control-Allow-Headers",
|
|
135854
135854
|
"Authorization, Content-Type, If-None-Match"
|
|
135855
135855
|
);
|
|
135856
|
-
c.res.headers.set("Access-Control-Expose-Headers", "ETag");
|
|
135856
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
135857
135857
|
}
|
|
135858
135858
|
if (c.req.method === "OPTIONS") {
|
|
135859
135859
|
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
@@ -135895,6 +135895,11 @@ var createConversationRoutes = (deps) => {
|
|
|
135895
135895
|
await deps.handleConversationsCount(url2, c.env.outgoing);
|
|
135896
135896
|
return alreadyHandled2();
|
|
135897
135897
|
});
|
|
135898
|
+
app.on("QUERY", "/:id{.+}/search-target", async (c) => {
|
|
135899
|
+
const id = c.req.param("id");
|
|
135900
|
+
await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);
|
|
135901
|
+
return alreadyHandled2();
|
|
135902
|
+
});
|
|
135898
135903
|
app.get("/:id{.+}", async (c) => {
|
|
135899
135904
|
const id = c.req.param("id");
|
|
135900
135905
|
const url2 = new URL(c.req.url);
|
|
@@ -136481,6 +136486,9 @@ var CODEX_CLI_PROVIDER2 = "codex-cli";
|
|
|
136481
136486
|
function isProviderName(value) {
|
|
136482
136487
|
return value === CLAUDE_CODE_PROVIDER2 || value === CODEX_CLI_PROVIDER2;
|
|
136483
136488
|
}
|
|
136489
|
+
function coerceProviderForRunner(value) {
|
|
136490
|
+
return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER2;
|
|
136491
|
+
}
|
|
136484
136492
|
function isProviderResumable(_provider, availabilityResumable) {
|
|
136485
136493
|
return availabilityResumable;
|
|
136486
136494
|
}
|
|
@@ -141350,6 +141358,55 @@ var ConversationWatcher = class {
|
|
|
141350
141358
|
}
|
|
141351
141359
|
};
|
|
141352
141360
|
|
|
141361
|
+
// src/services/conversations/findSearchTarget.ts
|
|
141362
|
+
var SNIPPET_CONTEXT = 60;
|
|
141363
|
+
var MAX_MATCH_INDEXES = 1e3;
|
|
141364
|
+
function buildSnippet(source, matchStart, matchLength) {
|
|
141365
|
+
const start = Math.max(0, matchStart - SNIPPET_CONTEXT);
|
|
141366
|
+
const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);
|
|
141367
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
141368
|
+
const suffix = end < source.length ? "\u2026" : "";
|
|
141369
|
+
return `${prefix}${source.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
141370
|
+
}
|
|
141371
|
+
function extendedBody(m2) {
|
|
141372
|
+
const parts = [];
|
|
141373
|
+
if (m2.isThinking && m2.thinkingContent) parts.push(m2.thinkingContent);
|
|
141374
|
+
for (const b2 of m2.metadata?.toolUseBlocks ?? []) {
|
|
141375
|
+
if (b2.input !== void 0) parts.push(JSON.stringify(b2.input));
|
|
141376
|
+
}
|
|
141377
|
+
for (const r of m2.metadata?.toolResults ?? []) {
|
|
141378
|
+
if (r.content !== void 0) parts.push(JSON.stringify(r.content));
|
|
141379
|
+
}
|
|
141380
|
+
return parts.join("\n");
|
|
141381
|
+
}
|
|
141382
|
+
function collectMatches(messages, needle, body) {
|
|
141383
|
+
const indexes = [];
|
|
141384
|
+
for (let i = 0; i < messages.length; i++) {
|
|
141385
|
+
if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);
|
|
141386
|
+
}
|
|
141387
|
+
return indexes;
|
|
141388
|
+
}
|
|
141389
|
+
function findSearchTarget(messages, query) {
|
|
141390
|
+
const needle = query.toLowerCase();
|
|
141391
|
+
let body = (m2) => m2.text ?? "";
|
|
141392
|
+
let matches = collectMatches(messages, needle, body);
|
|
141393
|
+
if (matches.length === 0) {
|
|
141394
|
+
body = extendedBody;
|
|
141395
|
+
matches = collectMatches(messages, needle, body);
|
|
141396
|
+
}
|
|
141397
|
+
if (matches.length === 0) return null;
|
|
141398
|
+
const anchorIndex = matches[matches.length - 1];
|
|
141399
|
+
const anchorBody = body(messages[anchorIndex]);
|
|
141400
|
+
const at2 = anchorBody.toLowerCase().indexOf(needle);
|
|
141401
|
+
return {
|
|
141402
|
+
messageIndex: anchorIndex,
|
|
141403
|
+
uuid: messages[anchorIndex].uuid ?? null,
|
|
141404
|
+
snippet: buildSnippet(anchorBody, at2, query.length),
|
|
141405
|
+
matchIndexes: matches.slice(-MAX_MATCH_INDEXES),
|
|
141406
|
+
totalMatches: matches.length
|
|
141407
|
+
};
|
|
141408
|
+
}
|
|
141409
|
+
|
|
141353
141410
|
// src/services/conversations/pruneAgentConversations.ts
|
|
141354
141411
|
var import_fs23 = require("fs");
|
|
141355
141412
|
function pruneAgentConversations(cache) {
|
|
@@ -142473,6 +142530,7 @@ var StreamerServer = class {
|
|
|
142473
142530
|
handleConversationsCount: (url2, res) => this.handleConversationsCount(url2, res),
|
|
142474
142531
|
handleGetConversation: (id, url2, res, ifNoneMatch) => this.handleGetConversation(id, url2, res, ifNoneMatch),
|
|
142475
142532
|
handleSearch: (url2, res) => this.handleSearch(url2, res),
|
|
142533
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
142476
142534
|
handleListProjects: (url2, res) => handleListProjects(url2, res),
|
|
142477
142535
|
handleGetPopularProjects: (url2, res) => this.handleGetPopularProjects(url2, res),
|
|
142478
142536
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -143373,7 +143431,7 @@ var StreamerServer = class {
|
|
|
143373
143431
|
messageCount: etagSource.messageCount,
|
|
143374
143432
|
timestamp: etagSource.timestamp
|
|
143375
143433
|
});
|
|
143376
|
-
const isFirstPage = !url2.searchParams.has("before_index");
|
|
143434
|
+
const isFirstPage = !url2.searchParams.has("before_index") && !url2.searchParams.has("anchor_index") && !url2.searchParams.has("after_index");
|
|
143377
143435
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
143378
143436
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
143379
143437
|
res.end();
|
|
@@ -143381,29 +143439,53 @@ var StreamerServer = class {
|
|
|
143381
143439
|
}
|
|
143382
143440
|
const filtered = conversation.messages;
|
|
143383
143441
|
const total = filtered.length;
|
|
143384
|
-
const
|
|
143442
|
+
const hasAnchor = url2.searchParams.has("anchor_index");
|
|
143443
|
+
const hasAfter = url2.searchParams.has("after_index");
|
|
143444
|
+
const usePaging = url2.searchParams.has("msg_limit") || url2.searchParams.has("before_index") || hasAnchor || hasAfter;
|
|
143385
143445
|
let slice = filtered;
|
|
143386
143446
|
let fromIdx = 0;
|
|
143387
143447
|
let messagePagination;
|
|
143388
143448
|
if (usePaging) {
|
|
143389
143449
|
const limit = Math.min(Math.max(intParam(url2, "msg_limit", 80), 1), 500);
|
|
143390
143450
|
let beforeIndex = total;
|
|
143451
|
+
let scanLimit = limit;
|
|
143452
|
+
let anchorIndex = null;
|
|
143453
|
+
let newerPaging = false;
|
|
143391
143454
|
if (url2.searchParams.has("before_index")) {
|
|
143392
143455
|
beforeIndex = intParam(url2, "before_index", total);
|
|
143393
143456
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
143457
|
+
} else if (hasAfter) {
|
|
143458
|
+
const from = Math.min(Math.max(intParam(url2, "after_index", 0), 0), total);
|
|
143459
|
+
beforeIndex = Math.min(total, from + limit);
|
|
143460
|
+
scanLimit = beforeIndex - from;
|
|
143461
|
+
newerPaging = true;
|
|
143462
|
+
} else if (hasAnchor) {
|
|
143463
|
+
anchorIndex = Math.min(
|
|
143464
|
+
Math.max(intParam(url2, "anchor_index", 0), 0),
|
|
143465
|
+
Math.max(0, total - 1)
|
|
143466
|
+
);
|
|
143467
|
+
const from = Math.max(0, anchorIndex - Math.floor(limit / 2));
|
|
143468
|
+
beforeIndex = Math.min(total, from + limit);
|
|
143469
|
+
newerPaging = true;
|
|
143394
143470
|
}
|
|
143395
143471
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
143396
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
143397
|
-
const start = page?.fromIndex ?? Math.max(0, beforeIndex -
|
|
143472
|
+
const page = scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null;
|
|
143473
|
+
const start = page?.fromIndex ?? Math.max(0, beforeIndex - scanLimit);
|
|
143398
143474
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
143399
143475
|
fromIdx = start;
|
|
143476
|
+
const effectiveTotal = page?.total ?? total;
|
|
143400
143477
|
messagePagination = {
|
|
143401
|
-
total:
|
|
143478
|
+
total: effectiveTotal,
|
|
143402
143479
|
before_index: beforeIndex,
|
|
143403
143480
|
from_index: start,
|
|
143404
143481
|
has_more_older: start > 0,
|
|
143405
143482
|
next_before_index: start > 0 ? start : null
|
|
143406
143483
|
};
|
|
143484
|
+
if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;
|
|
143485
|
+
if (newerPaging) {
|
|
143486
|
+
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
143487
|
+
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
143488
|
+
}
|
|
143407
143489
|
}
|
|
143408
143490
|
const messagesPayload = slice.map((m2, localIdx) => {
|
|
143409
143491
|
const content = [];
|
|
@@ -143443,7 +143525,7 @@ var StreamerServer = class {
|
|
|
143443
143525
|
});
|
|
143444
143526
|
const conv = conversation;
|
|
143445
143527
|
const cachedConvMeta = this.cache?.getMetaById(id);
|
|
143446
|
-
const convProvider = conv.provider ?? cachedConvMeta?.provider
|
|
143528
|
+
const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);
|
|
143447
143529
|
const availability = classifyResumability(conv.projectPath);
|
|
143448
143530
|
const body = {
|
|
143449
143531
|
meta: {
|
|
@@ -143478,6 +143560,65 @@ var StreamerServer = class {
|
|
|
143478
143560
|
});
|
|
143479
143561
|
res.end(JSON.stringify(body));
|
|
143480
143562
|
}
|
|
143563
|
+
// Resolves an active search query to the message a client should anchor to
|
|
143564
|
+
// inside one conversation. Matching is body-only (text first, then
|
|
143565
|
+
// thinking/tool payloads) — a metadata-only search hit (project path, title)
|
|
143566
|
+
// has no scroll target and returns 404 search_target_not_found.
|
|
143567
|
+
//
|
|
143568
|
+
// Implements HTTP QUERY (RFC 10008): the search query travels in a JSON
|
|
143569
|
+
// request body instead of a URL query param — QUERY is safe + idempotent +
|
|
143570
|
+
// cacheable like GET, but (like POST) can carry a body, which fits this
|
|
143571
|
+
// endpoint's single-string input exactly. `Accept-Query` advertises the
|
|
143572
|
+
// supported request media type per the spec.
|
|
143573
|
+
async handleSearchTarget(id, req, res) {
|
|
143574
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0].trim();
|
|
143575
|
+
if (contentType && contentType !== "application/json") {
|
|
143576
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143577
|
+
json2(res, 415, {
|
|
143578
|
+
error: "Unsupported Content-Type; expected application/json",
|
|
143579
|
+
code: "unsupported_media_type"
|
|
143580
|
+
});
|
|
143581
|
+
return;
|
|
143582
|
+
}
|
|
143583
|
+
let body;
|
|
143584
|
+
try {
|
|
143585
|
+
body = await readBody(req);
|
|
143586
|
+
} catch {
|
|
143587
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143588
|
+
json2(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
143589
|
+
return;
|
|
143590
|
+
}
|
|
143591
|
+
const q2 = typeof body?.q === "string" ? body.q.trim() : "";
|
|
143592
|
+
if (!q2) {
|
|
143593
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143594
|
+
json2(res, 422, { error: "Missing or empty query field: q", code: "invalid_query" });
|
|
143595
|
+
return;
|
|
143596
|
+
}
|
|
143597
|
+
if (q2.length > 256) {
|
|
143598
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143599
|
+
json2(res, 422, { error: "Query too long (max 256 characters)", code: "invalid_query" });
|
|
143600
|
+
return;
|
|
143601
|
+
}
|
|
143602
|
+
const conversation = await this.findConversationByUuid(id);
|
|
143603
|
+
if (!conversation) {
|
|
143604
|
+
json2(res, 404, { error: "Conversation not found", code: "not_found" });
|
|
143605
|
+
return;
|
|
143606
|
+
}
|
|
143607
|
+
const target = findSearchTarget(conversation.messages, q2);
|
|
143608
|
+
if (!target) {
|
|
143609
|
+
json2(res, 404, { error: "No message body matches query", code: "search_target_not_found" });
|
|
143610
|
+
return;
|
|
143611
|
+
}
|
|
143612
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143613
|
+
json2(res, 200, {
|
|
143614
|
+
query: q2,
|
|
143615
|
+
message_index: target.messageIndex,
|
|
143616
|
+
uuid: target.uuid,
|
|
143617
|
+
snippet: target.snippet,
|
|
143618
|
+
match_indexes: target.matchIndexes,
|
|
143619
|
+
total_matches: target.totalMatches
|
|
143620
|
+
});
|
|
143621
|
+
}
|
|
143481
143622
|
async handleSearch(url2, res) {
|
|
143482
143623
|
const q2 = url2.searchParams.get("q") ?? "";
|
|
143483
143624
|
if (!q2) {
|
|
@@ -143497,7 +143638,11 @@ var StreamerServer = class {
|
|
|
143497
143638
|
scanner
|
|
143498
143639
|
);
|
|
143499
143640
|
const adapted = results.map((r) => ({
|
|
143500
|
-
id
|
|
143641
|
+
// Use sessionId so the id matches /api/conversations and resolves via
|
|
143642
|
+
// findConversationByUuid — a client can round-trip a search result into
|
|
143643
|
+
// GET /api/conversations/:id or the search-target QUERY. The old
|
|
143644
|
+
// filename-stem derivation produced an id no other endpoint recognized.
|
|
143645
|
+
id: r.meta.sessionId || r.meta.id,
|
|
143501
143646
|
title: r.meta.projectName,
|
|
143502
143647
|
sessionName: r.meta.sessionName || void 0,
|
|
143503
143648
|
filePath: r.meta.filePath,
|
|
@@ -143594,7 +143739,7 @@ var StreamerServer = class {
|
|
|
143594
143739
|
return;
|
|
143595
143740
|
}
|
|
143596
143741
|
const cachedConvMeta = this.cache?.getMetaById(sessionId);
|
|
143597
|
-
const provider = conv?.provider ?? cachedConvMeta?.provider
|
|
143742
|
+
const provider = coerceProviderForRunner(conv?.provider ?? cachedConvMeta?.provider);
|
|
143598
143743
|
const session = await this.ptyManager.start(sessionId, {
|
|
143599
143744
|
provider,
|
|
143600
143745
|
projectPath,
|