paper-manager 0.11.0 → 0.11.2
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
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
A CLI tool for managing academic papers with knowledge base and vector search support.
|
|
7
7
|
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Semantic search** — FAISS vector indexing with configurable embedding models, query your papers by meaning rather than keywords
|
|
11
|
+
- **PDF metadata extraction** — automatically extracts title, author, keywords, DOI, and more from PDF files
|
|
12
|
+
- **DOI deduplication** — detects duplicate papers by DOI before adding, with `--force` override
|
|
13
|
+
- **Multi-format support** — import from PDF, TXT, MD, TEX, and other text-based formats
|
|
14
|
+
- **PDF-to-Markdown conversion** — optional high-quality conversion via [opendataloader-pdf](https://github.com/nicobailon/opendataloader-pdf) with image extraction
|
|
15
|
+
- **Dual-scope data model** — user-level (`~/.paper-manager/`) for global collections and project-level (`./.paper-manager/`) for project-specific papers, with automatic scope resolution
|
|
16
|
+
- **DOI-to-BibTeX** — convert DOI to BibTeX citation in one command
|
|
17
|
+
- **Machine-readable output** — `--json` and `--jq` flags on all read commands for scripting and automation
|
|
18
|
+
- **Literature notes** — attach key-value annotations to any paper
|
|
19
|
+
- **Local-first** — SQLite + FAISS + filesystem, no cloud dependencies
|
|
20
|
+
- **Agent skill** — installable as a [coding agent skill](https://github.com/vercel-labs/skills) for agent-driven paper management
|
|
21
|
+
|
|
8
22
|
## Installation
|
|
9
23
|
|
|
10
24
|
```bash
|
|
@@ -64,7 +78,7 @@ paper kb query <id> <query-text> [--json] [--jq <expr>] # Query a knowledge bas
|
|
|
64
78
|
### Literature (`paper lit`)
|
|
65
79
|
|
|
66
80
|
```bash
|
|
67
|
-
paper lit add <kb-id> <file-path>
|
|
81
|
+
paper lit add <kb-id> <file-path> [-f] # Add a literature (auto-extracts PDF metadata, rejects duplicate DOI)
|
|
68
82
|
paper lit remove <kb-id> <id> # Remove a literature
|
|
69
83
|
paper lit update <kb-id> <id> [opts] # Update literature metadata
|
|
70
84
|
paper lit list <kb-id> [--json] [--jq <expr>] # List literatures
|
|
@@ -36,6 +36,7 @@ export function createLiteratureCommand() {
|
|
|
36
36
|
.command("add <knowledge-base-id> <lit-path>")
|
|
37
37
|
.description("Add a literature from a file (PDF, TXT, MD, TEX, etc.)")
|
|
38
38
|
.option("-t, --title <title>", "Literature title")
|
|
39
|
+
.option("-f, --force", "Force add even if a literature with the same DOI already exists")
|
|
39
40
|
.action(async (kbId, litPath, options) => {
|
|
40
41
|
const resolved = resolveKnowledgeBase(kbId);
|
|
41
42
|
if (!resolved) {
|
|
@@ -77,6 +78,15 @@ export function createLiteratureCommand() {
|
|
|
77
78
|
log.step(`Creator: ${pdfMeta.creator}`);
|
|
78
79
|
}
|
|
79
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
|
+
}
|
|
80
90
|
const title = options.title ?? pdfMeta?.title ?? path.basename(litPath, path.extname(litPath));
|
|
81
91
|
// Create literature record
|
|
82
92
|
const literature = litOps.createLiterature({
|
package/dist/config/index.js
CHANGED
|
@@ -5,12 +5,45 @@ import * as z from "zod";
|
|
|
5
5
|
import { EmbeddingModelConfigSchema } from "../types/index.js";
|
|
6
6
|
// ─── Path Utilities ─────────────────────────────────────────
|
|
7
7
|
const USER_DATA_DIR = path.join(os.homedir(), ".paper-manager");
|
|
8
|
-
const
|
|
8
|
+
const DIR_NAME = ".paper-manager";
|
|
9
|
+
function findProjectDataDir() {
|
|
10
|
+
let dir = process.cwd();
|
|
11
|
+
while (true) {
|
|
12
|
+
const candidate = path.join(dir, DIR_NAME);
|
|
13
|
+
if (fs.existsSync(candidate))
|
|
14
|
+
return candidate;
|
|
15
|
+
const parent = path.dirname(dir);
|
|
16
|
+
if (parent === dir)
|
|
17
|
+
break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
// Fallback: CWD (no .paper-manager/ found up the tree)
|
|
21
|
+
return path.resolve(DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
let cachedProjectDataDir;
|
|
9
24
|
export function getUserDataDir() {
|
|
10
25
|
return USER_DATA_DIR;
|
|
11
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the project-level `.paper-manager/` directory path, traversing
|
|
29
|
+
* up from CWD. Falls back to CWD if not found. Result is cached per process.
|
|
30
|
+
*/
|
|
12
31
|
export function getProjectDataDir() {
|
|
13
|
-
|
|
32
|
+
if (cachedProjectDataDir === undefined) {
|
|
33
|
+
cachedProjectDataDir = findProjectDataDir();
|
|
34
|
+
}
|
|
35
|
+
return cachedProjectDataDir;
|
|
36
|
+
}
|
|
37
|
+
/** @internal Reset cached project data dir. For testing only. */
|
|
38
|
+
export function resetProjectDataDirCache() {
|
|
39
|
+
cachedProjectDataDir = undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns CWD-based `.paper-manager/` path without traversal.
|
|
43
|
+
* Used by `config init` to always create in the current directory.
|
|
44
|
+
*/
|
|
45
|
+
export function getProjectInitDir() {
|
|
46
|
+
return path.resolve(DIR_NAME);
|
|
14
47
|
}
|
|
15
48
|
export function getFilesDir(base) {
|
|
16
49
|
return path.join(base, "files");
|
|
@@ -22,7 +55,7 @@ function getUserConfigPath() {
|
|
|
22
55
|
return path.join(USER_DATA_DIR, "config.json");
|
|
23
56
|
}
|
|
24
57
|
function getProjectConfigPath() {
|
|
25
|
-
return path.join(
|
|
58
|
+
return path.join(getProjectDataDir(), "config.json");
|
|
26
59
|
}
|
|
27
60
|
// ─── Config Schema Map ─────────────────────────────────────
|
|
28
61
|
const configSchemas = {
|
package/dist/config/init.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { initializeDatabase, openDatabase } from "../db/index.js";
|
|
4
|
-
import { getFilesDir,
|
|
4
|
+
import { getFilesDir, getProjectInitDir, getUserDataDir, getVectorStoreDir, writeConfigFile, } from "./index.js";
|
|
5
5
|
export function initScope(options) {
|
|
6
|
-
const baseDir = options?.user ? getUserDataDir() :
|
|
6
|
+
const baseDir = options?.user ? getUserDataDir() : getProjectInitDir();
|
|
7
7
|
const items = [];
|
|
8
8
|
// 1. Base directory
|
|
9
9
|
if (fs.existsSync(baseDir)) {
|
|
@@ -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
|
}
|