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 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] # 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> # Query a knowledge base
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> # Add a literature (auto-extracts PDF metadata)
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> # List literatures
71
- paper lit search <kb-id> [opts] # Search literatures by metadata (--title/--author/--keyword/--doi)
72
- paper lit show <kb-id> <id> # Show literature details
73
- paper lit note list <lit-id> # List notes
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
- log.plain("[]");
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
- log.plain(JSON.stringify(results, null, 2));
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
- log.plain("[]");
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
- log.plain(JSON.stringify(output, null, 2));
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
- log.plain("[]");
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
- log.plain(JSON.stringify(literatures, null, 2));
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
- log.plain("[]");
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
- log.plain(JSON.stringify(results, null, 2));
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
- log.plain(JSON.stringify(literature, null, 2));
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
- log.plain(JSON.stringify(literature.notes, null, 2));
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
+ }
@@ -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
- log.plain(JSON.stringify(meta, null, 2));
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.10.4",
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",