@threadbase-sh/streamer 1.27.3 → 1.28.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 +495 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +464 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -1
- package/dist/index.d.ts +117 -1
- package/dist/index.js +445 -11
- package/dist/index.js.map +1 -1
- package/dist/migrations/009_create_offset_index.sql +28 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2317,10 +2317,10 @@ async function readGitBranch(dir) {
|
|
|
2317
2317
|
// src/server.ts
|
|
2318
2318
|
var import_node_ws = require("@hono/node-ws");
|
|
2319
2319
|
var import_client = require("@temporalio/client");
|
|
2320
|
-
var
|
|
2320
|
+
var import_scanner3 = require("@threadbase-sh/scanner");
|
|
2321
2321
|
var import_events = require("events");
|
|
2322
2322
|
var import_fs12 = require("fs");
|
|
2323
|
-
var
|
|
2323
|
+
var import_promises7 = require("fs/promises");
|
|
2324
2324
|
var import_http = require("http");
|
|
2325
2325
|
var import_os6 = require("os");
|
|
2326
2326
|
var import_path13 = require("path");
|
|
@@ -3162,9 +3162,12 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
3162
3162
|
}
|
|
3163
3163
|
|
|
3164
3164
|
// src/conversation-cache.ts
|
|
3165
|
+
var import_scanner2 = require("@threadbase-sh/scanner");
|
|
3165
3166
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
3166
3167
|
var import_fs8 = require("fs");
|
|
3168
|
+
var import_promises3 = require("fs/promises");
|
|
3167
3169
|
var import_path10 = require("path");
|
|
3170
|
+
var import_promises4 = require("timers/promises");
|
|
3168
3171
|
|
|
3169
3172
|
// src/db/sqlite-migrate.ts
|
|
3170
3173
|
var import_fs6 = require("fs");
|
|
@@ -3277,6 +3280,33 @@ function parseAgentEntrypointsEnv(raw) {
|
|
|
3277
3280
|
return new Set(parts);
|
|
3278
3281
|
}
|
|
3279
3282
|
|
|
3283
|
+
// src/utils/fileIdentity.ts
|
|
3284
|
+
var import_crypto5 = require("crypto");
|
|
3285
|
+
function fileIdentity(stat3, headBytes) {
|
|
3286
|
+
if (stat3.ino && stat3.ino > 0) return `inode:${stat3.dev}:${stat3.ino}`;
|
|
3287
|
+
const head = headBytes ?? Buffer.alloc(0);
|
|
3288
|
+
return `fp:${(0, import_crypto5.createHash)("sha1").update(head).digest("hex")}`;
|
|
3289
|
+
}
|
|
3290
|
+
function splitCompleteLines(buf, baseOffset) {
|
|
3291
|
+
const spans = [];
|
|
3292
|
+
let lineStart = 0;
|
|
3293
|
+
let consumed = 0;
|
|
3294
|
+
for (let i = 0; i < buf.length; i++) {
|
|
3295
|
+
if (buf[i] !== 10) continue;
|
|
3296
|
+
const lineLen = i - lineStart;
|
|
3297
|
+
if (lineLen > 0) {
|
|
3298
|
+
spans.push({
|
|
3299
|
+
byteOffset: baseOffset + lineStart,
|
|
3300
|
+
byteLength: lineLen,
|
|
3301
|
+
text: buf.toString("utf-8", lineStart, i)
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
lineStart = i + 1;
|
|
3305
|
+
consumed = lineStart;
|
|
3306
|
+
}
|
|
3307
|
+
return { spans, consumed };
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3280
3310
|
// src/conversation-cache.ts
|
|
3281
3311
|
function shortProjectName(fullPath) {
|
|
3282
3312
|
const parts = fullPath.split(/[/\\]/).filter(Boolean);
|
|
@@ -3325,6 +3355,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
3325
3355
|
tailSize;
|
|
3326
3356
|
fileIndex = /* @__PURE__ */ new Map();
|
|
3327
3357
|
fileIndexLoaded = false;
|
|
3358
|
+
// Per-file scanner parse state for the incremental offset-index writer. The
|
|
3359
|
+
// reducer is stateful across lines (pending tool_uses, latest timestamp), so
|
|
3360
|
+
// it must persist between watcher reads of the same file. Cleared on
|
|
3361
|
+
// truncation/backfill.
|
|
3362
|
+
indexParseState = /* @__PURE__ */ new Map();
|
|
3363
|
+
// Single-flight guard for backfillIndex — concurrent detail requests for the
|
|
3364
|
+
// same cold file await one walk, not N. Entry dropped on settle.
|
|
3365
|
+
backfillInFlight = /* @__PURE__ */ new Map();
|
|
3328
3366
|
// Monotonically increasing counter for tail updated_at — guarantees strict
|
|
3329
3367
|
// ordering even when multiple updateFromLine() calls land within the same ms.
|
|
3330
3368
|
tailSeq = Date.now();
|
|
@@ -3488,6 +3526,42 @@ var ConversationCache = class _ConversationCache {
|
|
|
3488
3526
|
GROUP BY project_path
|
|
3489
3527
|
ORDER BY cnt DESC
|
|
3490
3528
|
LIMIT ?`
|
|
3529
|
+
),
|
|
3530
|
+
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
3531
|
+
upsertFileState: db.prepare(
|
|
3532
|
+
`INSERT INTO conversation_file_state
|
|
3533
|
+
(path, identity, size, mtime_ms, byte_offset, last_message_index)
|
|
3534
|
+
VALUES (@path, @identity, @size, @mtime_ms, @byte_offset, @last_message_index)
|
|
3535
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
3536
|
+
identity = excluded.identity,
|
|
3537
|
+
size = excluded.size,
|
|
3538
|
+
mtime_ms = excluded.mtime_ms,
|
|
3539
|
+
byte_offset = excluded.byte_offset,
|
|
3540
|
+
last_message_index = excluded.last_message_index`
|
|
3541
|
+
),
|
|
3542
|
+
deleteFileState: db.prepare("DELETE FROM conversation_file_state WHERE path = ?"),
|
|
3543
|
+
deleteMessageIndex: db.prepare(
|
|
3544
|
+
"DELETE FROM conversation_message_index WHERE conversation_id = ?"
|
|
3545
|
+
),
|
|
3546
|
+
insertMessageIndexRow: db.prepare(
|
|
3547
|
+
`INSERT INTO conversation_message_index
|
|
3548
|
+
(conversation_id, message_index, byte_offset, byte_length, uuid, role, ts)
|
|
3549
|
+
VALUES (@conversation_id, @message_index, @byte_offset, @byte_length, @uuid, @role, @ts)
|
|
3550
|
+
ON CONFLICT(conversation_id, message_index) DO UPDATE SET
|
|
3551
|
+
byte_offset = excluded.byte_offset,
|
|
3552
|
+
byte_length = excluded.byte_length,
|
|
3553
|
+
uuid = excluded.uuid,
|
|
3554
|
+
role = excluded.role,
|
|
3555
|
+
ts = excluded.ts`
|
|
3556
|
+
),
|
|
3557
|
+
getMessageIndexWindow: db.prepare(
|
|
3558
|
+
`SELECT message_index, byte_offset, byte_length, uuid, role, ts
|
|
3559
|
+
FROM conversation_message_index
|
|
3560
|
+
WHERE conversation_id = ? AND message_index >= ? AND message_index < ?
|
|
3561
|
+
ORDER BY message_index ASC`
|
|
3562
|
+
),
|
|
3563
|
+
getIndexedMessageCount: db.prepare(
|
|
3564
|
+
"SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?"
|
|
3491
3565
|
)
|
|
3492
3566
|
};
|
|
3493
3567
|
}
|
|
@@ -3498,6 +3572,273 @@ var ConversationCache = class _ConversationCache {
|
|
|
3498
3572
|
getDatabase() {
|
|
3499
3573
|
return this.db;
|
|
3500
3574
|
}
|
|
3575
|
+
// ── Offset index (design 1b) ────────────────────────────────────────────
|
|
3576
|
+
// conversation_file_state + conversation_message_index back the windowed
|
|
3577
|
+
// detail read path (SQL window select + pread of byte ranges). All methods
|
|
3578
|
+
// are thin wrappers over the prepared statements above.
|
|
3579
|
+
getFileState(path) {
|
|
3580
|
+
return this.stmts.getFileState.get(path) ?? null;
|
|
3581
|
+
}
|
|
3582
|
+
upsertFileState(row) {
|
|
3583
|
+
this.stmts.upsertFileState.run(row);
|
|
3584
|
+
}
|
|
3585
|
+
/** Drop a file's index rows + file_state (truncation / identity change). */
|
|
3586
|
+
deleteFileIndex(path, conversationId) {
|
|
3587
|
+
const tx = this.db.transaction(() => {
|
|
3588
|
+
this.stmts.deleteMessageIndex.run(conversationId);
|
|
3589
|
+
this.stmts.deleteFileState.run(path);
|
|
3590
|
+
});
|
|
3591
|
+
tx();
|
|
3592
|
+
this.indexParseState.delete(path);
|
|
3593
|
+
}
|
|
3594
|
+
/** Append/replace index rows in one transaction. */
|
|
3595
|
+
appendMessageIndexRows(rows) {
|
|
3596
|
+
const tx = this.db.transaction((batch) => {
|
|
3597
|
+
for (const r of batch) this.stmts.insertMessageIndexRow.run(r);
|
|
3598
|
+
});
|
|
3599
|
+
tx(rows);
|
|
3600
|
+
}
|
|
3601
|
+
/** Rows for message_index in [fromIndex, toIndex), ordered ascending. */
|
|
3602
|
+
getMessageIndexWindow(conversationId, fromIndex, toIndex) {
|
|
3603
|
+
return this.stmts.getMessageIndexWindow.all(
|
|
3604
|
+
conversationId,
|
|
3605
|
+
fromIndex,
|
|
3606
|
+
toIndex
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
getIndexedMessageCount(conversationId) {
|
|
3610
|
+
return this.stmts.getIndexedMessageCount.get(conversationId).cnt;
|
|
3611
|
+
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Conversation id for a JSONL path — the filename stem (matches the pseudo-id
|
|
3614
|
+
* updateFromLine derives and the uuid the detail read path resolves). The
|
|
3615
|
+
* offset index keys on this so the window select and the cursor agree.
|
|
3616
|
+
*/
|
|
3617
|
+
static conversationIdForFile(filePath) {
|
|
3618
|
+
return filePath.split(/[/\\]/).pop()?.replace(/\.jsonl$/, "") ?? filePath;
|
|
3619
|
+
}
|
|
3620
|
+
// The offset index understands only claude-code JSONL: parseJsonlLine is the
|
|
3621
|
+
// claude-code reducer, so a codex file "indexes" as zero messages and the
|
|
3622
|
+
// resulting file_state serves empty windows for a real conversation (the
|
|
3623
|
+
// silent-wrong-data bug hotfixed after 1.28.0). Non-claude providers are
|
|
3624
|
+
// excluded from the index entirely; the scanner serves them (it routes each
|
|
3625
|
+
// provider to its own parser).
|
|
3626
|
+
isIndexableFile(filePath) {
|
|
3627
|
+
const meta = this.getMetaById(_ConversationCache.conversationIdForFile(filePath));
|
|
3628
|
+
return (meta?.provider ?? CLAUDE_CODE_PROVIDER) === CLAUDE_CODE_PROVIDER;
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Incremental offset-index writer: extend the index for a burst of appended
|
|
3632
|
+
* lines (one watcher read) using their byte spans. Each line is classified
|
|
3633
|
+
* with the scanner's parseJsonlLine (a running per-file reducer state), so the
|
|
3634
|
+
* message ordering can never drift from the scanner's. Message lines get an
|
|
3635
|
+
* index row at the next message_index; non-message lines (summary/sidecar)
|
|
3636
|
+
* get no row but still advance byte_offset. file_state is updated to the end
|
|
3637
|
+
* of the last consumed span.
|
|
3638
|
+
*
|
|
3639
|
+
* Requires an up-to-date `stat` (identity/size/mtime) for the file so the read
|
|
3640
|
+
* path can detect truncation/replacement.
|
|
3641
|
+
*
|
|
3642
|
+
* `readFrom` is the absolute byte offset the watcher read started at, and
|
|
3643
|
+
* `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new
|
|
3644
|
+
* entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index
|
|
3645
|
+
* left off (`readFrom === existing.byte_offset`, or 0 with no state). If it
|
|
3646
|
+
* doesn't — the watcher attached at EOF after the server was down, or an
|
|
3647
|
+
* append raced an in-flight backfill — extending would assign wrong
|
|
3648
|
+
* message_index values over a hole. In that case this writes nothing and
|
|
3649
|
+
* returns null so the caller drops the index and backfills.
|
|
3650
|
+
*
|
|
3651
|
+
* On success returns the message_index assigned to each input span (null for a
|
|
3652
|
+
* non-message line) so the caller can stamp WS `seq`. Empty array when spans
|
|
3653
|
+
* is empty. `endOffset` is stored verbatim as byte_offset so the watcher's
|
|
3654
|
+
* offset and file_state.byte_offset are the same number by construction.
|
|
3655
|
+
*/
|
|
3656
|
+
extendMessageIndex(filePath, spans, stat3, readFrom, endOffset) {
|
|
3657
|
+
if (!this.isIndexableFile(filePath)) return spans.map(() => null);
|
|
3658
|
+
const existing = this.getFileState(filePath);
|
|
3659
|
+
const expectedStart = existing?.byte_offset ?? 0;
|
|
3660
|
+
if (readFrom !== expectedStart) return null;
|
|
3661
|
+
if (spans.length === 0) return [];
|
|
3662
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
3663
|
+
let state = this.indexParseState.get(filePath);
|
|
3664
|
+
if (!state) {
|
|
3665
|
+
state = (0, import_scanner2.createJsonlParseState)();
|
|
3666
|
+
this.indexParseState.set(filePath, state);
|
|
3667
|
+
}
|
|
3668
|
+
let nextIndex = existing ? existing.last_message_index + 1 : 0;
|
|
3669
|
+
const rows = [];
|
|
3670
|
+
const seqs = [];
|
|
3671
|
+
for (const span of spans) {
|
|
3672
|
+
const msg = (0, import_scanner2.parseJsonlLine)(span.text, state);
|
|
3673
|
+
if (!msg) {
|
|
3674
|
+
seqs.push(null);
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
rows.push({
|
|
3678
|
+
conversation_id: convId,
|
|
3679
|
+
message_index: nextIndex,
|
|
3680
|
+
byte_offset: span.byteOffset,
|
|
3681
|
+
byte_length: span.byteLength,
|
|
3682
|
+
uuid: msg.uuid ?? null,
|
|
3683
|
+
role: msg.role ?? null,
|
|
3684
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
3685
|
+
});
|
|
3686
|
+
seqs.push(nextIndex);
|
|
3687
|
+
nextIndex++;
|
|
3688
|
+
}
|
|
3689
|
+
const tx = this.db.transaction(() => {
|
|
3690
|
+
for (const r of rows) this.stmts.insertMessageIndexRow.run(r);
|
|
3691
|
+
this.stmts.upsertFileState.run({
|
|
3692
|
+
path: filePath,
|
|
3693
|
+
identity: fileIdentity(stat3),
|
|
3694
|
+
size: stat3.size,
|
|
3695
|
+
mtime_ms: Math.round(stat3.mtimeMs),
|
|
3696
|
+
// Store the watcher's end offset verbatim — same number as entry.offset,
|
|
3697
|
+
// so the next read's contiguity check compares like-for-like (never
|
|
3698
|
+
// false-positive on a read that ended in trailing empty lines).
|
|
3699
|
+
byte_offset: endOffset,
|
|
3700
|
+
last_message_index: nextIndex - 1
|
|
3701
|
+
});
|
|
3702
|
+
});
|
|
3703
|
+
tx();
|
|
3704
|
+
return seqs;
|
|
3705
|
+
}
|
|
3706
|
+
clearIndexParseState(filePath) {
|
|
3707
|
+
this.indexParseState.delete(filePath);
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* On-demand full backfill of the offset index for a file with no/stale
|
|
3711
|
+
* file_state (cold conversation, or after a truncation/replacement). Rebuilds
|
|
3712
|
+
* from byte 0: drops any existing rows, walks the whole file in chunks with a
|
|
3713
|
+
* running parse state, yields to the event loop every ~1000 lines so a large
|
|
3714
|
+
* file never blocks, and writes index rows + file_state.
|
|
3715
|
+
*
|
|
3716
|
+
* Single-flighted per path: concurrent callers await the same walk. The
|
|
3717
|
+
* triggering detail request is served by the scanner fallback while this runs.
|
|
3718
|
+
*/
|
|
3719
|
+
backfillIndex(filePath) {
|
|
3720
|
+
const inFlight = this.backfillInFlight.get(filePath);
|
|
3721
|
+
if (inFlight) return inFlight;
|
|
3722
|
+
const walk = this.runBackfill(filePath).finally(() => {
|
|
3723
|
+
this.backfillInFlight.delete(filePath);
|
|
3724
|
+
});
|
|
3725
|
+
this.backfillInFlight.set(filePath, walk);
|
|
3726
|
+
return walk;
|
|
3727
|
+
}
|
|
3728
|
+
async runBackfill(filePath) {
|
|
3729
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
3730
|
+
this.deleteFileIndex(filePath, convId);
|
|
3731
|
+
this.indexParseState.delete(filePath);
|
|
3732
|
+
if (!this.isIndexableFile(filePath)) return;
|
|
3733
|
+
const CHUNK = 256 * 1024;
|
|
3734
|
+
const YIELD_EVERY = 1e3;
|
|
3735
|
+
const state = (0, import_scanner2.createJsonlParseState)();
|
|
3736
|
+
const fh = await (0, import_promises3.open)(filePath, "r");
|
|
3737
|
+
let fileOffset = 0;
|
|
3738
|
+
let carry = Buffer.alloc(0);
|
|
3739
|
+
let nextIndex = 0;
|
|
3740
|
+
let linesSinceYield = 0;
|
|
3741
|
+
let lastConsumedEnd = 0;
|
|
3742
|
+
let stat3;
|
|
3743
|
+
try {
|
|
3744
|
+
stat3 = await fh.stat();
|
|
3745
|
+
const buf = Buffer.alloc(CHUNK);
|
|
3746
|
+
for (; ; ) {
|
|
3747
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK, null);
|
|
3748
|
+
if (bytesRead === 0) break;
|
|
3749
|
+
const combined = carry.length > 0 ? Buffer.concat([carry, buf.subarray(0, bytesRead)]) : buf.subarray(0, bytesRead);
|
|
3750
|
+
const { spans, consumed } = splitCompleteLines(combined, fileOffset);
|
|
3751
|
+
const rows = [];
|
|
3752
|
+
for (const span of spans) {
|
|
3753
|
+
const msg = (0, import_scanner2.parseJsonlLine)(span.text, state);
|
|
3754
|
+
linesSinceYield++;
|
|
3755
|
+
if (msg) {
|
|
3756
|
+
rows.push({
|
|
3757
|
+
conversation_id: convId,
|
|
3758
|
+
message_index: nextIndex,
|
|
3759
|
+
byte_offset: span.byteOffset,
|
|
3760
|
+
byte_length: span.byteLength,
|
|
3761
|
+
uuid: msg.uuid ?? null,
|
|
3762
|
+
role: msg.role ?? null,
|
|
3763
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
3764
|
+
});
|
|
3765
|
+
nextIndex++;
|
|
3766
|
+
}
|
|
3767
|
+
if (linesSinceYield >= YIELD_EVERY) {
|
|
3768
|
+
linesSinceYield = 0;
|
|
3769
|
+
await (0, import_promises4.setImmediate)();
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if (rows.length > 0) this.appendMessageIndexRows(rows);
|
|
3773
|
+
lastConsumedEnd = fileOffset + consumed;
|
|
3774
|
+
carry = Buffer.from(combined.subarray(consumed));
|
|
3775
|
+
fileOffset += consumed;
|
|
3776
|
+
}
|
|
3777
|
+
} finally {
|
|
3778
|
+
await fh.close();
|
|
3779
|
+
}
|
|
3780
|
+
this.upsertFileState({
|
|
3781
|
+
path: filePath,
|
|
3782
|
+
identity: fileIdentity(stat3),
|
|
3783
|
+
size: stat3.size,
|
|
3784
|
+
mtime_ms: Math.round(stat3.mtimeMs),
|
|
3785
|
+
byte_offset: lastConsumedEnd,
|
|
3786
|
+
last_message_index: nextIndex - 1
|
|
3787
|
+
});
|
|
3788
|
+
this.indexParseState.set(filePath, state);
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Windowed detail read straight from the offset index — the hot path.
|
|
3792
|
+
* Returns the parsed messages for message_index in [fromIndex, toIndex) plus
|
|
3793
|
+
* the total indexed count, or null when the index can't serve this file (no
|
|
3794
|
+
* file_state, identity/size mismatch = truncation/replacement, or cold index)
|
|
3795
|
+
* so the caller falls back to the scanner and enqueues a backfill.
|
|
3796
|
+
*
|
|
3797
|
+
* On a match it SQL-selects the window's byte ranges and preads exactly those
|
|
3798
|
+
* ranges from the JSONL (never the whole file), parsing only the sliced lines.
|
|
3799
|
+
* Returns messages in the same ConversationMessage shape parseJsonlLine
|
|
3800
|
+
* produces during a scan, so the payload is identical to the scanner path.
|
|
3801
|
+
*/
|
|
3802
|
+
readMessageWindow(filePath, fromIndex, toIndex) {
|
|
3803
|
+
const fileState = this.getFileState(filePath);
|
|
3804
|
+
if (!fileState) return null;
|
|
3805
|
+
let stat3;
|
|
3806
|
+
try {
|
|
3807
|
+
stat3 = (0, import_fs8.statSync)(filePath);
|
|
3808
|
+
} catch {
|
|
3809
|
+
return null;
|
|
3810
|
+
}
|
|
3811
|
+
if (fileIdentity(stat3) !== fileState.identity || stat3.size !== fileState.byte_offset) {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
if (fileState.last_message_index < 0 || !this.isIndexableFile(filePath)) {
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
const total = fileState.last_message_index + 1;
|
|
3818
|
+
const from = Math.max(0, fromIndex);
|
|
3819
|
+
const to = Math.min(toIndex, total);
|
|
3820
|
+
if (to <= from) return { messages: [], total, fromIndex: from };
|
|
3821
|
+
const rows = this.getMessageIndexWindow(
|
|
3822
|
+
_ConversationCache.conversationIdForFile(filePath),
|
|
3823
|
+
from,
|
|
3824
|
+
to
|
|
3825
|
+
);
|
|
3826
|
+
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
3827
|
+
const messages = [];
|
|
3828
|
+
const fd = (0, import_fs8.openSync)(filePath, "r");
|
|
3829
|
+
try {
|
|
3830
|
+
const state = (0, import_scanner2.createJsonlParseState)();
|
|
3831
|
+
for (const row of rows) {
|
|
3832
|
+
const buf = Buffer.alloc(row.byte_length);
|
|
3833
|
+
(0, import_fs8.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
3834
|
+
const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
|
|
3835
|
+
if (msg) messages.push(msg);
|
|
3836
|
+
}
|
|
3837
|
+
} finally {
|
|
3838
|
+
(0, import_fs8.closeSync)(fd);
|
|
3839
|
+
}
|
|
3840
|
+
return { messages, total, fromIndex: from };
|
|
3841
|
+
}
|
|
3501
3842
|
agentEntrypointsKey() {
|
|
3502
3843
|
return [...this.agentEntrypoints].sort().join(",");
|
|
3503
3844
|
}
|
|
@@ -4131,7 +4472,7 @@ var ConversationsRepository = class {
|
|
|
4131
4472
|
};
|
|
4132
4473
|
|
|
4133
4474
|
// src/db/repositories/projects.repository.ts
|
|
4134
|
-
var
|
|
4475
|
+
var import_crypto6 = require("crypto");
|
|
4135
4476
|
|
|
4136
4477
|
// src/utils/canonicalizeProjectPath.ts
|
|
4137
4478
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -4224,7 +4565,7 @@ var ProjectsRepository = class {
|
|
|
4224
4565
|
});
|
|
4225
4566
|
return rowToProject(this.getById.get(existing.id));
|
|
4226
4567
|
}
|
|
4227
|
-
const id = (0,
|
|
4568
|
+
const id = (0, import_crypto6.randomUUID)();
|
|
4228
4569
|
this.insert.run({
|
|
4229
4570
|
id,
|
|
4230
4571
|
path,
|
|
@@ -4315,7 +4656,7 @@ function handleListProjects(url, res) {
|
|
|
4315
4656
|
}
|
|
4316
4657
|
|
|
4317
4658
|
// src/pair-store.ts
|
|
4318
|
-
var
|
|
4659
|
+
var import_crypto7 = require("crypto");
|
|
4319
4660
|
var DEFAULT_TTL_SECONDS = 180;
|
|
4320
4661
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
4321
4662
|
var PairTokenStore = class {
|
|
@@ -4330,7 +4671,7 @@ var PairTokenStore = class {
|
|
|
4330
4671
|
}
|
|
4331
4672
|
}
|
|
4332
4673
|
mint() {
|
|
4333
|
-
const token = `pt_${(0,
|
|
4674
|
+
const token = `pt_${(0, import_crypto7.randomBytes)(16).toString("hex")}`;
|
|
4334
4675
|
const expiresAt = Date.now() + this.ttlMs;
|
|
4335
4676
|
this.current = { token, expiresAt, used: false };
|
|
4336
4677
|
return {
|
|
@@ -4392,18 +4733,20 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
4392
4733
|
// src/services/conversations/conversationWatcher.ts
|
|
4393
4734
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
4394
4735
|
var import_fs10 = require("fs");
|
|
4395
|
-
var
|
|
4736
|
+
var import_promises5 = require("fs/promises");
|
|
4396
4737
|
var ConversationWatcher = class {
|
|
4397
4738
|
files = /* @__PURE__ */ new Map();
|
|
4398
4739
|
directories = /* @__PURE__ */ new Map();
|
|
4399
4740
|
onNewLine;
|
|
4400
4741
|
onNewLines;
|
|
4742
|
+
onNewLineSpans;
|
|
4401
4743
|
onConversationChanged;
|
|
4402
4744
|
onFileDeleted;
|
|
4403
4745
|
onError;
|
|
4404
4746
|
constructor(events = {}) {
|
|
4405
4747
|
this.onNewLine = events.onNewLine;
|
|
4406
4748
|
this.onNewLines = events.onNewLines;
|
|
4749
|
+
this.onNewLineSpans = events.onNewLineSpans;
|
|
4407
4750
|
this.onConversationChanged = events.onConversationChanged;
|
|
4408
4751
|
this.onFileDeleted = events.onFileDeleted;
|
|
4409
4752
|
this.onError = events.onError;
|
|
@@ -4496,20 +4839,24 @@ var ConversationWatcher = class {
|
|
|
4496
4839
|
entry.reading = true;
|
|
4497
4840
|
try {
|
|
4498
4841
|
for (; ; ) {
|
|
4499
|
-
const st = await (0,
|
|
4842
|
+
const st = await (0, import_promises5.stat)(filePath);
|
|
4500
4843
|
if (st.size <= entry.offset) break;
|
|
4501
4844
|
const readFrom = entry.offset;
|
|
4502
4845
|
const bytesToRead = st.size - readFrom;
|
|
4503
4846
|
const buf = Buffer.alloc(bytesToRead);
|
|
4504
|
-
const fh = await (0,
|
|
4847
|
+
const fh = await (0, import_promises5.open)(filePath, "r");
|
|
4505
4848
|
try {
|
|
4506
4849
|
await fh.read(buf, 0, bytesToRead, readFrom);
|
|
4507
4850
|
} finally {
|
|
4508
4851
|
await fh.close();
|
|
4509
4852
|
}
|
|
4510
|
-
|
|
4853
|
+
const { spans, consumed } = splitCompleteLines(buf, readFrom);
|
|
4854
|
+
entry.offset = readFrom + consumed;
|
|
4511
4855
|
if (!this.files.has(filePath)) return;
|
|
4512
|
-
const lines =
|
|
4856
|
+
const lines = spans.map((s) => s.text);
|
|
4857
|
+
if (spans.length > 0) {
|
|
4858
|
+
this.onNewLineSpans?.(filePath, spans, readFrom, entry.offset);
|
|
4859
|
+
}
|
|
4513
4860
|
if (this.onNewLines) {
|
|
4514
4861
|
this.onNewLines(filePath, lines);
|
|
4515
4862
|
} else {
|
|
@@ -4923,8 +5270,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
4923
5270
|
}
|
|
4924
5271
|
|
|
4925
5272
|
// src/uploads.ts
|
|
4926
|
-
var
|
|
4927
|
-
var
|
|
5273
|
+
var import_crypto8 = require("crypto");
|
|
5274
|
+
var import_promises6 = require("fs/promises");
|
|
4928
5275
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
4929
5276
|
var import_path12 = require("path");
|
|
4930
5277
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
@@ -4957,12 +5304,12 @@ async function saveUploadFile(input) {
|
|
|
4957
5304
|
mimeType = "image/jpeg";
|
|
4958
5305
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
4959
5306
|
}
|
|
4960
|
-
const id = `up_${(0,
|
|
5307
|
+
const id = `up_${(0, import_crypto8.randomBytes)(8).toString("hex")}`;
|
|
4961
5308
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
4962
5309
|
const dir = (0, import_path12.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
4963
|
-
await (0,
|
|
5310
|
+
await (0, import_promises6.mkdir)(dir, { recursive: true });
|
|
4964
5311
|
const filePath = (0, import_path12.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
4965
|
-
await (0,
|
|
5312
|
+
await (0, import_promises6.writeFile)(filePath, buffer);
|
|
4966
5313
|
return {
|
|
4967
5314
|
id,
|
|
4968
5315
|
filePath,
|
|
@@ -5149,6 +5496,10 @@ var StreamerServer = class {
|
|
|
5149
5496
|
fileWatcher;
|
|
5150
5497
|
sessionFileMap = /* @__PURE__ */ new Map();
|
|
5151
5498
|
// sessionId → JSONL filePath
|
|
5499
|
+
// Per-file seq assignments from the most recent onNewLineSpans (offset index),
|
|
5500
|
+
// handed to the immediately-following onNewLines so it can stamp WS `seq` on
|
|
5501
|
+
// the matching conversation_events entries. Same read → same lines order.
|
|
5502
|
+
pendingLineSeqs = /* @__PURE__ */ new Map();
|
|
5152
5503
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
5153
5504
|
// Content key of the AskUserQuestion currently broadcast for a session (from
|
|
5154
5505
|
// either the rendered screen or JSONL), used to de-dupe the two paths: when
|
|
@@ -5272,7 +5623,7 @@ var StreamerServer = class {
|
|
|
5272
5623
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
5273
5624
|
const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config.browseRoot;
|
|
5274
5625
|
if (rawRoot) {
|
|
5275
|
-
(0,
|
|
5626
|
+
(0, import_promises7.realpath)(rawRoot).then((resolved) => {
|
|
5276
5627
|
this.browseRoot = resolved;
|
|
5277
5628
|
if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });
|
|
5278
5629
|
}).catch(() => {
|
|
@@ -5294,6 +5645,42 @@ var StreamerServer = class {
|
|
|
5294
5645
|
this.sessionStore = new SessionStore();
|
|
5295
5646
|
this.wsHub = new WSHub();
|
|
5296
5647
|
this.fileWatcher = new ConversationWatcher({
|
|
5648
|
+
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
5649
|
+
if (!this.cache) return;
|
|
5650
|
+
const cache = this.cache;
|
|
5651
|
+
this.pendingLineSeqs.delete(filePath);
|
|
5652
|
+
try {
|
|
5653
|
+
const seqs = cache.extendMessageIndex(
|
|
5654
|
+
filePath,
|
|
5655
|
+
spans,
|
|
5656
|
+
(0, import_fs12.statSync)(filePath),
|
|
5657
|
+
readFrom,
|
|
5658
|
+
endOffset
|
|
5659
|
+
);
|
|
5660
|
+
if (seqs === null) {
|
|
5661
|
+
cache.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));
|
|
5662
|
+
cache.clearIndexParseState(filePath);
|
|
5663
|
+
this.trackCacheWrite(
|
|
5664
|
+
cache.backfillIndex(filePath).catch((err) => {
|
|
5665
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
5666
|
+
event: "offset_index.backfill_failed",
|
|
5667
|
+
filePath,
|
|
5668
|
+
trigger: "noncontiguous-append",
|
|
5669
|
+
err
|
|
5670
|
+
});
|
|
5671
|
+
})
|
|
5672
|
+
);
|
|
5673
|
+
return;
|
|
5674
|
+
}
|
|
5675
|
+
this.pendingLineSeqs.set(filePath, seqs);
|
|
5676
|
+
} catch (err) {
|
|
5677
|
+
this.log.warn("offset-index.extend_failed", {
|
|
5678
|
+
event: "offset_index.extend_failed",
|
|
5679
|
+
filePath,
|
|
5680
|
+
err
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
},
|
|
5297
5684
|
onNewLines: (filePath, lines) => {
|
|
5298
5685
|
this.cache?.updateFromLines(filePath, lines);
|
|
5299
5686
|
for (const [sessionId, watchedPath] of this.sessionFileMap) {
|
|
@@ -5320,13 +5707,20 @@ var StreamerServer = class {
|
|
|
5320
5707
|
this.pendingQuestionKey.set(sessionId, key);
|
|
5321
5708
|
if (broadcast) this.wsHub.broadcast(m);
|
|
5322
5709
|
}
|
|
5323
|
-
this.
|
|
5710
|
+
const seqs = this.pendingLineSeqs.get(filePath);
|
|
5711
|
+
this.wsHub.broadcast({
|
|
5712
|
+
type: "conversation_events",
|
|
5713
|
+
sessionId,
|
|
5714
|
+
lines,
|
|
5715
|
+
...seqs && seqs.length === lines.length ? { seqs } : {}
|
|
5716
|
+
});
|
|
5324
5717
|
for (const line of lines) {
|
|
5325
5718
|
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
5326
5719
|
}
|
|
5327
5720
|
break;
|
|
5328
5721
|
}
|
|
5329
5722
|
}
|
|
5723
|
+
this.pendingLineSeqs.delete(filePath);
|
|
5330
5724
|
},
|
|
5331
5725
|
onConversationChanged: (filePath) => {
|
|
5332
5726
|
this.fileWatcher.poke(filePath);
|
|
@@ -6049,13 +6443,13 @@ var StreamerServer = class {
|
|
|
6049
6443
|
}
|
|
6050
6444
|
const scanner = await this.getScanner();
|
|
6051
6445
|
let metas = [...scanner.getMetadataCache().values()];
|
|
6052
|
-
metas = (0,
|
|
6053
|
-
if (project) metas = (0,
|
|
6446
|
+
metas = (0, import_scanner3.applyIncludeFilter)(metas, "conversations");
|
|
6447
|
+
if (project) metas = (0, import_scanner3.applyProjectFilter)(metas, project);
|
|
6054
6448
|
if (providerFilter)
|
|
6055
6449
|
metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);
|
|
6056
|
-
metas = (0,
|
|
6450
|
+
metas = (0, import_scanner3.applySort)(metas, sort);
|
|
6057
6451
|
const total = metas.length;
|
|
6058
|
-
const page = (0,
|
|
6452
|
+
const page = (0, import_scanner3.applyPagination)(metas, limit, offset);
|
|
6059
6453
|
const adapted = page.items.map((c) => {
|
|
6060
6454
|
const id = c.sessionId || c.id.split("/").pop()?.replace(/\.jsonl$/, "") || c.id;
|
|
6061
6455
|
return {
|
|
@@ -6099,8 +6493,8 @@ var StreamerServer = class {
|
|
|
6099
6493
|
}
|
|
6100
6494
|
const scanner = await this.getScanner(true);
|
|
6101
6495
|
let metas = [...scanner.getMetadataCache().values()];
|
|
6102
|
-
metas = (0,
|
|
6103
|
-
if (project) metas = (0,
|
|
6496
|
+
metas = (0, import_scanner3.applyIncludeFilter)(metas, "conversations");
|
|
6497
|
+
if (project) metas = (0, import_scanner3.applyProjectFilter)(metas, project);
|
|
6104
6498
|
if (providerFilter)
|
|
6105
6499
|
metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);
|
|
6106
6500
|
json(res, 200, { total: metas.length });
|
|
@@ -6197,7 +6591,7 @@ var StreamerServer = class {
|
|
|
6197
6591
|
// reconciles the one conversation being requested, so paying a full-tree
|
|
6198
6592
|
// rescan just because some OTHER file changed is the stall this avoids.
|
|
6199
6593
|
newScanner(options) {
|
|
6200
|
-
return new
|
|
6594
|
+
return new import_scanner3.ConversationScanner(
|
|
6201
6595
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
6202
6596
|
);
|
|
6203
6597
|
}
|
|
@@ -6251,7 +6645,7 @@ var StreamerServer = class {
|
|
|
6251
6645
|
if (this.scannerReady) await this.scannerReady;
|
|
6252
6646
|
this.scannerStale = false;
|
|
6253
6647
|
if (!this.scanner) {
|
|
6254
|
-
this.scanner = new
|
|
6648
|
+
this.scanner = new import_scanner3.ConversationScanner();
|
|
6255
6649
|
this.allScanners.add(this.scanner);
|
|
6256
6650
|
}
|
|
6257
6651
|
const scanner = this.scanner;
|
|
@@ -6410,9 +6804,13 @@ var StreamerServer = class {
|
|
|
6410
6804
|
return;
|
|
6411
6805
|
}
|
|
6412
6806
|
const etagSource = conversation;
|
|
6807
|
+
const indexedCount = etagSource.filePath && this.cache ? this.cache.getIndexedMessageCount(
|
|
6808
|
+
ConversationCache.conversationIdForFile(etagSource.filePath)
|
|
6809
|
+
) : 0;
|
|
6810
|
+
const etagMessageCount = Math.max(etagSource.messageCount, indexedCount);
|
|
6413
6811
|
const etag = computeConversationEtag({
|
|
6414
6812
|
filePath: etagSource.filePath,
|
|
6415
|
-
messageCount:
|
|
6813
|
+
messageCount: etagMessageCount,
|
|
6416
6814
|
timestamp: etagSource.timestamp
|
|
6417
6815
|
});
|
|
6418
6816
|
const isFirstPage = !url.searchParams.has("before_index") && !url.searchParams.has("anchor_index") && !url.searchParams.has("after_index");
|
|
@@ -6429,12 +6827,14 @@ var StreamerServer = class {
|
|
|
6429
6827
|
let slice = filtered;
|
|
6430
6828
|
let fromIdx = 0;
|
|
6431
6829
|
let messagePagination;
|
|
6830
|
+
let indexTotal = null;
|
|
6432
6831
|
if (usePaging) {
|
|
6433
6832
|
const limit = Math.min(Math.max(intParam(url, "msg_limit", 80), 1), 500);
|
|
6434
6833
|
let beforeIndex = total;
|
|
6435
6834
|
let scanLimit = limit;
|
|
6436
6835
|
let anchorIndex = null;
|
|
6437
6836
|
let newerPaging = false;
|
|
6837
|
+
let usedAfterIndex = false;
|
|
6438
6838
|
if (url.searchParams.has("before_index")) {
|
|
6439
6839
|
beforeIndex = intParam(url, "before_index", total);
|
|
6440
6840
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
@@ -6443,6 +6843,7 @@ var StreamerServer = class {
|
|
|
6443
6843
|
beforeIndex = Math.min(total, from + limit);
|
|
6444
6844
|
scanLimit = beforeIndex - from;
|
|
6445
6845
|
newerPaging = true;
|
|
6846
|
+
usedAfterIndex = true;
|
|
6446
6847
|
} else if (hasAnchor) {
|
|
6447
6848
|
anchorIndex = Math.min(
|
|
6448
6849
|
Math.max(intParam(url, "anchor_index", 0), 0),
|
|
@@ -6452,9 +6853,34 @@ var StreamerServer = class {
|
|
|
6452
6853
|
beforeIndex = Math.min(total, from + limit);
|
|
6453
6854
|
newerPaging = true;
|
|
6454
6855
|
}
|
|
6455
|
-
const
|
|
6456
|
-
const
|
|
6457
|
-
|
|
6856
|
+
const isTailRequest = !url.searchParams.has("before_index") && !hasAfter && !hasAnchor;
|
|
6857
|
+
const indexFilePath = conversation.filePath;
|
|
6858
|
+
if (isTailRequest && indexFilePath && this.cache) {
|
|
6859
|
+
const indexed = this.cache.getIndexedMessageCount(
|
|
6860
|
+
ConversationCache.conversationIdForFile(indexFilePath)
|
|
6861
|
+
);
|
|
6862
|
+
if (indexed > beforeIndex) {
|
|
6863
|
+
beforeIndex = indexed;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
const windowStart = Math.max(0, beforeIndex - scanLimit);
|
|
6867
|
+
const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
|
|
6868
|
+
if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {
|
|
6869
|
+
this.trackCacheWrite(
|
|
6870
|
+
this.cache.backfillIndex(indexFilePath).catch((err) => {
|
|
6871
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
6872
|
+
event: "offset_index.backfill_failed",
|
|
6873
|
+
conversationId: id,
|
|
6874
|
+
filePath: indexFilePath,
|
|
6875
|
+
err
|
|
6876
|
+
});
|
|
6877
|
+
})
|
|
6878
|
+
);
|
|
6879
|
+
}
|
|
6880
|
+
const pagedScanner = !indexWindow && this.scannerReady ? await this.getScanner(true) : null;
|
|
6881
|
+
const page = indexWindow ?? (scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null);
|
|
6882
|
+
if (indexWindow) indexTotal = indexWindow.total;
|
|
6883
|
+
const start = page?.fromIndex ?? windowStart;
|
|
6458
6884
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
6459
6885
|
fromIdx = start;
|
|
6460
6886
|
const effectiveTotal = page?.total ?? total;
|
|
@@ -6470,6 +6896,9 @@ var StreamerServer = class {
|
|
|
6470
6896
|
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
6471
6897
|
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
6472
6898
|
}
|
|
6899
|
+
if (usedAfterIndex) {
|
|
6900
|
+
messagePagination.etag = etag;
|
|
6901
|
+
}
|
|
6473
6902
|
}
|
|
6474
6903
|
const messagesPayload = slice.map((m, localIdx) => {
|
|
6475
6904
|
const content = [];
|
|
@@ -6511,6 +6940,8 @@ var StreamerServer = class {
|
|
|
6511
6940
|
const cachedConvMeta = this.cache?.getMetaById(id);
|
|
6512
6941
|
const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);
|
|
6513
6942
|
const availability = classifyResumability(conv.projectPath);
|
|
6943
|
+
const metaMessageCount = indexTotal != null && indexTotal > conv.messageCount ? indexTotal : conv.messageCount;
|
|
6944
|
+
const metaLastUpdatedAt = indexTotal != null && indexTotal > conv.messageCount ? slice.at(-1)?.timestamp ?? conv.timestamp : conv.timestamp;
|
|
6514
6945
|
const body = {
|
|
6515
6946
|
meta: {
|
|
6516
6947
|
id,
|
|
@@ -6518,8 +6949,8 @@ var StreamerServer = class {
|
|
|
6518
6949
|
project_name: conv.projectName,
|
|
6519
6950
|
project_path: conv.projectPath,
|
|
6520
6951
|
file_path: conv.filePath,
|
|
6521
|
-
last_updated_at:
|
|
6522
|
-
message_count:
|
|
6952
|
+
last_updated_at: metaLastUpdatedAt,
|
|
6953
|
+
message_count: metaMessageCount,
|
|
6523
6954
|
last_prompt: conv.lastPrompt ?? void 0,
|
|
6524
6955
|
provider: convProvider,
|
|
6525
6956
|
resumable: isProviderResumable(convProvider, availability.resumable),
|
|
@@ -6611,7 +7042,7 @@ var StreamerServer = class {
|
|
|
6611
7042
|
}
|
|
6612
7043
|
const limit = intParam(url, "limit", 50);
|
|
6613
7044
|
const scanner = await this.getScanner();
|
|
6614
|
-
const results = await (0,
|
|
7045
|
+
const results = await (0, import_scanner3.search)(
|
|
6615
7046
|
q,
|
|
6616
7047
|
{
|
|
6617
7048
|
limit,
|