@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/cli.cjs
CHANGED
|
@@ -1530,6 +1530,15 @@ function loadPublicUrl() {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
return void 0;
|
|
1532
1532
|
}
|
|
1533
|
+
function loadBrowserCors() {
|
|
1534
|
+
try {
|
|
1535
|
+
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
1536
|
+
const match2 = content.match(/browser_cors:\s*(.+)/);
|
|
1537
|
+
if (match2?.[1]) return match2[1].trim();
|
|
1538
|
+
} catch {
|
|
1539
|
+
}
|
|
1540
|
+
return void 0;
|
|
1541
|
+
}
|
|
1533
1542
|
function loadCacheDir() {
|
|
1534
1543
|
try {
|
|
1535
1544
|
const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
|
|
@@ -135802,25 +135811,55 @@ var authMiddleware = (deps) => async (c, next) => {
|
|
|
135802
135811
|
};
|
|
135803
135812
|
|
|
135804
135813
|
// src/api/middleware/cors.middleware.ts
|
|
135805
|
-
var
|
|
135814
|
+
var DEFAULT_DEV_ORIGINS = [
|
|
135806
135815
|
"http://localhost:8081",
|
|
135807
135816
|
"http://localhost:19006",
|
|
135808
135817
|
"http://localhost:3000"
|
|
135809
|
-
]
|
|
135810
|
-
|
|
135811
|
-
|
|
135812
|
-
const
|
|
135813
|
-
|
|
135814
|
-
|
|
135815
|
-
|
|
135816
|
-
|
|
135817
|
-
|
|
135818
|
-
|
|
135819
|
-
|
|
135820
|
-
|
|
135821
|
-
|
|
135822
|
-
|
|
135823
|
-
|
|
135818
|
+
];
|
|
135819
|
+
function resolveAllowedOrigins(raw2) {
|
|
135820
|
+
if (!raw2) return null;
|
|
135821
|
+
const trimmed = raw2.trim();
|
|
135822
|
+
const lower = trimmed.toLowerCase();
|
|
135823
|
+
if (lower === "0" || lower === "false" || lower === "no" || lower === "off" || trimmed === "") {
|
|
135824
|
+
return null;
|
|
135825
|
+
}
|
|
135826
|
+
const origins = new Set(DEFAULT_DEV_ORIGINS);
|
|
135827
|
+
if (!["1", "true", "yes", "on"].includes(lower)) {
|
|
135828
|
+
for (const o of trimmed.split(",")) {
|
|
135829
|
+
const origin = o.trim();
|
|
135830
|
+
if (origin) origins.add(origin);
|
|
135831
|
+
}
|
|
135832
|
+
}
|
|
135833
|
+
return origins;
|
|
135834
|
+
}
|
|
135835
|
+
var corsMiddleware = (configValue) => {
|
|
135836
|
+
const allowedOrigins = resolveAllowedOrigins(
|
|
135837
|
+
process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue
|
|
135838
|
+
);
|
|
135839
|
+
return async (c, next) => {
|
|
135840
|
+
const origin = c.req.header("origin");
|
|
135841
|
+
const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;
|
|
135842
|
+
if (allowedOrigin) {
|
|
135843
|
+
const raw2 = c.env.outgoing;
|
|
135844
|
+
raw2.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
135845
|
+
raw2.setHeader("Vary", "Origin");
|
|
135846
|
+
raw2.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
135847
|
+
raw2.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
|
|
135848
|
+
raw2.setHeader("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
135849
|
+
c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
135850
|
+
c.res.headers.set("Vary", "Origin");
|
|
135851
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, QUERY, OPTIONS");
|
|
135852
|
+
c.res.headers.set(
|
|
135853
|
+
"Access-Control-Allow-Headers",
|
|
135854
|
+
"Authorization, Content-Type, If-None-Match"
|
|
135855
|
+
);
|
|
135856
|
+
c.res.headers.set("Access-Control-Expose-Headers", "ETag, Accept-Query");
|
|
135857
|
+
}
|
|
135858
|
+
if (c.req.method === "OPTIONS") {
|
|
135859
|
+
return c.newResponse(null, allowedOrigin ? 204 : 403);
|
|
135860
|
+
}
|
|
135861
|
+
await next();
|
|
135862
|
+
};
|
|
135824
135863
|
};
|
|
135825
135864
|
|
|
135826
135865
|
// src/api/middleware/error.middleware.ts
|
|
@@ -135856,6 +135895,11 @@ var createConversationRoutes = (deps) => {
|
|
|
135856
135895
|
await deps.handleConversationsCount(url2, c.env.outgoing);
|
|
135857
135896
|
return alreadyHandled2();
|
|
135858
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
|
+
});
|
|
135859
135903
|
app.get("/:id{.+}", async (c) => {
|
|
135860
135904
|
const id = c.req.param("id");
|
|
135861
135905
|
const url2 = new URL(c.req.url);
|
|
@@ -136317,7 +136361,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
136317
136361
|
event: "http.request"
|
|
136318
136362
|
});
|
|
136319
136363
|
});
|
|
136320
|
-
app.use("*", corsMiddleware());
|
|
136364
|
+
app.use("*", corsMiddleware(deps.browserCors));
|
|
136321
136365
|
app.use("*", authMiddleware(deps));
|
|
136322
136366
|
app.onError(errorMiddleware);
|
|
136323
136367
|
app.route("/healthz", createHealthRoutes());
|
|
@@ -141311,6 +141355,55 @@ var ConversationWatcher = class {
|
|
|
141311
141355
|
}
|
|
141312
141356
|
};
|
|
141313
141357
|
|
|
141358
|
+
// src/services/conversations/findSearchTarget.ts
|
|
141359
|
+
var SNIPPET_CONTEXT = 60;
|
|
141360
|
+
var MAX_MATCH_INDEXES = 1e3;
|
|
141361
|
+
function buildSnippet(source, matchStart, matchLength) {
|
|
141362
|
+
const start = Math.max(0, matchStart - SNIPPET_CONTEXT);
|
|
141363
|
+
const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);
|
|
141364
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
141365
|
+
const suffix = end < source.length ? "\u2026" : "";
|
|
141366
|
+
return `${prefix}${source.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
141367
|
+
}
|
|
141368
|
+
function extendedBody(m2) {
|
|
141369
|
+
const parts = [];
|
|
141370
|
+
if (m2.isThinking && m2.thinkingContent) parts.push(m2.thinkingContent);
|
|
141371
|
+
for (const b2 of m2.metadata?.toolUseBlocks ?? []) {
|
|
141372
|
+
if (b2.input !== void 0) parts.push(JSON.stringify(b2.input));
|
|
141373
|
+
}
|
|
141374
|
+
for (const r of m2.metadata?.toolResults ?? []) {
|
|
141375
|
+
if (r.content !== void 0) parts.push(JSON.stringify(r.content));
|
|
141376
|
+
}
|
|
141377
|
+
return parts.join("\n");
|
|
141378
|
+
}
|
|
141379
|
+
function collectMatches(messages, needle, body) {
|
|
141380
|
+
const indexes = [];
|
|
141381
|
+
for (let i = 0; i < messages.length; i++) {
|
|
141382
|
+
if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);
|
|
141383
|
+
}
|
|
141384
|
+
return indexes;
|
|
141385
|
+
}
|
|
141386
|
+
function findSearchTarget(messages, query) {
|
|
141387
|
+
const needle = query.toLowerCase();
|
|
141388
|
+
let body = (m2) => m2.text ?? "";
|
|
141389
|
+
let matches = collectMatches(messages, needle, body);
|
|
141390
|
+
if (matches.length === 0) {
|
|
141391
|
+
body = extendedBody;
|
|
141392
|
+
matches = collectMatches(messages, needle, body);
|
|
141393
|
+
}
|
|
141394
|
+
if (matches.length === 0) return null;
|
|
141395
|
+
const anchorIndex = matches[matches.length - 1];
|
|
141396
|
+
const anchorBody = body(messages[anchorIndex]);
|
|
141397
|
+
const at2 = anchorBody.toLowerCase().indexOf(needle);
|
|
141398
|
+
return {
|
|
141399
|
+
messageIndex: anchorIndex,
|
|
141400
|
+
uuid: messages[anchorIndex].uuid ?? null,
|
|
141401
|
+
snippet: buildSnippet(anchorBody, at2, query.length),
|
|
141402
|
+
matchIndexes: matches.slice(-MAX_MATCH_INDEXES),
|
|
141403
|
+
totalMatches: matches.length
|
|
141404
|
+
};
|
|
141405
|
+
}
|
|
141406
|
+
|
|
141314
141407
|
// src/services/conversations/pruneAgentConversations.ts
|
|
141315
141408
|
var import_fs23 = require("fs");
|
|
141316
141409
|
function pruneAgentConversations(cache) {
|
|
@@ -142164,6 +142257,7 @@ var StreamerServer = class {
|
|
|
142164
142257
|
disableDb = false;
|
|
142165
142258
|
browseRoot = null;
|
|
142166
142259
|
publicUrl = null;
|
|
142260
|
+
browserCors;
|
|
142167
142261
|
pairTokens = new PairTokenStore();
|
|
142168
142262
|
exchangeAttempts = /* @__PURE__ */ new Map();
|
|
142169
142263
|
sessionStartAttempts = /* @__PURE__ */ new Map();
|
|
@@ -142249,6 +142343,7 @@ var StreamerServer = class {
|
|
|
142249
142343
|
this.log.warn(`Warning: ${result.error}`, { error: result.error });
|
|
142250
142344
|
}
|
|
142251
142345
|
}
|
|
142346
|
+
this.browserCors = config2.browserCors ?? loadBrowserCors();
|
|
142252
142347
|
this.sessionStore = new SessionStore();
|
|
142253
142348
|
this.wsHub = new WSHub();
|
|
142254
142349
|
this.fileWatcher = new ConversationWatcher({
|
|
@@ -142403,6 +142498,7 @@ var StreamerServer = class {
|
|
|
142403
142498
|
rotateApiKey: () => this.rotateApiKey(),
|
|
142404
142499
|
publicUrl: this.publicUrl,
|
|
142405
142500
|
browseRoot: this.browseRoot,
|
|
142501
|
+
browserCors: this.browserCors,
|
|
142406
142502
|
ptyManager: this.ptyManager,
|
|
142407
142503
|
sessionStore: this.sessionStore,
|
|
142408
142504
|
wsHub: this.wsHub,
|
|
@@ -142431,6 +142527,7 @@ var StreamerServer = class {
|
|
|
142431
142527
|
handleConversationsCount: (url2, res) => this.handleConversationsCount(url2, res),
|
|
142432
142528
|
handleGetConversation: (id, url2, res, ifNoneMatch) => this.handleGetConversation(id, url2, res, ifNoneMatch),
|
|
142433
142529
|
handleSearch: (url2, res) => this.handleSearch(url2, res),
|
|
142530
|
+
handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),
|
|
142434
142531
|
handleListProjects: (url2, res) => handleListProjects(url2, res),
|
|
142435
142532
|
handleGetPopularProjects: (url2, res) => this.handleGetPopularProjects(url2, res),
|
|
142436
142533
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -143331,7 +143428,7 @@ var StreamerServer = class {
|
|
|
143331
143428
|
messageCount: etagSource.messageCount,
|
|
143332
143429
|
timestamp: etagSource.timestamp
|
|
143333
143430
|
});
|
|
143334
|
-
const isFirstPage = !url2.searchParams.has("before_index");
|
|
143431
|
+
const isFirstPage = !url2.searchParams.has("before_index") && !url2.searchParams.has("anchor_index") && !url2.searchParams.has("after_index");
|
|
143335
143432
|
if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {
|
|
143336
143433
|
res.writeHead(304, { ETag: etag, "Access-Control-Expose-Headers": "ETag" });
|
|
143337
143434
|
res.end();
|
|
@@ -143339,29 +143436,53 @@ var StreamerServer = class {
|
|
|
143339
143436
|
}
|
|
143340
143437
|
const filtered = conversation.messages;
|
|
143341
143438
|
const total = filtered.length;
|
|
143342
|
-
const
|
|
143439
|
+
const hasAnchor = url2.searchParams.has("anchor_index");
|
|
143440
|
+
const hasAfter = url2.searchParams.has("after_index");
|
|
143441
|
+
const usePaging = url2.searchParams.has("msg_limit") || url2.searchParams.has("before_index") || hasAnchor || hasAfter;
|
|
143343
143442
|
let slice = filtered;
|
|
143344
143443
|
let fromIdx = 0;
|
|
143345
143444
|
let messagePagination;
|
|
143346
143445
|
if (usePaging) {
|
|
143347
143446
|
const limit = Math.min(Math.max(intParam(url2, "msg_limit", 80), 1), 500);
|
|
143348
143447
|
let beforeIndex = total;
|
|
143448
|
+
let scanLimit = limit;
|
|
143449
|
+
let anchorIndex = null;
|
|
143450
|
+
let newerPaging = false;
|
|
143349
143451
|
if (url2.searchParams.has("before_index")) {
|
|
143350
143452
|
beforeIndex = intParam(url2, "before_index", total);
|
|
143351
143453
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
143454
|
+
} else if (hasAfter) {
|
|
143455
|
+
const from = Math.min(Math.max(intParam(url2, "after_index", 0), 0), total);
|
|
143456
|
+
beforeIndex = Math.min(total, from + limit);
|
|
143457
|
+
scanLimit = beforeIndex - from;
|
|
143458
|
+
newerPaging = true;
|
|
143459
|
+
} else if (hasAnchor) {
|
|
143460
|
+
anchorIndex = Math.min(
|
|
143461
|
+
Math.max(intParam(url2, "anchor_index", 0), 0),
|
|
143462
|
+
Math.max(0, total - 1)
|
|
143463
|
+
);
|
|
143464
|
+
const from = Math.max(0, anchorIndex - Math.floor(limit / 2));
|
|
143465
|
+
beforeIndex = Math.min(total, from + limit);
|
|
143466
|
+
newerPaging = true;
|
|
143352
143467
|
}
|
|
143353
143468
|
const pagedScanner = this.scannerReady ? await this.getScanner(true) : null;
|
|
143354
|
-
const page = pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit }) : null;
|
|
143355
|
-
const start = page?.fromIndex ?? Math.max(0, beforeIndex -
|
|
143469
|
+
const page = scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null;
|
|
143470
|
+
const start = page?.fromIndex ?? Math.max(0, beforeIndex - scanLimit);
|
|
143356
143471
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
143357
143472
|
fromIdx = start;
|
|
143473
|
+
const effectiveTotal = page?.total ?? total;
|
|
143358
143474
|
messagePagination = {
|
|
143359
|
-
total:
|
|
143475
|
+
total: effectiveTotal,
|
|
143360
143476
|
before_index: beforeIndex,
|
|
143361
143477
|
from_index: start,
|
|
143362
143478
|
has_more_older: start > 0,
|
|
143363
143479
|
next_before_index: start > 0 ? start : null
|
|
143364
143480
|
};
|
|
143481
|
+
if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;
|
|
143482
|
+
if (newerPaging) {
|
|
143483
|
+
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
143484
|
+
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
143485
|
+
}
|
|
143365
143486
|
}
|
|
143366
143487
|
const messagesPayload = slice.map((m2, localIdx) => {
|
|
143367
143488
|
const content = [];
|
|
@@ -143436,6 +143557,65 @@ var StreamerServer = class {
|
|
|
143436
143557
|
});
|
|
143437
143558
|
res.end(JSON.stringify(body));
|
|
143438
143559
|
}
|
|
143560
|
+
// Resolves an active search query to the message a client should anchor to
|
|
143561
|
+
// inside one conversation. Matching is body-only (text first, then
|
|
143562
|
+
// thinking/tool payloads) — a metadata-only search hit (project path, title)
|
|
143563
|
+
// has no scroll target and returns 404 search_target_not_found.
|
|
143564
|
+
//
|
|
143565
|
+
// Implements HTTP QUERY (RFC 10008): the search query travels in a JSON
|
|
143566
|
+
// request body instead of a URL query param — QUERY is safe + idempotent +
|
|
143567
|
+
// cacheable like GET, but (like POST) can carry a body, which fits this
|
|
143568
|
+
// endpoint's single-string input exactly. `Accept-Query` advertises the
|
|
143569
|
+
// supported request media type per the spec.
|
|
143570
|
+
async handleSearchTarget(id, req, res) {
|
|
143571
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0].trim();
|
|
143572
|
+
if (contentType && contentType !== "application/json") {
|
|
143573
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143574
|
+
json2(res, 415, {
|
|
143575
|
+
error: "Unsupported Content-Type; expected application/json",
|
|
143576
|
+
code: "unsupported_media_type"
|
|
143577
|
+
});
|
|
143578
|
+
return;
|
|
143579
|
+
}
|
|
143580
|
+
let body;
|
|
143581
|
+
try {
|
|
143582
|
+
body = await readBody(req);
|
|
143583
|
+
} catch {
|
|
143584
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143585
|
+
json2(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
|
|
143586
|
+
return;
|
|
143587
|
+
}
|
|
143588
|
+
const q2 = typeof body?.q === "string" ? body.q.trim() : "";
|
|
143589
|
+
if (!q2) {
|
|
143590
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143591
|
+
json2(res, 422, { error: "Missing or empty query field: q", code: "invalid_query" });
|
|
143592
|
+
return;
|
|
143593
|
+
}
|
|
143594
|
+
if (q2.length > 256) {
|
|
143595
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143596
|
+
json2(res, 422, { error: "Query too long (max 256 characters)", code: "invalid_query" });
|
|
143597
|
+
return;
|
|
143598
|
+
}
|
|
143599
|
+
const conversation = await this.findConversationByUuid(id);
|
|
143600
|
+
if (!conversation) {
|
|
143601
|
+
json2(res, 404, { error: "Conversation not found", code: "not_found" });
|
|
143602
|
+
return;
|
|
143603
|
+
}
|
|
143604
|
+
const target = findSearchTarget(conversation.messages, q2);
|
|
143605
|
+
if (!target) {
|
|
143606
|
+
json2(res, 404, { error: "No message body matches query", code: "search_target_not_found" });
|
|
143607
|
+
return;
|
|
143608
|
+
}
|
|
143609
|
+
res.setHeader("Accept-Query", "application/json");
|
|
143610
|
+
json2(res, 200, {
|
|
143611
|
+
query: q2,
|
|
143612
|
+
message_index: target.messageIndex,
|
|
143613
|
+
uuid: target.uuid,
|
|
143614
|
+
snippet: target.snippet,
|
|
143615
|
+
match_indexes: target.matchIndexes,
|
|
143616
|
+
total_matches: target.totalMatches
|
|
143617
|
+
});
|
|
143618
|
+
}
|
|
143439
143619
|
async handleSearch(url2, res) {
|
|
143440
143620
|
const q2 = url2.searchParams.get("q") ?? "";
|
|
143441
143621
|
if (!q2) {
|
|
@@ -143455,7 +143635,11 @@ var StreamerServer = class {
|
|
|
143455
143635
|
scanner
|
|
143456
143636
|
);
|
|
143457
143637
|
const adapted = results.map((r) => ({
|
|
143458
|
-
id
|
|
143638
|
+
// Use sessionId so the id matches /api/conversations and resolves via
|
|
143639
|
+
// findConversationByUuid — a client can round-trip a search result into
|
|
143640
|
+
// GET /api/conversations/:id or the search-target QUERY. The old
|
|
143641
|
+
// filename-stem derivation produced an id no other endpoint recognized.
|
|
143642
|
+
id: r.meta.sessionId || r.meta.id,
|
|
143459
143643
|
title: r.meta.projectName,
|
|
143460
143644
|
sessionName: r.meta.sessionName || void 0,
|
|
143461
143645
|
filePath: r.meta.filePath,
|