paper-manager 0.10.4 → 0.11.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/README.md +10 -10
- package/dist/commands/knowledge-base.js +11 -8
- package/dist/commands/literature.js +27 -12
- package/dist/commands/output.js +25 -0
- package/dist/commands/util.js +4 -2
- package/dist/db/operations/literatures.js +8 -0
- package/dist/db/project/literatures.js +3 -0
- package/dist/db/user/literatures.js +3 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -55,22 +55,22 @@ paper config list [--user] # List all config
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
paper kb create <name> -d <desc> [-e <model-id>] [--user] # Create a knowledge base
|
|
58
|
-
paper kb list [--all | --user]
|
|
59
|
-
paper kb update <id> [-n <name>] [-d <desc>]
|
|
60
|
-
paper kb remove <id>
|
|
61
|
-
paper kb query <id> <query-text>
|
|
58
|
+
paper kb list [--all | --user] [--json] [--jq <expr>] # List knowledge bases
|
|
59
|
+
paper kb update <id> [-n <name>] [-d <desc>] # Update knowledge base metadata
|
|
60
|
+
paper kb remove <id> # Remove a knowledge base
|
|
61
|
+
paper kb query <id> <query-text> [--json] [--jq <expr>] # Query a knowledge base
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
### Literature (`paper lit`)
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
paper lit add <kb-id> <file-path>
|
|
67
|
+
paper lit add <kb-id> <file-path> [-f] # Add a literature (auto-extracts PDF metadata, rejects duplicate DOI)
|
|
68
68
|
paper lit remove <kb-id> <id> # Remove a literature
|
|
69
69
|
paper lit update <kb-id> <id> [opts] # Update literature metadata
|
|
70
|
-
paper lit list <kb-id>
|
|
71
|
-
paper lit search <kb-id> [opts]
|
|
72
|
-
paper lit show <kb-id> <id>
|
|
73
|
-
paper lit note list <lit-id>
|
|
70
|
+
paper lit list <kb-id> [--json] [--jq <expr>] # List literatures
|
|
71
|
+
paper lit search <kb-id> [opts] [--json] [--jq <expr>] # Search literatures by metadata
|
|
72
|
+
paper lit show <kb-id> <id> [--json] [--jq <expr>] # Show literature details
|
|
73
|
+
paper lit note list <lit-id> [--json] [--jq <expr>] # List notes
|
|
74
74
|
paper lit note set <lit-id> <k> <v> # Set a note
|
|
75
75
|
paper lit note remove <lit-id> <key> # Remove a note
|
|
76
76
|
```
|
|
@@ -79,7 +79,7 @@ paper lit note remove <lit-id> <key> # Remove a note
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
paper util doi2bib <doi> # Convert a DOI to BibTeX citation
|
|
82
|
-
paper util pdf-meta <file> [--json] # Extract metadata from a PDF file
|
|
82
|
+
paper util pdf-meta <file> [--json] [--jq <expr>] # Extract metadata from a PDF file
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
## Configuration
|
|
@@ -9,6 +9,7 @@ import * as userLit from "../db/user/literatures.js";
|
|
|
9
9
|
import { removeImageDir } from "../extractor/markdown.js";
|
|
10
10
|
import { log } from "../logger.js";
|
|
11
11
|
import { queryVectorStore } from "../vector-store/index.js";
|
|
12
|
+
import { outputJson } from "./output.js";
|
|
12
13
|
function resolveKnowledgeBase(id) {
|
|
13
14
|
const pkb = projectKb.getKnowledgeBase(id);
|
|
14
15
|
if (pkb)
|
|
@@ -59,6 +60,7 @@ export function createKnowledgeBaseCommand() {
|
|
|
59
60
|
.option("--user", "List user knowledge bases only")
|
|
60
61
|
.option("--all", "List all knowledge bases (default)")
|
|
61
62
|
.option("--json", "Output as JSON")
|
|
63
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
62
64
|
.action((options) => {
|
|
63
65
|
let results = [];
|
|
64
66
|
if (options.user) {
|
|
@@ -70,16 +72,16 @@ export function createKnowledgeBaseCommand() {
|
|
|
70
72
|
results = [...projectKbs, ...userKbs];
|
|
71
73
|
}
|
|
72
74
|
if (results.length === 0) {
|
|
73
|
-
if (options.json) {
|
|
74
|
-
|
|
75
|
+
if (options.json || options.jq) {
|
|
76
|
+
outputJson([], options.jq);
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
79
|
log.info("No knowledge bases found.");
|
|
78
80
|
}
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
|
-
if (options.json) {
|
|
82
|
-
|
|
83
|
+
if (options.json || options.jq) {
|
|
84
|
+
outputJson(results, options.jq);
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
for (const kb of results) {
|
|
@@ -164,6 +166,7 @@ export function createKnowledgeBaseCommand() {
|
|
|
164
166
|
.description("Query a knowledge base")
|
|
165
167
|
.option("-k, --top-k <number>", "Number of results", "5")
|
|
166
168
|
.option("--json", "Output as JSON")
|
|
169
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
167
170
|
.action(async (id, queryText, options) => {
|
|
168
171
|
const resolved = resolveKnowledgeBase(id);
|
|
169
172
|
if (!resolved) {
|
|
@@ -181,19 +184,19 @@ export function createKnowledgeBaseCommand() {
|
|
|
181
184
|
const k = parseInt(options.topK, 10);
|
|
182
185
|
const results = await queryVectorStore(modelConfig, vectorDir, queryText, k);
|
|
183
186
|
if (results.length === 0) {
|
|
184
|
-
if (options.json) {
|
|
185
|
-
|
|
187
|
+
if (options.json || options.jq) {
|
|
188
|
+
outputJson([], options.jq);
|
|
186
189
|
}
|
|
187
190
|
else {
|
|
188
191
|
log.info("No results found.");
|
|
189
192
|
}
|
|
190
193
|
return;
|
|
191
194
|
}
|
|
192
|
-
if (options.json) {
|
|
195
|
+
if (options.json || options.jq) {
|
|
193
196
|
const output = results
|
|
194
197
|
.filter((doc) => doc != null)
|
|
195
198
|
.map((doc) => ({ pageContent: doc.pageContent, metadata: doc.metadata }));
|
|
196
|
-
|
|
199
|
+
outputJson(output, options.jq);
|
|
197
200
|
return;
|
|
198
201
|
}
|
|
199
202
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -13,6 +13,7 @@ import { convertPdfToMarkdown, isOpendataLoaderAvailable, removeImageDir, saveCo
|
|
|
13
13
|
import { log } from "../logger.js";
|
|
14
14
|
import { splitDocuments } from "../text-splitter.js";
|
|
15
15
|
import { addDocuments, createVectorStore } from "../vector-store/index.js";
|
|
16
|
+
import { outputJson } from "./output.js";
|
|
16
17
|
function resolveKnowledgeBase(id) {
|
|
17
18
|
const pkb = projectKb.getKnowledgeBase(id);
|
|
18
19
|
if (pkb)
|
|
@@ -35,6 +36,7 @@ export function createLiteratureCommand() {
|
|
|
35
36
|
.command("add <knowledge-base-id> <lit-path>")
|
|
36
37
|
.description("Add a literature from a file (PDF, TXT, MD, TEX, etc.)")
|
|
37
38
|
.option("-t, --title <title>", "Literature title")
|
|
39
|
+
.option("-f, --force", "Force add even if a literature with the same DOI already exists")
|
|
38
40
|
.action(async (kbId, litPath, options) => {
|
|
39
41
|
const resolved = resolveKnowledgeBase(kbId);
|
|
40
42
|
if (!resolved) {
|
|
@@ -76,6 +78,15 @@ export function createLiteratureCommand() {
|
|
|
76
78
|
log.step(`Creator: ${pdfMeta.creator}`);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
// Check for duplicate DOI in the knowledge base
|
|
82
|
+
if (pdfMeta?.doi && !options.force) {
|
|
83
|
+
const existing = litOps.findLiteratureByDoi(kbId, pdfMeta.doi);
|
|
84
|
+
if (existing) {
|
|
85
|
+
log.error(`A literature with DOI "${pdfMeta.doi}" already exists in this knowledge base: ${existing.id} (${existing.title})`);
|
|
86
|
+
log.info("Use --force to add anyway.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
79
90
|
const title = options.title ?? pdfMeta?.title ?? path.basename(litPath, path.extname(litPath));
|
|
80
91
|
// Create literature record
|
|
81
92
|
const literature = litOps.createLiterature({
|
|
@@ -249,6 +260,7 @@ export function createLiteratureCommand() {
|
|
|
249
260
|
.command("list <knowledge-base-id>")
|
|
250
261
|
.description("List literatures in a knowledge base")
|
|
251
262
|
.option("--json", "Output as JSON")
|
|
263
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
252
264
|
.action((kbId, options) => {
|
|
253
265
|
const resolved = resolveKnowledgeBase(kbId);
|
|
254
266
|
if (!resolved) {
|
|
@@ -258,16 +270,16 @@ export function createLiteratureCommand() {
|
|
|
258
270
|
const litOps = getLitOps(resolved.scope);
|
|
259
271
|
const literatures = litOps.listLiteratures(kbId);
|
|
260
272
|
if (literatures.length === 0) {
|
|
261
|
-
if (options.json) {
|
|
262
|
-
|
|
273
|
+
if (options.json || options.jq) {
|
|
274
|
+
outputJson([], options.jq);
|
|
263
275
|
}
|
|
264
276
|
else {
|
|
265
277
|
log.info("No literatures found.");
|
|
266
278
|
}
|
|
267
279
|
return;
|
|
268
280
|
}
|
|
269
|
-
if (options.json) {
|
|
270
|
-
|
|
281
|
+
if (options.json || options.jq) {
|
|
282
|
+
outputJson(literatures, options.jq);
|
|
271
283
|
return;
|
|
272
284
|
}
|
|
273
285
|
const filesDir = getFilesDir(getBaseDir(resolved.scope));
|
|
@@ -292,6 +304,7 @@ export function createLiteratureCommand() {
|
|
|
292
304
|
.option("-k, --keyword <keyword>", "Keyword substring")
|
|
293
305
|
.option("--doi <doi>", "DOI substring")
|
|
294
306
|
.option("--json", "Output as JSON")
|
|
307
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
295
308
|
.action((kbId, options) => {
|
|
296
309
|
const resolved = resolveKnowledgeBase(kbId);
|
|
297
310
|
if (!resolved) {
|
|
@@ -313,16 +326,16 @@ export function createLiteratureCommand() {
|
|
|
313
326
|
doi: options.doi,
|
|
314
327
|
});
|
|
315
328
|
if (results.length === 0) {
|
|
316
|
-
if (options.json) {
|
|
317
|
-
|
|
329
|
+
if (options.json || options.jq) {
|
|
330
|
+
outputJson([], options.jq);
|
|
318
331
|
}
|
|
319
332
|
else {
|
|
320
333
|
log.info("No literatures found.");
|
|
321
334
|
}
|
|
322
335
|
return;
|
|
323
336
|
}
|
|
324
|
-
if (options.json) {
|
|
325
|
-
|
|
337
|
+
if (options.json || options.jq) {
|
|
338
|
+
outputJson(results, options.jq);
|
|
326
339
|
return;
|
|
327
340
|
}
|
|
328
341
|
for (const l of results) {
|
|
@@ -344,6 +357,7 @@ export function createLiteratureCommand() {
|
|
|
344
357
|
.command("show <knowledge-base-id> <id>")
|
|
345
358
|
.description("Show literature details")
|
|
346
359
|
.option("--json", "Output as JSON")
|
|
360
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
347
361
|
.action((kbId, id, options) => {
|
|
348
362
|
const resolved = resolveKnowledgeBase(kbId);
|
|
349
363
|
if (!resolved) {
|
|
@@ -356,8 +370,8 @@ export function createLiteratureCommand() {
|
|
|
356
370
|
log.error(`Literature not found: ${id}`);
|
|
357
371
|
process.exit(1);
|
|
358
372
|
}
|
|
359
|
-
if (options.json) {
|
|
360
|
-
|
|
373
|
+
if (options.json || options.jq) {
|
|
374
|
+
outputJson(literature, options.jq);
|
|
361
375
|
return;
|
|
362
376
|
}
|
|
363
377
|
const filesDir = getFilesDir(getBaseDir(resolved.scope));
|
|
@@ -369,14 +383,15 @@ export function createLiteratureCommand() {
|
|
|
369
383
|
.command("list <literature-id>")
|
|
370
384
|
.description("List all notes for a literature")
|
|
371
385
|
.option("--json", "Output as JSON")
|
|
386
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
372
387
|
.action((litId, options) => {
|
|
373
388
|
const literature = findLiterature(litId);
|
|
374
389
|
if (!literature) {
|
|
375
390
|
log.error(`Literature not found: ${litId}`);
|
|
376
391
|
process.exit(1);
|
|
377
392
|
}
|
|
378
|
-
if (options.json) {
|
|
379
|
-
|
|
393
|
+
if (options.json || options.jq) {
|
|
394
|
+
outputJson(literature.notes, options.jq);
|
|
380
395
|
return;
|
|
381
396
|
}
|
|
382
397
|
const entries = Object.entries(literature.notes);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jq } from "@eurfelux/jq-js";
|
|
2
|
+
import { log } from "../logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Output data as JSON, optionally filtered by a jq expression.
|
|
5
|
+
* When `jqExpr` is provided, applies the jq filter and prints each result value.
|
|
6
|
+
* Otherwise prints pretty-printed JSON.
|
|
7
|
+
*/
|
|
8
|
+
export function outputJson(data, jqExpr) {
|
|
9
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
10
|
+
if (jqExpr) {
|
|
11
|
+
const normalized = JSON.parse(jsonStr);
|
|
12
|
+
const results = jq(jqExpr, normalized);
|
|
13
|
+
for (const result of results) {
|
|
14
|
+
if (typeof result === "string") {
|
|
15
|
+
log.plain(result);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
log.plain(JSON.stringify(result, null, 2));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
log.plain(jsonStr);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/commands/util.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { extractPdfMetadata } from "../extractor/pdf.js";
|
|
5
5
|
import { log } from "../logger.js";
|
|
6
|
+
import { outputJson } from "./output.js";
|
|
6
7
|
export function createUtilCommand() {
|
|
7
8
|
const util = new Command("util").description("Utility commands");
|
|
8
9
|
util
|
|
@@ -26,6 +27,7 @@ export function createUtilCommand() {
|
|
|
26
27
|
.command("pdf-meta <file>")
|
|
27
28
|
.description("Extract metadata from a PDF file")
|
|
28
29
|
.option("--json", "Output as JSON")
|
|
30
|
+
.option("--jq <expression>", "Filter JSON output with a jq expression (implies --json)")
|
|
29
31
|
.action(async (file, options) => {
|
|
30
32
|
const absolutePath = path.resolve(file);
|
|
31
33
|
if (!existsSync(absolutePath)) {
|
|
@@ -44,8 +46,8 @@ export function createUtilCommand() {
|
|
|
44
46
|
log.error(`Failed to parse PDF: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
47
|
process.exit(1);
|
|
46
48
|
}
|
|
47
|
-
if (options.json) {
|
|
48
|
-
|
|
49
|
+
if (options.json || options.jq) {
|
|
50
|
+
outputJson(meta, options.jq);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
log.header("PDF Metadata");
|
|
@@ -85,6 +85,14 @@ export function updateLiterature(db, id, input) {
|
|
|
85
85
|
const row = db.update(literatures).set(updates).where(eq(literatures.id, id)).returning().get();
|
|
86
86
|
return row ?? null;
|
|
87
87
|
}
|
|
88
|
+
export function findLiteratureByDoi(db, knowledgeBaseId, doi) {
|
|
89
|
+
const row = db
|
|
90
|
+
.select()
|
|
91
|
+
.from(literatures)
|
|
92
|
+
.where(and(eq(literatures.knowledgeBaseId, knowledgeBaseId), eq(literatures.doi, doi)))
|
|
93
|
+
.get();
|
|
94
|
+
return row ?? null;
|
|
95
|
+
}
|
|
88
96
|
export function deleteLiterature(db, id) {
|
|
89
97
|
const result = db.delete(literatures).where(eq(literatures.id, id)).run();
|
|
90
98
|
return result.changes > 0;
|
|
@@ -21,6 +21,9 @@ export function deleteLiteraturesByKnowledgeBaseId(knowledgeBaseId) {
|
|
|
21
21
|
export function searchLiteratures(knowledgeBaseId, filters) {
|
|
22
22
|
return ops.searchLiteratures(getProjectDb(), knowledgeBaseId, filters);
|
|
23
23
|
}
|
|
24
|
+
export function findLiteratureByDoi(knowledgeBaseId, doi) {
|
|
25
|
+
return ops.findLiteratureByDoi(getProjectDb(), knowledgeBaseId, doi);
|
|
26
|
+
}
|
|
24
27
|
export function getLiteraturesByKnowledgeBaseId(knowledgeBaseId) {
|
|
25
28
|
return ops.getLiteraturesByKnowledgeBaseId(getProjectDb(), knowledgeBaseId);
|
|
26
29
|
}
|
|
@@ -21,6 +21,9 @@ export function deleteLiteraturesByKnowledgeBaseId(knowledgeBaseId) {
|
|
|
21
21
|
export function searchLiteratures(knowledgeBaseId, filters) {
|
|
22
22
|
return ops.searchLiteratures(getUserDb(), knowledgeBaseId, filters);
|
|
23
23
|
}
|
|
24
|
+
export function findLiteratureByDoi(knowledgeBaseId, doi) {
|
|
25
|
+
return ops.findLiteratureByDoi(getUserDb(), knowledgeBaseId, doi);
|
|
26
|
+
}
|
|
24
27
|
export function getLiteraturesByKnowledgeBaseId(knowledgeBaseId) {
|
|
25
28
|
return ops.getLiteraturesByKnowledgeBaseId(getUserDb(), knowledgeBaseId);
|
|
26
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paper-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "A paper management system.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/EurFelux/paper-manager",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ai-sdk/openai": "^3.0.37",
|
|
23
23
|
"@ai-sdk/provider": "^3.0.8",
|
|
24
|
+
"@eurfelux/jq-js": "^0.3.0",
|
|
24
25
|
"ai": "^6.0.105",
|
|
25
26
|
"better-sqlite3": "^12.6.2",
|
|
26
27
|
"chalk": "^5.6.2",
|