@fs/mycroft 0.3.0 → 0.4.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/completions/mycroft.bash +11 -1
- package/completions/mycroft.fish +15 -2
- package/completions/mycroft.zsh +14 -1
- package/dist/batch-embedder-C2E6OHBQ.js +14 -0
- package/dist/batch-embedder-C2E6OHBQ.js.map +1 -0
- package/dist/batch-summarizer-CM3NO7TK.js +14 -0
- package/dist/batch-summarizer-CM3NO7TK.js.map +1 -0
- package/dist/chunk-LV52FEMB.js +169 -0
- package/dist/chunk-LV52FEMB.js.map +1 -0
- package/dist/chunk-T6X7DRBN.js +275 -0
- package/dist/chunk-T6X7DRBN.js.map +1 -0
- package/dist/chunk-VBEGUDHG.js +103 -0
- package/dist/chunk-VBEGUDHG.js.map +1 -0
- package/dist/cli.js +150 -157
- package/dist/cli.js.map +1 -1
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
submitBatchEmbeddings
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-VBEGUDHG.js";
|
|
5
5
|
import {
|
|
6
|
+
CHARS_PER_TOKEN,
|
|
7
|
+
SUMMARY_PROMPT,
|
|
8
|
+
parseStructuredSummary,
|
|
9
|
+
splitIntoSections,
|
|
6
10
|
submitBatchSummaries
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-T6X7DRBN.js";
|
|
8
12
|
import {
|
|
9
13
|
CHUNK_OVERLAP,
|
|
10
14
|
CHUNK_SIZE,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
resolvePaths,
|
|
28
32
|
setConfigOverrides,
|
|
29
33
|
stdout
|
|
30
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-LV52FEMB.js";
|
|
31
35
|
|
|
32
36
|
// src/cli.ts
|
|
33
37
|
import { Command } from "commander";
|
|
@@ -89,8 +93,9 @@ var createWarnFilter = () => {
|
|
|
89
93
|
var parseEpub = async (epubPath, resourceSaveDir) => {
|
|
90
94
|
logInfo(`[EPUB Parser] Starting parse for: ${basename(epubPath)}`);
|
|
91
95
|
const suppressedWarnings = createWarnFilter();
|
|
96
|
+
let epubFile = null;
|
|
92
97
|
try {
|
|
93
|
-
|
|
98
|
+
epubFile = await initEpubFile(epubPath, resourceSaveDir);
|
|
94
99
|
await epubFile.loadEpub();
|
|
95
100
|
logInfo(`[EPUB Parser] EPUB loaded successfully`);
|
|
96
101
|
await epubFile.parse();
|
|
@@ -132,7 +137,6 @@ var parseEpub = async (epubPath, resourceSaveDir) => {
|
|
|
132
137
|
});
|
|
133
138
|
chapterTitles.push(chapterTitle);
|
|
134
139
|
}
|
|
135
|
-
epubFile.destroy();
|
|
136
140
|
const author = safeMetadata.creator?.[0]?.contributor ?? null;
|
|
137
141
|
logInfo(`[EPUB Parser] Extracted ${chapters.length} chapters with content`);
|
|
138
142
|
logInfo(`[EPUB Parser] Title: "${safeMetadata.title || fileBaseName || "Untitled"}", Author: "${author || "Unknown"}"`);
|
|
@@ -147,6 +151,7 @@ var parseEpub = async (epubPath, resourceSaveDir) => {
|
|
|
147
151
|
narrativeEndIndex
|
|
148
152
|
};
|
|
149
153
|
} finally {
|
|
154
|
+
epubFile?.destroy();
|
|
150
155
|
console.warn = originalWarn;
|
|
151
156
|
}
|
|
152
157
|
};
|
|
@@ -226,14 +231,14 @@ var chunkChapters = (bookId, chapters) => {
|
|
|
226
231
|
import { embedMany } from "ai";
|
|
227
232
|
import { openai } from "@ai-sdk/openai";
|
|
228
233
|
var MAX_TOKENS_PER_BATCH = 25e4;
|
|
229
|
-
var
|
|
234
|
+
var CHARS_PER_TOKEN2 = 4;
|
|
230
235
|
var embedChunks = async (chunks, options) => {
|
|
231
236
|
if (chunks.length === 0) return [];
|
|
232
237
|
const batches = [];
|
|
233
238
|
let currentBatch = [];
|
|
234
239
|
let currentTokens = 0;
|
|
235
240
|
for (const chunk of chunks) {
|
|
236
|
-
const estimatedTokens = Math.ceil(chunk.content.length /
|
|
241
|
+
const estimatedTokens = Math.ceil(chunk.content.length / CHARS_PER_TOKEN2);
|
|
237
242
|
if (currentTokens + estimatedTokens > MAX_TOKENS_PER_BATCH && currentBatch.length > 0) {
|
|
238
243
|
batches.push(currentBatch);
|
|
239
244
|
currentBatch = [];
|
|
@@ -250,7 +255,7 @@ var embedChunks = async (chunks, options) => {
|
|
|
250
255
|
const models = await getModels();
|
|
251
256
|
for (let i = 0; i < batches.length; i++) {
|
|
252
257
|
const batch = batches[i];
|
|
253
|
-
const estimatedTokens = batch.reduce((sum, c) => sum + Math.ceil(c.content.length /
|
|
258
|
+
const estimatedTokens = batch.reduce((sum, c) => sum + Math.ceil(c.content.length / CHARS_PER_TOKEN2), 0);
|
|
254
259
|
logInfo(`[Embedder] Batch ${i + 1}/${batches.length}: ${batch.length} chunks (~${estimatedTokens.toLocaleString()} tokens)`);
|
|
255
260
|
const { embeddings } = await embedMany({
|
|
256
261
|
model: openai.embeddingModel(models.embedding),
|
|
@@ -258,9 +263,13 @@ var embedChunks = async (chunks, options) => {
|
|
|
258
263
|
});
|
|
259
264
|
const embeddedBatch = [];
|
|
260
265
|
for (let j = 0; j < batch.length; j++) {
|
|
266
|
+
const vector = embeddings[j] ?? [];
|
|
267
|
+
if (vector.length === 0) {
|
|
268
|
+
logWarn(`[Embedder] Chunk ${allEmbedded.length + j} has empty embedding`);
|
|
269
|
+
}
|
|
261
270
|
const embeddedChunk = {
|
|
262
271
|
...batch[j],
|
|
263
|
-
vector
|
|
272
|
+
vector
|
|
264
273
|
};
|
|
265
274
|
embeddedBatch.push(embeddedChunk);
|
|
266
275
|
allEmbedded.push({
|
|
@@ -282,11 +291,14 @@ var embedChunks = async (chunks, options) => {
|
|
|
282
291
|
|
|
283
292
|
// src/services/vector-store.ts
|
|
284
293
|
import { LocalIndex } from "vectra";
|
|
294
|
+
var indexCache = /* @__PURE__ */ new Map();
|
|
285
295
|
var indexPathForBook = async (bookId) => {
|
|
286
296
|
const paths = await ensureDataDirs();
|
|
287
297
|
return `${paths.vectorsDir}/${bookId}`;
|
|
288
298
|
};
|
|
289
299
|
var createBookIndex = async (bookId) => {
|
|
300
|
+
const cached = indexCache.get(bookId);
|
|
301
|
+
if (cached) return cached;
|
|
290
302
|
const index = new LocalIndex(await indexPathForBook(bookId));
|
|
291
303
|
const exists = await index.isIndexCreated();
|
|
292
304
|
if (!exists) {
|
|
@@ -297,6 +309,7 @@ var createBookIndex = async (bookId) => {
|
|
|
297
309
|
}
|
|
298
310
|
});
|
|
299
311
|
}
|
|
312
|
+
indexCache.set(bookId, index);
|
|
300
313
|
return index;
|
|
301
314
|
};
|
|
302
315
|
var addChunksToIndex = async (bookId, chunks) => {
|
|
@@ -336,6 +349,7 @@ var queryBookIndex = async (bookId, queryVector, queryText, topK, maxChapterInde
|
|
|
336
349
|
return mapped.filter((item) => item.chapterIndex <= maxChapterIndex).slice(0, topK);
|
|
337
350
|
};
|
|
338
351
|
var deleteBookIndex = async (bookId) => {
|
|
352
|
+
indexCache.delete(bookId);
|
|
339
353
|
const index = new LocalIndex(await indexPathForBook(bookId));
|
|
340
354
|
const exists = await index.isIndexCreated();
|
|
341
355
|
if (!exists) return;
|
|
@@ -345,42 +359,7 @@ var deleteBookIndex = async (bookId) => {
|
|
|
345
359
|
// src/services/summarizer.ts
|
|
346
360
|
import { generateText } from "ai";
|
|
347
361
|
import { openai as openai2 } from "@ai-sdk/openai";
|
|
348
|
-
var
|
|
349
|
-
var estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN2);
|
|
350
|
-
var SUMMARY_PROMPT = (title, chapterNum, content) => `You are analyzing a chapter from a book (fiction or nonfiction). Extract key information to help readers understand the chapter's content.
|
|
351
|
-
|
|
352
|
-
Chapter Title: ${title}
|
|
353
|
-
Chapter Number: ${chapterNum}
|
|
354
|
-
|
|
355
|
-
---
|
|
356
|
-
${content}
|
|
357
|
-
---
|
|
358
|
-
|
|
359
|
-
Extract the following information and respond ONLY with valid JSON (no markdown, no code blocks):
|
|
360
|
-
|
|
361
|
-
{
|
|
362
|
-
"characters": ["Name - brief description (role, traits, first appearance)", ...],
|
|
363
|
-
"events": "What happens in this chapter? (2-3 sentences)",
|
|
364
|
-
"setting": "Where does this chapter take place?",
|
|
365
|
-
"revelations": "Any important information revealed? (secrets, backstory, foreshadowing)"
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
Keep the total response around ${SUMMARY_TARGET_WORDS} words.`;
|
|
369
|
-
var splitIntoSections = (text, maxTokens) => {
|
|
370
|
-
const estimatedTokens = estimateTokens(text);
|
|
371
|
-
if (estimatedTokens <= maxTokens) {
|
|
372
|
-
return [text];
|
|
373
|
-
}
|
|
374
|
-
const numSections = Math.ceil(estimatedTokens / maxTokens);
|
|
375
|
-
const charsPerSection = Math.floor(text.length / numSections);
|
|
376
|
-
const sections = [];
|
|
377
|
-
for (let i = 0; i < numSections; i++) {
|
|
378
|
-
const start = i * charsPerSection;
|
|
379
|
-
const end = i === numSections - 1 ? text.length : (i + 1) * charsPerSection;
|
|
380
|
-
sections.push(text.slice(start, end));
|
|
381
|
-
}
|
|
382
|
-
return sections;
|
|
383
|
-
};
|
|
362
|
+
var estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
384
363
|
var summarizeSection = async (text, title, sectionNum) => {
|
|
385
364
|
const models = await getModels();
|
|
386
365
|
const { text: summary } = await generateText({
|
|
@@ -396,33 +375,9 @@ var generateStructuredSummary = async (content, title, chapterIndex) => {
|
|
|
396
375
|
const models = await getModels();
|
|
397
376
|
const { text } = await generateText({
|
|
398
377
|
model: openai2(models.summary),
|
|
399
|
-
prompt: SUMMARY_PROMPT(title, chapterIndex + 1, content)
|
|
378
|
+
prompt: SUMMARY_PROMPT(title, chapterIndex + 1, content, SUMMARY_TARGET_WORDS)
|
|
400
379
|
});
|
|
401
|
-
|
|
402
|
-
if (jsonText.startsWith("```json")) {
|
|
403
|
-
jsonText = jsonText.slice(7, -3).trim();
|
|
404
|
-
} else if (jsonText.startsWith("```")) {
|
|
405
|
-
jsonText = jsonText.slice(3, -3).trim();
|
|
406
|
-
}
|
|
407
|
-
const parsed = JSON.parse(jsonText);
|
|
408
|
-
const fullSummary = `Chapter ${chapterIndex + 1}: ${title}
|
|
409
|
-
|
|
410
|
-
Characters: ${parsed.characters.join(", ")}
|
|
411
|
-
|
|
412
|
-
Events: ${parsed.events}
|
|
413
|
-
|
|
414
|
-
Setting: ${parsed.setting}
|
|
415
|
-
|
|
416
|
-
Revelations: ${parsed.revelations}`;
|
|
417
|
-
return {
|
|
418
|
-
chapterIndex,
|
|
419
|
-
chapterTitle: title,
|
|
420
|
-
characters: parsed.characters,
|
|
421
|
-
events: parsed.events,
|
|
422
|
-
setting: parsed.setting,
|
|
423
|
-
revelations: parsed.revelations,
|
|
424
|
-
fullSummary
|
|
425
|
-
};
|
|
380
|
+
return parseStructuredSummary(text, chapterIndex, title);
|
|
426
381
|
} catch (error) {
|
|
427
382
|
logWarn(`[Summarizer] Failed to parse summary JSON for "${title}": ${error instanceof Error ? error.message : String(error)}`);
|
|
428
383
|
return null;
|
|
@@ -479,6 +434,7 @@ var resolveDbPath = async () => {
|
|
|
479
434
|
};
|
|
480
435
|
var createDb = async () => {
|
|
481
436
|
const db = new Database(await resolveDbPath());
|
|
437
|
+
db.pragma("foreign_keys = ON");
|
|
482
438
|
db.exec(`
|
|
483
439
|
CREATE TABLE IF NOT EXISTS books (
|
|
484
440
|
id TEXT PRIMARY KEY,
|
|
@@ -496,7 +452,7 @@ var createDb = async () => {
|
|
|
496
452
|
db.exec(`
|
|
497
453
|
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
498
454
|
id TEXT PRIMARY KEY,
|
|
499
|
-
book_id TEXT NOT NULL,
|
|
455
|
+
book_id TEXT NOT NULL REFERENCES books(id) ON DELETE CASCADE,
|
|
500
456
|
title TEXT,
|
|
501
457
|
summary TEXT,
|
|
502
458
|
created_at INTEGER DEFAULT (strftime('%s','now')),
|
|
@@ -506,7 +462,7 @@ var createDb = async () => {
|
|
|
506
462
|
db.exec(`
|
|
507
463
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
508
464
|
id TEXT PRIMARY KEY,
|
|
509
|
-
session_id TEXT NOT NULL,
|
|
465
|
+
session_id TEXT NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
|
|
510
466
|
role TEXT NOT NULL,
|
|
511
467
|
content TEXT NOT NULL,
|
|
512
468
|
token_count INTEGER,
|
|
@@ -555,7 +511,10 @@ var mapRow = (row) => ({
|
|
|
555
511
|
ingestState: row.ingest_state ?? null,
|
|
556
512
|
ingestResumePath: row.ingest_resume_path ?? null,
|
|
557
513
|
summaryBatchId: row.summary_batch_id ?? null,
|
|
558
|
-
summaryBatchFileId: row.summary_batch_file_id ?? null
|
|
514
|
+
summaryBatchFileId: row.summary_batch_file_id ?? null,
|
|
515
|
+
summaryBatchChapters: row.summary_batch_chapters ?? null,
|
|
516
|
+
summaries: row.summaries ?? null,
|
|
517
|
+
batchChunks: row.batch_chunks ?? null
|
|
559
518
|
});
|
|
560
519
|
var dbPromise = null;
|
|
561
520
|
var getDb = async () => {
|
|
@@ -689,9 +648,12 @@ var getBookSummaryBatchChapters = async (id) => {
|
|
|
689
648
|
};
|
|
690
649
|
var deleteBook = async (id) => {
|
|
691
650
|
const db = await getDb();
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
651
|
+
const deleteAll = db.transaction((bookId) => {
|
|
652
|
+
db.prepare("DELETE FROM chat_messages WHERE session_id IN (SELECT id FROM chat_sessions WHERE book_id = ?)").run(bookId);
|
|
653
|
+
db.prepare("DELETE FROM chat_sessions WHERE book_id = ?").run(bookId);
|
|
654
|
+
db.prepare("DELETE FROM books WHERE id = ?").run(bookId);
|
|
655
|
+
});
|
|
656
|
+
deleteAll(id);
|
|
695
657
|
};
|
|
696
658
|
var mapSession = (row) => ({
|
|
697
659
|
id: row.id,
|
|
@@ -722,8 +684,8 @@ var insertChatSession = async (session) => {
|
|
|
722
684
|
bookId: session.bookId,
|
|
723
685
|
title: session.title ?? null,
|
|
724
686
|
summary: session.summary ?? null,
|
|
725
|
-
createdAt: session.createdAt ?? Date.now(),
|
|
726
|
-
updatedAt: session.updatedAt ?? Date.now()
|
|
687
|
+
createdAt: session.createdAt ?? Math.floor(Date.now() / 1e3),
|
|
688
|
+
updatedAt: session.updatedAt ?? Math.floor(Date.now() / 1e3)
|
|
727
689
|
});
|
|
728
690
|
return session.id;
|
|
729
691
|
};
|
|
@@ -768,7 +730,7 @@ var insertChatMessage = async (message) => {
|
|
|
768
730
|
role: message.role,
|
|
769
731
|
content: message.content,
|
|
770
732
|
tokenCount: message.tokenCount ?? null,
|
|
771
|
-
createdAt: message.createdAt ?? Date.now()
|
|
733
|
+
createdAt: message.createdAt ?? Math.floor(Date.now() / 1e3)
|
|
772
734
|
});
|
|
773
735
|
return message.id;
|
|
774
736
|
};
|
|
@@ -927,7 +889,7 @@ var ingestEpub = async (filePath, selectedChapterIndices, options) => {
|
|
|
927
889
|
return { id: bookId, status: "completed" };
|
|
928
890
|
};
|
|
929
891
|
var resumeIngest = async (bookId, storedChunks, batchId, batchFileId) => {
|
|
930
|
-
const { checkBatchStatus, downloadBatchResults, cleanupBatchFiles } = await import("./batch-embedder-
|
|
892
|
+
const { checkBatchStatus, downloadBatchResults, cleanupBatchFiles } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
931
893
|
logInfo(`[Resume] Checking embedding batch ${batchId} for book ${bookId}`);
|
|
932
894
|
const status = await checkBatchStatus(batchId);
|
|
933
895
|
logInfo(`[Resume] Batch status: ${status.status} (completed: ${status.completed}/${status.total})`);
|
|
@@ -937,7 +899,7 @@ var resumeIngest = async (bookId, storedChunks, batchId, batchFileId) => {
|
|
|
937
899
|
if (status.status === "failed" || status.status === "expired" || status.status === "cancelled") {
|
|
938
900
|
logWarn(`[Resume] Batch ${batchId} ended with status "${status.status}". Re-submitting...`);
|
|
939
901
|
await cleanupBatchFiles(batchFileId, status.outputFileId);
|
|
940
|
-
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-
|
|
902
|
+
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
941
903
|
const { batchId: newBatchId, inputFileId: newFileId } = await submitBatchEmbeddings2(storedChunks);
|
|
942
904
|
await updateBook(bookId, { batchId: newBatchId, batchFileId: newFileId });
|
|
943
905
|
logInfo(`[Resume] New batch submitted (${newBatchId}). Run resume again later.`);
|
|
@@ -949,7 +911,7 @@ var resumeIngest = async (bookId, storedChunks, batchId, batchFileId) => {
|
|
|
949
911
|
if (!status.outputFileId) {
|
|
950
912
|
logWarn(`[Resume] Batch ${batchId} completed but produced no output (${status.failed}/${status.total} failed). Re-submitting...`);
|
|
951
913
|
await cleanupBatchFiles(batchFileId, null);
|
|
952
|
-
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-
|
|
914
|
+
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
953
915
|
const { batchId: newBatchId, inputFileId: newFileId } = await submitBatchEmbeddings2(storedChunks);
|
|
954
916
|
await updateBook(bookId, { batchId: newBatchId, batchFileId: newFileId });
|
|
955
917
|
logInfo(`[Resume] New batch submitted (${newBatchId}). Run resume again later.`);
|
|
@@ -970,8 +932,8 @@ var resumeIngest = async (bookId, storedChunks, batchId, batchFileId) => {
|
|
|
970
932
|
return { status: "completed" };
|
|
971
933
|
};
|
|
972
934
|
var resumeSummaryBatch = async (bookId, summaryBatchId, summaryBatchFileId, storedData) => {
|
|
973
|
-
const { checkBatchStatus, cleanupBatchFiles } = await import("./batch-embedder-
|
|
974
|
-
const { downloadBatchSummaryResults, submitMergePass, downloadMergeResults } = await import("./batch-summarizer-
|
|
935
|
+
const { checkBatchStatus, cleanupBatchFiles } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
936
|
+
const { downloadBatchSummaryResults, submitMergePass, downloadMergeResults } = await import("./batch-summarizer-CM3NO7TK.js");
|
|
975
937
|
logInfo(`[Resume] Checking summary batch ${summaryBatchId} for book ${bookId}`);
|
|
976
938
|
const status = await checkBatchStatus(summaryBatchId);
|
|
977
939
|
logInfo(`[Resume] Summary batch status: ${status.status} (completed: ${status.completed}/${status.total})`);
|
|
@@ -981,7 +943,7 @@ var resumeSummaryBatch = async (bookId, summaryBatchId, summaryBatchFileId, stor
|
|
|
981
943
|
if (status.status === "failed" || status.status === "expired" || status.status === "cancelled") {
|
|
982
944
|
logWarn(`[Resume] Summary batch ${summaryBatchId} ended with status "${status.status}". Re-submitting...`);
|
|
983
945
|
await cleanupBatchFiles(summaryBatchFileId, status.outputFileId);
|
|
984
|
-
const { submitBatchSummaries: submitBatchSummaries2 } = await import("./batch-summarizer-
|
|
946
|
+
const { submitBatchSummaries: submitBatchSummaries2 } = await import("./batch-summarizer-CM3NO7TK.js");
|
|
985
947
|
const { batchId: newBatchId, inputFileId: newFileId, metadata: newMetadata } = await submitBatchSummaries2(storedData.chapters);
|
|
986
948
|
await updateBook(bookId, {
|
|
987
949
|
summaryBatchId: newBatchId,
|
|
@@ -997,7 +959,7 @@ var resumeSummaryBatch = async (bookId, summaryBatchId, summaryBatchFileId, stor
|
|
|
997
959
|
if (!status.outputFileId) {
|
|
998
960
|
logWarn(`[Resume] Summary batch ${summaryBatchId} completed but produced no output (${status.failed}/${status.total} failed). Re-submitting...`);
|
|
999
961
|
await cleanupBatchFiles(summaryBatchFileId, null);
|
|
1000
|
-
const { submitBatchSummaries: submitBatchSummaries2 } = await import("./batch-summarizer-
|
|
962
|
+
const { submitBatchSummaries: submitBatchSummaries2 } = await import("./batch-summarizer-CM3NO7TK.js");
|
|
1001
963
|
const { batchId: newBatchId, inputFileId: newFileId, metadata: newMetadata } = await submitBatchSummaries2(storedData.chapters);
|
|
1002
964
|
await updateBook(bookId, {
|
|
1003
965
|
summaryBatchId: newBatchId,
|
|
@@ -1031,8 +993,8 @@ var resumeSummaryBatch = async (bookId, summaryBatchId, summaryBatchFileId, stor
|
|
|
1031
993
|
return await finalizeSummariesAndSubmitEmbeddings(bookId, summaries, storedData);
|
|
1032
994
|
};
|
|
1033
995
|
var resumeMergeBatch = async (bookId, summaryBatchId, summaryBatchFileId, storedData) => {
|
|
1034
|
-
const { checkBatchStatus, cleanupBatchFiles } = await import("./batch-embedder-
|
|
1035
|
-
const { downloadMergeResults } = await import("./batch-summarizer-
|
|
996
|
+
const { checkBatchStatus, cleanupBatchFiles } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
997
|
+
const { downloadMergeResults } = await import("./batch-summarizer-CM3NO7TK.js");
|
|
1036
998
|
logInfo(`[Resume] Checking merge batch ${summaryBatchId} for book ${bookId}`);
|
|
1037
999
|
const status = await checkBatchStatus(summaryBatchId);
|
|
1038
1000
|
logInfo(`[Resume] Merge batch status: ${status.status} (completed: ${status.completed}/${status.total})`);
|
|
@@ -1054,7 +1016,7 @@ var resumeMergeBatch = async (bookId, summaryBatchId, summaryBatchFileId, stored
|
|
|
1054
1016
|
return await finalizeSummariesAndSubmitEmbeddings(bookId, allSummaries, storedData);
|
|
1055
1017
|
};
|
|
1056
1018
|
var finalizeSummariesAndSubmitEmbeddings = async (bookId, summaries, storedData) => {
|
|
1057
|
-
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-
|
|
1019
|
+
const { submitBatchEmbeddings: submitBatchEmbeddings2 } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
1058
1020
|
const summaryRecords = summaries.map((s) => ({
|
|
1059
1021
|
...s,
|
|
1060
1022
|
chapterIndex: storedData.selectedIndices[s.chapterIndex] ?? s.chapterIndex
|
|
@@ -1246,7 +1208,7 @@ NOTES
|
|
|
1246
1208
|
// src/commands/list.ts
|
|
1247
1209
|
var formatDate = (timestamp) => {
|
|
1248
1210
|
if (!timestamp) return "-";
|
|
1249
|
-
return new Date(timestamp).toISOString().slice(0, 10);
|
|
1211
|
+
return new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
1250
1212
|
};
|
|
1251
1213
|
var listCommand = async () => {
|
|
1252
1214
|
await ensureDataDirs();
|
|
@@ -1255,8 +1217,8 @@ var listCommand = async () => {
|
|
|
1255
1217
|
stdout("No books indexed yet.");
|
|
1256
1218
|
return;
|
|
1257
1219
|
}
|
|
1258
|
-
stdout("ID | Title | Author | Chunks | Indexed
|
|
1259
|
-
stdout("
|
|
1220
|
+
stdout("ID | Title | Author | Chunks | Indexed | Status");
|
|
1221
|
+
stdout("---------|-------|--------|--------|------------|-------");
|
|
1260
1222
|
for (const book of books) {
|
|
1261
1223
|
const shortId = book.id.slice(0, 8);
|
|
1262
1224
|
const title = book.title;
|
|
@@ -1324,10 +1286,38 @@ var registerBookShow = (program2) => {
|
|
|
1324
1286
|
// src/commands/ask.ts
|
|
1325
1287
|
import { embed, streamText } from "ai";
|
|
1326
1288
|
import { openai as openai3 } from "@ai-sdk/openai";
|
|
1289
|
+
|
|
1290
|
+
// src/shared/utils.ts
|
|
1291
|
+
var CHARS_PER_TOKEN3 = 4;
|
|
1292
|
+
var estimateTokens2 = (text) => Math.ceil(text.length / CHARS_PER_TOKEN3);
|
|
1293
|
+
var renderSources = (sources) => {
|
|
1294
|
+
if (sources.length === 0) return "";
|
|
1295
|
+
const lines = sources.map((match, index) => {
|
|
1296
|
+
const title = match.chapterTitle || `Chapter ${match.chapterIndex + 1}`;
|
|
1297
|
+
const excerpt = match.content.slice(0, 120).replace(/\s+/g, " ");
|
|
1298
|
+
return `[${index + 1}] ${title}: ${excerpt}`;
|
|
1299
|
+
});
|
|
1300
|
+
return `
|
|
1301
|
+
Sources:
|
|
1302
|
+
${lines.join("\n")}`;
|
|
1303
|
+
};
|
|
1304
|
+
var resolveMaxChapter = (book, maxChapterOption) => {
|
|
1305
|
+
const narrativeStart = book.narrativeStartIndex ?? 0;
|
|
1306
|
+
const userProgress = book.progressChapter ?? null;
|
|
1307
|
+
if (maxChapterOption !== void 0) {
|
|
1308
|
+
return narrativeStart + maxChapterOption;
|
|
1309
|
+
}
|
|
1310
|
+
if (userProgress !== null) {
|
|
1311
|
+
return narrativeStart + userProgress;
|
|
1312
|
+
}
|
|
1313
|
+
return void 0;
|
|
1314
|
+
};
|
|
1327
1315
|
var formatContext = (chunks) => chunks.map(
|
|
1328
1316
|
(chunk, index) => `Excerpt [${index + 1}] (${chunk.chapterTitle || `Chapter ${chunk.chapterIndex + 1}`}):
|
|
1329
1317
|
${chunk.content}`
|
|
1330
1318
|
).join("\n\n");
|
|
1319
|
+
|
|
1320
|
+
// src/commands/ask.ts
|
|
1331
1321
|
var askCommand = async (id, question, options) => {
|
|
1332
1322
|
if (!await isAskEnabled()) {
|
|
1333
1323
|
throw new Error("Ask is disabled in config (askEnabled: false). Enable it to use this command.");
|
|
@@ -1347,9 +1337,7 @@ var askCommand = async (id, question, options) => {
|
|
|
1347
1337
|
model: openai3.embeddingModel(models.embedding),
|
|
1348
1338
|
value: question
|
|
1349
1339
|
});
|
|
1350
|
-
const
|
|
1351
|
-
const userProgress = book.progressChapter ?? null;
|
|
1352
|
-
const maxChapterIndex = options.maxChapter !== void 0 ? narrativeStart + options.maxChapter : userProgress !== null ? narrativeStart + userProgress : void 0;
|
|
1340
|
+
const maxChapterIndex = resolveMaxChapter(book, options.maxChapter);
|
|
1353
1341
|
const retrievalLimit = options.topK * 3;
|
|
1354
1342
|
const allMatches = await queryBookIndex(resolvedId, embedding, question, retrievalLimit, maxChapterIndex);
|
|
1355
1343
|
const summaries = allMatches.filter((m) => m.type === "summary");
|
|
@@ -1383,28 +1371,20 @@ ${context}`
|
|
|
1383
1371
|
} finally {
|
|
1384
1372
|
releaseSigint();
|
|
1385
1373
|
}
|
|
1386
|
-
|
|
1387
|
-
process.stdout.write("\n\nSources:\n");
|
|
1388
|
-
selectedMatches.forEach((match, index) => {
|
|
1389
|
-
const title = match.chapterTitle || `Chapter ${match.chapterIndex + 1}`;
|
|
1390
|
-
const excerpt = match.content.slice(0, 120).replace(/\s+/g, " ");
|
|
1391
|
-
process.stdout.write(`[${index + 1}] ${title}: ${excerpt}
|
|
1392
|
-
`);
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1374
|
+
stdout(renderSources(selectedMatches));
|
|
1395
1375
|
};
|
|
1396
1376
|
|
|
1397
1377
|
// src/commands/query-options.ts
|
|
1398
1378
|
var parseQueryOptions = (options) => {
|
|
1399
1379
|
const topK = Number(options.topK);
|
|
1400
|
-
if (!Number.isFinite(topK) || topK <= 0) {
|
|
1401
|
-
throw new Error("--top-k must be a positive
|
|
1380
|
+
if (!Number.isFinite(topK) || topK <= 0 || !Number.isInteger(topK)) {
|
|
1381
|
+
throw new Error("--top-k must be a positive integer.");
|
|
1402
1382
|
}
|
|
1403
1383
|
let maxChapter;
|
|
1404
1384
|
if (options.maxChapter !== void 0) {
|
|
1405
1385
|
const parsed = Number(options.maxChapter);
|
|
1406
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1407
|
-
throw new Error("--max-chapter must be a non-negative
|
|
1386
|
+
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
|
|
1387
|
+
throw new Error("--max-chapter must be a non-negative integer.");
|
|
1408
1388
|
}
|
|
1409
1389
|
maxChapter = parsed;
|
|
1410
1390
|
}
|
|
@@ -1413,7 +1393,14 @@ var parseQueryOptions = (options) => {
|
|
|
1413
1393
|
|
|
1414
1394
|
// src/commands/book/ask.ts
|
|
1415
1395
|
var registerBookAsk = (program2) => {
|
|
1416
|
-
program2.command("ask").description("Ask a question about a book").argument("<id>", "Book id or prefix").argument("<question>", "Question to ask").option("--top-k <n>", "Number of passages to retrieve", "5").option("--max-chapter <n>", "Spoiler-free limit (0-based within narrative)").
|
|
1396
|
+
program2.command("ask").description("Ask a question about a book").argument("<id>", "Book id or prefix").argument("<question>", "Question to ask").option("--top-k <n>", "Number of passages to retrieve", "5").option("--max-chapter <n>", "Spoiler-free limit (0-based within narrative)").addHelpText(
|
|
1397
|
+
"after",
|
|
1398
|
+
`
|
|
1399
|
+
EXAMPLES
|
|
1400
|
+
mycroft book ask 8f2c1a4b "Who is the main character?"
|
|
1401
|
+
mycroft book ask 8f2c1a4b "What happened in chapter 3?" --max-chapter 3
|
|
1402
|
+
`
|
|
1403
|
+
).action(async (id, question, options) => {
|
|
1417
1404
|
const { topK, maxChapter } = parseQueryOptions(options);
|
|
1418
1405
|
await askCommand(id, question, { topK, maxChapter });
|
|
1419
1406
|
});
|
|
@@ -1438,7 +1425,7 @@ var searchCommand = async (id, query, options) => {
|
|
|
1438
1425
|
model: openai4.embeddingModel(models.embedding),
|
|
1439
1426
|
value: query
|
|
1440
1427
|
});
|
|
1441
|
-
const maxChapterIndex =
|
|
1428
|
+
const maxChapterIndex = resolveMaxChapter(book, options.maxChapter);
|
|
1442
1429
|
const results = await queryBookIndex(resolvedId, embedding, query, options.topK, maxChapterIndex);
|
|
1443
1430
|
if (results.length === 0) {
|
|
1444
1431
|
stdout("No results.");
|
|
@@ -1456,7 +1443,14 @@ var searchCommand = async (id, query, options) => {
|
|
|
1456
1443
|
|
|
1457
1444
|
// src/commands/book/search.ts
|
|
1458
1445
|
var registerBookSearch = (program2) => {
|
|
1459
|
-
program2.command("search").description("Vector search without LLM").argument("<id>", "Book id or prefix").argument("<query>", "Search query").option("--top-k <n>", "Number of passages to retrieve", "5").option("--max-chapter <n>", "Spoiler-free limit (0-based within narrative)").
|
|
1446
|
+
program2.command("search").description("Vector search without LLM").argument("<id>", "Book id or prefix").argument("<query>", "Search query").option("--top-k <n>", "Number of passages to retrieve", "5").option("--max-chapter <n>", "Spoiler-free limit (0-based within narrative)").addHelpText(
|
|
1447
|
+
"after",
|
|
1448
|
+
`
|
|
1449
|
+
EXAMPLES
|
|
1450
|
+
mycroft book search 8f2c1a4b "the storm scene"
|
|
1451
|
+
mycroft book search 8f2c1a4b "betrayal" --top-k 10
|
|
1452
|
+
`
|
|
1453
|
+
).action(async (id, query, options) => {
|
|
1460
1454
|
const { topK, maxChapter } = parseQueryOptions(options);
|
|
1461
1455
|
await searchCommand(id, query, { topK, maxChapter });
|
|
1462
1456
|
});
|
|
@@ -1487,14 +1481,23 @@ var deleteCommand = async (id, options) => {
|
|
|
1487
1481
|
await deleteBook(resolvedId);
|
|
1488
1482
|
await deleteBookIndex(resolvedId);
|
|
1489
1483
|
if (book.epubPath) {
|
|
1490
|
-
await unlink2(book.epubPath).catch(() =>
|
|
1484
|
+
await unlink2(book.epubPath).catch((err) => {
|
|
1485
|
+
if (err.code !== "ENOENT") throw err;
|
|
1486
|
+
});
|
|
1491
1487
|
}
|
|
1492
1488
|
stdout(`Deleted book ${book.id}`);
|
|
1493
1489
|
};
|
|
1494
1490
|
|
|
1495
1491
|
// src/commands/book/delete.ts
|
|
1496
1492
|
var registerBookDelete = (program2) => {
|
|
1497
|
-
program2.command("delete").description("Remove book, EPUB, and vectors").argument("<id>", "Book id or prefix").option("--force", "Skip confirmation").
|
|
1493
|
+
program2.command("delete").description("Remove book, EPUB, and vectors").argument("<id>", "Book id or prefix").option("--force", "Skip confirmation").addHelpText(
|
|
1494
|
+
"after",
|
|
1495
|
+
`
|
|
1496
|
+
EXAMPLES
|
|
1497
|
+
mycroft book delete 8f2c1a4b
|
|
1498
|
+
mycroft book delete 8f2c1a4b --force
|
|
1499
|
+
`
|
|
1500
|
+
).action(async (id, options) => {
|
|
1498
1501
|
await deleteCommand(id, { force: options.force });
|
|
1499
1502
|
});
|
|
1500
1503
|
};
|
|
@@ -1521,7 +1524,12 @@ var resumeCommand = async (id) => {
|
|
|
1521
1524
|
if (!rawData) {
|
|
1522
1525
|
throw new Error(`No stored summary batch data for book "${book.title}". Re-ingest with "mycroft book ingest --batch --summary".`);
|
|
1523
1526
|
}
|
|
1524
|
-
|
|
1527
|
+
let storedData;
|
|
1528
|
+
try {
|
|
1529
|
+
storedData = JSON.parse(rawData);
|
|
1530
|
+
} catch {
|
|
1531
|
+
throw new Error(`Corrupt summary batch data for book "${book.title}". Re-ingest with "mycroft book ingest --batch --summary".`);
|
|
1532
|
+
}
|
|
1525
1533
|
let result2;
|
|
1526
1534
|
if (storedData.isMergePass) {
|
|
1527
1535
|
result2 = await resumeMergeBatch(resolvedId, book.summaryBatchId, book.summaryBatchFileId ?? book.summaryBatchId, storedData);
|
|
@@ -1556,7 +1564,12 @@ Summary batch still in progress (${result2.status}: ${result2.completed}/${resul
|
|
|
1556
1564
|
if (!rawChunks) {
|
|
1557
1565
|
throw new Error(`No stored chunks found for book "${book.title}". Re-ingest with "mycroft book ingest --batch".`);
|
|
1558
1566
|
}
|
|
1559
|
-
|
|
1567
|
+
let chunks;
|
|
1568
|
+
try {
|
|
1569
|
+
chunks = JSON.parse(rawChunks);
|
|
1570
|
+
} catch {
|
|
1571
|
+
throw new Error(`Corrupt chunk data for book "${book.title}". Re-ingest with "mycroft book ingest --batch".`);
|
|
1572
|
+
}
|
|
1560
1573
|
const result2 = await resumeIngest(resolvedId, chunks, book.batchId, book.batchFileId ?? book.batchId);
|
|
1561
1574
|
if (result2.status === "completed") {
|
|
1562
1575
|
stdout(`
|
|
@@ -1624,7 +1637,7 @@ Status: completed`);
|
|
|
1624
1637
|
}
|
|
1625
1638
|
if (book.summaryBatchId) {
|
|
1626
1639
|
requireOpenAIKey();
|
|
1627
|
-
const { checkBatchStatus } = await import("./batch-embedder-
|
|
1640
|
+
const { checkBatchStatus } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
1628
1641
|
const status = await checkBatchStatus(book.summaryBatchId);
|
|
1629
1642
|
stdout(`
|
|
1630
1643
|
Status: summary batch ${status.status}`);
|
|
@@ -1653,7 +1666,7 @@ Summary batch still processing.`);
|
|
|
1653
1666
|
}
|
|
1654
1667
|
if (book.batchId) {
|
|
1655
1668
|
requireOpenAIKey();
|
|
1656
|
-
const { checkBatchStatus } = await import("./batch-embedder-
|
|
1669
|
+
const { checkBatchStatus } = await import("./batch-embedder-C2E6OHBQ.js");
|
|
1657
1670
|
const status = await checkBatchStatus(book.batchId);
|
|
1658
1671
|
stdout(`
|
|
1659
1672
|
Status: embedding batch ${status.status}`);
|
|
@@ -1835,16 +1848,11 @@ var registerConfigOnboard = (program2) => {
|
|
|
1835
1848
|
|
|
1836
1849
|
// src/services/chat.ts
|
|
1837
1850
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1838
|
-
import { embed as embed3, generateText as generateText2 } from "ai";
|
|
1851
|
+
import { embed as embed3, generateText as generateText2, streamText as streamText2 } from "ai";
|
|
1839
1852
|
import { openai as openai5 } from "@ai-sdk/openai";
|
|
1840
1853
|
var MAX_RECENT_MESSAGES = 12;
|
|
1841
1854
|
var SUMMARY_TRIGGER_MESSAGES = 24;
|
|
1842
1855
|
var SUMMARY_TARGET_WORDS2 = 160;
|
|
1843
|
-
var formatContext2 = (chunks) => chunks.map(
|
|
1844
|
-
(chunk, index) => `Excerpt [${index + 1}] (${chunk.chapterTitle || `Chapter ${chunk.chapterIndex + 1}`}):
|
|
1845
|
-
${chunk.content}`
|
|
1846
|
-
).join("\n\n");
|
|
1847
|
-
var estimateTokens2 = (text) => Math.ceil(text.length / 4);
|
|
1848
1856
|
var summarizeMessages = async (messages) => {
|
|
1849
1857
|
const transcript = messages.map((message) => `${message.role.toUpperCase()}: ${message.content}`).join("\n\n");
|
|
1850
1858
|
const models = await getModels();
|
|
@@ -1908,9 +1916,7 @@ var chatAsk = async (sessionId, question, options) => {
|
|
|
1908
1916
|
model: openai5.embeddingModel(models.embedding),
|
|
1909
1917
|
value: question
|
|
1910
1918
|
});
|
|
1911
|
-
const
|
|
1912
|
-
const userProgress = book.progressChapter ?? null;
|
|
1913
|
-
const maxChapterIndex = options.maxChapter !== void 0 ? narrativeStart + options.maxChapter : userProgress !== null ? narrativeStart + userProgress : void 0;
|
|
1919
|
+
const maxChapterIndex = resolveMaxChapter(book, options.maxChapter);
|
|
1914
1920
|
const retrievalLimit = options.topK * 3;
|
|
1915
1921
|
const allMatches = await queryBookIndex(session.bookId, embedding, question, retrievalLimit, maxChapterIndex);
|
|
1916
1922
|
const summaries = allMatches.filter((m) => m.type === "summary");
|
|
@@ -1918,26 +1924,17 @@ var chatAsk = async (sessionId, question, options) => {
|
|
|
1918
1924
|
const topSummaries = summaries.slice(0, 2);
|
|
1919
1925
|
const topChunks = chunks.slice(0, Math.max(0, options.topK - topSummaries.length));
|
|
1920
1926
|
const selectedMatches = [...topSummaries, ...topChunks];
|
|
1921
|
-
const context =
|
|
1927
|
+
const context = formatContext(selectedMatches);
|
|
1922
1928
|
const messages = await getChatMessages(sessionId);
|
|
1923
1929
|
const conversation = buildConversationContext(session, messages);
|
|
1924
|
-
const now = Date.now();
|
|
1925
|
-
const userMessage = {
|
|
1926
|
-
id: randomUUID2(),
|
|
1927
|
-
sessionId,
|
|
1928
|
-
role: "user",
|
|
1929
|
-
content: question,
|
|
1930
|
-
tokenCount: estimateTokens2(question),
|
|
1931
|
-
createdAt: now
|
|
1932
|
-
};
|
|
1933
|
-
await insertChatMessage(userMessage);
|
|
1930
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1934
1931
|
const prompt2 = [
|
|
1935
1932
|
conversation ? `Conversation:
|
|
1936
1933
|
${conversation}` : "",
|
|
1937
1934
|
`Question: ${question}`,
|
|
1938
1935
|
context
|
|
1939
1936
|
].filter(Boolean).join("\n\n");
|
|
1940
|
-
const
|
|
1937
|
+
const stream = streamText2({
|
|
1941
1938
|
model: openai5(models.chat),
|
|
1942
1939
|
system: `You are a reading companion helping readers understand this book.
|
|
1943
1940
|
|
|
@@ -1952,6 +1949,16 @@ Guidelines:
|
|
|
1952
1949
|
- The context may be limited to earlier chapters only - don't infer beyond what's provided`,
|
|
1953
1950
|
prompt: prompt2
|
|
1954
1951
|
});
|
|
1952
|
+
const text = await stream.text;
|
|
1953
|
+
const userMessage = {
|
|
1954
|
+
id: randomUUID2(),
|
|
1955
|
+
sessionId,
|
|
1956
|
+
role: "user",
|
|
1957
|
+
content: question,
|
|
1958
|
+
tokenCount: estimateTokens2(question),
|
|
1959
|
+
createdAt: now
|
|
1960
|
+
};
|
|
1961
|
+
await insertChatMessage(userMessage);
|
|
1955
1962
|
const assistantMessage = {
|
|
1956
1963
|
id: randomUUID2(),
|
|
1957
1964
|
sessionId,
|
|
@@ -1961,7 +1968,7 @@ Guidelines:
|
|
|
1961
1968
|
createdAt: now
|
|
1962
1969
|
};
|
|
1963
1970
|
await insertChatMessage(assistantMessage);
|
|
1964
|
-
const updatedAt = Date.now();
|
|
1971
|
+
const updatedAt = Math.floor(Date.now() / 1e3);
|
|
1965
1972
|
await updateChatSession(sessionId, { updatedAt });
|
|
1966
1973
|
await maybeSummarizeSession(session, [...messages, userMessage, assistantMessage], updatedAt);
|
|
1967
1974
|
return { answer: text, sources: selectedMatches };
|
|
@@ -1998,21 +2005,14 @@ var registerChatAsk = (program2) => {
|
|
|
1998
2005
|
}
|
|
1999
2006
|
const { answer, sources } = await chatAsk(resolvedId, question, { topK, maxChapter });
|
|
2000
2007
|
stdout(answer);
|
|
2001
|
-
|
|
2002
|
-
stdout("\nSources:");
|
|
2003
|
-
sources.forEach((match, index) => {
|
|
2004
|
-
const title = match.chapterTitle || `Chapter ${match.chapterIndex + 1}`;
|
|
2005
|
-
const excerpt = match.content.slice(0, 120).replace(/\s+/g, " ");
|
|
2006
|
-
stdout(`[${index + 1}] ${title}: ${excerpt}`);
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2008
|
+
stdout(renderSources(sources));
|
|
2009
2009
|
});
|
|
2010
2010
|
};
|
|
2011
2011
|
|
|
2012
2012
|
// src/commands/chat/list.ts
|
|
2013
2013
|
var formatDate2 = (timestamp) => {
|
|
2014
2014
|
if (!timestamp) return "-";
|
|
2015
|
-
return new Date(timestamp).toISOString().slice(0, 10);
|
|
2015
|
+
return new Date(timestamp * 1e3).toISOString().slice(0, 10);
|
|
2016
2016
|
};
|
|
2017
2017
|
var registerChatList = (program2) => {
|
|
2018
2018
|
program2.command("list").description("List chat sessions").action(async () => {
|
|
@@ -2092,14 +2092,7 @@ var registerChatRepl = (program2) => {
|
|
|
2092
2092
|
const { answer, sources } = await chatAsk(session.id, question, { topK, maxChapter });
|
|
2093
2093
|
stdout(`
|
|
2094
2094
|
${answer}`);
|
|
2095
|
-
|
|
2096
|
-
stdout("\nSources:");
|
|
2097
|
-
sources.forEach((match, index) => {
|
|
2098
|
-
const title = match.chapterTitle || `Chapter ${match.chapterIndex + 1}`;
|
|
2099
|
-
const excerpt = match.content.slice(0, 120).replace(/\s+/g, " ");
|
|
2100
|
-
stdout(`[${index + 1}] ${title}: ${excerpt}`);
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2095
|
+
stdout(renderSources(sources));
|
|
2103
2096
|
stdout("");
|
|
2104
2097
|
}
|
|
2105
2098
|
});
|