@threadbase-sh/streamer 1.25.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 +207 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +207 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +207 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -375,6 +375,15 @@ function loadPublicUrl() {
|
|
|
375
375
|
}
|
|
376
376
|
return void 0;
|
|
377
377
|
}
|
|
378
|
+
function loadBrowserCors() {
|
|
379
|
+
try {
|
|
380
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
381
|
+
const match = content.match(/browser_cors:\s*(.+)/);
|
|
382
|
+
if (match?.[1]) return match[1].trim();
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
378
387
|
function loadCacheDir() {
|
|
379
388
|
try {
|
|
380
389
|
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
@@ -2612,25 +2621,55 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
2612
2621
|
};
|
|
2613
2622
|
|
|
2614
2623
|
// src/api/middleware/cors.middleware.ts
|
|
2615
|
-
var
|
|
2624
|
+
var DEFAULT_DEV_ORIGINS = [
|
|
2616
2625
|
"http://localhost:8081",
|
|
2617
2626
|
"http://localhost:19006",
|
|
2618
2627
|
"http://localhost:3000"
|
|
2619
|
-
]
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
const
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2628
|
+
];
|
|
2629
|
+
function resolveAllowedOrigins(raw) {
|
|
2630
|
+
if (!raw) return null;
|
|
2631
|
+
const trimmed = raw.trim();
|
|
2632
|
+
const lower = trimmed.toLowerCase();
|
|
2633
|
+
if (lower === "0" || lower === "false" || lower === "no" || lower === "off" || trimmed === "") {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
const origins = new Set(DEFAULT_DEV_ORIGINS);
|
|
2637
|
+
if (!["1", "true", "yes", "on"].includes(lower)) {
|
|
2638
|
+
for (const o of trimmed.split(",")) {
|
|
2639
|
+
const origin = o.trim();
|
|
2640
|
+
if (origin) origins.add(origin);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return origins;
|
|
2644
|
+
}
|
|
2645
|
+
var corsMiddleware = (configValue) => {
|
|
2646
|
+
const allowedOrigins = resolveAllowedOrigins(
|
|
2647
|
+
process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue
|
|
2648
|
+
);
|
|
2649
|
+
return async (c, next) => {
|
|
2650
|
+
const origin = c.req.header("origin");
|
|
2651
|
+
const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;
|
|
2652
|
+
if (allowedOrigin) {
|
|
2653
|
+
const raw = c.env.outgoing;
|
|
2654
|
+
raw.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
2655
|
+
raw.setHeader("Vary", "Origin");
|
|
2656
|
+
raw.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2657
|
+
raw.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
2658
|
+
raw.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2659
|
+
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
2660
|
+
c.res.headers.set("Vary", "Origin");
|
|
2661
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
2662
|
+
c.res.headers.set(
|
|
2663
|
+
"Access-Control-Allow-Headers",
|
|
2664
|
+
"Authorization, Content-Type, If-None-Match"
|
|
2665
|
+
);
|
|
2666
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
2667
|
+
}
|
|
2668
|
+
if (c.req.method === "OPTIONS") {
|
|
2669
|
+
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
2670
|
+
}
|
|
2671
|
+
await next();
|
|
2672
|
+
};
|
|
2634
2673
|
};
|
|
2635
2674
|
|
|
2636
2675
|
// src/api/middleware/error.middleware.ts
|
|
@@ -2668,6 +2707,11 @@ var createConversationRoutes = (deps) => {
|
|
|
2668
2707
|
await deps.handleConversationsCount(url, c.env.outgoing);
|
|
2669
2708
|
return alreadyHandled2();
|
|
2670
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
|
+
});
|
|
2671
2715
|
app.get("/:id{.+}", async (c) => {
|
|
2672
2716
|
const id = c.req.param("id");
|
|
2673
2717
|
const url = new URL(c.req.url);
|
|
@@ -3045,7 +3089,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
3045
3089
|
event: "http.request"
|
|
3046
3090
|
});
|
|
3047
3091
|
});
|
|
3048
|
-
app.use("*", corsMiddleware());
|
|
3092
|
+
app.use("*", corsMiddleware(deps.browserCors));
|
|
3049
3093
|
app.use("*", authMiddleware(deps));
|
|
3050
3094
|
app.onError(errorMiddleware);
|
|
3051
3095
|
app.route("/healthz", createHealthRoutes());
|
|
@@ -4486,6 +4530,55 @@ var ConversationWatcher = class {
|
|
|
4486
4530
|
}
|
|
4487
4531
|
};
|
|
4488
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
|
+
|
|
4489
4582
|
// src/services/conversations/pruneAgentConversations.ts
|
|
4490
4583
|
var import_fs11 = require("fs");
|
|
4491
4584
|
function pruneAgentConversations(cache) {
|
|
@@ -5101,6 +5194,7 @@ var StreamerServer = class {
|
|
|
5101
5194
|
disableDb = false;
|
|
5102
5195
|
browseRoot = null;
|
|
5103
5196
|
publicUrl = null;
|
|
5197
|
+
browserCors;
|
|
5104
5198
|
pairTokens = new PairTokenStore();
|
|
5105
5199
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
5106
5200
|
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
@@ -5186,6 +5280,7 @@ var StreamerServer = class {
|
|
|
5186
5280
|
this.log.warn(`Warning: ${result.error}`, { error: result.error });
|
|
5187
5281
|
}
|
|
5188
5282
|
}
|
|
5283
|
+
this.browserCors = config.browserCors ?? loadBrowserCors();
|
|
5189
5284
|
this.sessionStore = new SessionStore();
|
|
5190
5285
|
this.wsHub = new WSHub();
|
|
5191
5286
|
this.fileWatcher = new ConversationWatcher({
|
|
@@ -5340,6 +5435,7 @@ var StreamerServer = class {
|
|
|
5340
5435
|
rotateApiKey: () => this.rotateApiKey(),
|
|
5341
5436
|
publicUrl: this.publicUrl,
|
|
5342
5437
|
browseRoot: this.browseRoot,
|
|
5438
|
+
browserCors: this.browserCors,
|
|
5343
5439
|
ptyManager: this.ptyManager,
|
|
5344
5440
|
sessionStore: this.sessionStore,
|
|
5345
5441
|
wsHub: this.wsHub,
|
|
@@ -5368,6 +5464,7 @@ var StreamerServer = class {
|
|
|
5368
5464
|
handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),
|
|
5369
5465
|
handleGetConversation: (id, url, res, ifNoneMatch) => this.handleGetConversation(id, url, res, ifNoneMatch),
|
|
5370
5466
|
handleSearch: (url, res) => this.handleSearch(url, res),
|
|
5467
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
5371
5468
|
handleListProjects: (url, res) => handleListProjects(url, res),
|
|
5372
5469
|
handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),
|
|
5373
5470
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -6268,7 +6365,7 @@ var StreamerServer = class {
|
|
|
6268
6365
|
messageCount: etagSource.messageCount,
|
|
6269
6366
|
timestamp: etagSource.timestamp
|
|
6270
6367
|
});
|
|
6271
|
-
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");
|
|
6272
6369
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
6273
6370
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
6274
6371
|
res.end();
|
|
@@ -6276,29 +6373,53 @@ var StreamerServer = class {
|
|
|
6276
6373
|
}
|
|
6277
6374
|
const filtered = conversation.messages;
|
|
6278
6375
|
const total = filtered.length;
|
|
6279
|
-
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;
|
|
6280
6379
|
let slice = filtered;
|
|
6281
6380
|
let fromIdx = 0;
|
|
6282
6381
|
let messagePagination;
|
|
6283
6382
|
if (usePaging) {
|
|
6284
6383
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6285
6384
|
let beforeIndex = total;
|
|
6385
|
+
let scanLimit = limit;
|
|
6386
|
+
let anchorIndex = null;
|
|
6387
|
+
let newerPaging = false;
|
|
6286
6388
|
if (url.searchParams.has("before_index")) {
|
|
6287
6389
|
beforeIndex = intParam(url, "before_index", total);
|
|
6288
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;
|
|
6289
6404
|
}
|
|
6290
6405
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
6291
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
6292
|
-
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);
|
|
6293
6408
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6294
6409
|
fromIdx = start;
|
|
6410
|
+
const effectiveTotal = page?.total ?? total;
|
|
6295
6411
|
messagePagination = {
|
|
6296
|
-
total:
|
|
6412
|
+
total: effectiveTotal,
|
|
6297
6413
|
before_index: beforeIndex,
|
|
6298
6414
|
from_index: start,
|
|
6299
6415
|
has_more_older: start > 0,
|
|
6300
6416
|
next_before_index: start > 0 ? start : null
|
|
6301
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
|
+
}
|
|
6302
6423
|
}
|
|
6303
6424
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6304
6425
|
const content = [];
|
|
@@ -6373,6 +6494,65 @@ var StreamerServer = class {
|
|
|
6373
6494
|
});
|
|
6374
6495
|
res.end(JSON.stringify(body));
|
|
6375
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
|
+
}
|
|
6376
6556
|
async handleSearch(url, res) {
|
|
6377
6557
|
const q = url.searchParams.get("q") ?? "";
|
|
6378
6558
|
if (!q) {
|
|
@@ -6392,7 +6572,11 @@ var StreamerServer = class {
|
|
|
6392
6572
|
scanner
|
|
6393
6573
|
);
|
|
6394
6574
|
const adapted = results.map((r) => ({
|
|
6395
|
-
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,
|
|
6396
6580
|
title: r.meta.projectName,
|
|
6397
6581
|
sessionName: r.meta.sessionName || void 0,
|
|
6398
6582
|
filePath: r.meta.filePath,
|