wtf-p 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/README.md +18 -0
- package/bin/commands/install-logic.js +6 -10
- package/bin/lib/analyze-impact.js +105 -0
- package/bin/lib/bib-format.js +161 -0
- package/bin/lib/bib-index.js +104 -0
- package/bin/lib/citation-fetcher.js +299 -0
- package/bin/lib/citation-ranker.js +133 -0
- package/bin/lib/manifest.js +18 -0
- package/bin/lib/scholar-lookup.js +188 -0
- package/bin/lib/semantic-scholar.js +184 -0
- package/package.json +1 -1
- package/vendors/claude/agents/wtfp/citation-expert.md +45 -0
- package/vendors/claude/agents/wtfp/citation-formatter.md +42 -0
- package/vendors/claude/agents/wtfp/citation-retriever.md +31 -0
- package/vendors/claude/commands/wtfp/analyze-bib.md +41 -52
- package/vendors/claude/commands/wtfp/check-refs.md +11 -9
- package/vendors/claude/commands/wtfp/new-paper.md +42 -81
- package/vendors/claude/commands/wtfp/research-gap.md +3 -4
- package/vendors/claude/mcp/research-server/package.json +13 -0
- package/vendors/claude/mcp/research-server/src/index.js +133 -0
package/README.md
CHANGED
|
@@ -213,6 +213,24 @@ The mapping phase analyzes:
|
|
|
213
213
|
|
|
214
214
|
---
|
|
215
215
|
|
|
216
|
+
## Citation Expert (v0.4.0)
|
|
217
|
+
|
|
218
|
+
WTF-P includes a specialized tiered pipeline for bibliography management.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
/wtfp:analyze-bib # Deep analysis of impact (seminal vs rising)
|
|
222
|
+
/wtfp:research-gap # Intent-aware search (seminal/recent)
|
|
223
|
+
/wtfp:check-refs # Auto-suggest missing citations
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Capabilities:
|
|
227
|
+
- **Tiered Search:** Integrated Semantic Scholar, SerpAPI (Google Scholar), and CrossRef.
|
|
228
|
+
- **Impact Ranking:** Automatically scores papers by citations, velocity, and venue prestige.
|
|
229
|
+
- **Deduplication:** universal anchoring via DOI and Scholar Cluster IDs.
|
|
230
|
+
- **Provenance:** Entries track their metadata source and verification status.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
216
234
|
## Command Reference
|
|
217
235
|
|
|
218
236
|
### Setup
|
|
@@ -187,16 +187,12 @@ async function install(isGlobal, isUpdate, options, pkg) {
|
|
|
187
187
|
vendorConfig.components.forEach(component => {
|
|
188
188
|
// Check --only filters
|
|
189
189
|
if (onlyInstall !== 'all') {
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// Allow
|
|
197
|
-
} else {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
190
|
+
const allowedIds = [onlyInstall];
|
|
191
|
+
// Legacy mapping: 'commands' includes 'skills'
|
|
192
|
+
if (onlyInstall === 'commands') allowedIds.push('skills');
|
|
193
|
+
|
|
194
|
+
if (!allowedIds.includes(component.id)) {
|
|
195
|
+
return;
|
|
200
196
|
}
|
|
201
197
|
}
|
|
202
198
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const s2 = require('./semantic-scholar');
|
|
3
|
+
const ranker = require('./citation-ranker');
|
|
4
|
+
const bibIndex = require('./bib-index'); // Assuming this exists or I need to use the one in bin/lib if available
|
|
5
|
+
|
|
6
|
+
// Check if bib-index exists in bin/lib or I need to implement basic parsing
|
|
7
|
+
// The command says `node ~/.claude/bin/bib-index.js`.
|
|
8
|
+
// In this project structure, it is likely `bin/lib/bib-index.js`.
|
|
9
|
+
|
|
10
|
+
const BIB_INDEX_PATH = './bib-index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Impact Analyzer
|
|
14
|
+
*
|
|
15
|
+
* Analyzes a BibTeX file or JSON index:
|
|
16
|
+
* 1. Fetches metrics for each paper (via S2)
|
|
17
|
+
* 2. Categorizes them (Seminal, Rising, Outdated)
|
|
18
|
+
* 3. Returns summary
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
async function analyze(filePath) {
|
|
22
|
+
// 1. Index/Parse
|
|
23
|
+
let entries = [];
|
|
24
|
+
try {
|
|
25
|
+
// We'll require bib-index locally if possible
|
|
26
|
+
const indexer = require(BIB_INDEX_PATH);
|
|
27
|
+
const json = indexer.index(fs.readFileSync(filePath, 'utf8'));
|
|
28
|
+
entries = JSON.parse(json);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// Fallback: simple regex parsing if bib-index lib isn't easily require-able
|
|
31
|
+
console.error("Could not use bib-index, falling back to simple parse", e);
|
|
32
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
33
|
+
const regex = /@\w+\s*{\s*([^,]+),[\s\S]*?title\s*=\s*[{"'](.+?)[}"'][\s\S]*?year\s*=\s*[{"'](\d{4})[}"']/gi;
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = regex.exec(content)) !== null) {
|
|
36
|
+
entries.push({ key: match[1], title: match[2], year: parseInt(match[3]) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = {
|
|
41
|
+
seminal: [], // > 1000 citations
|
|
42
|
+
rising: [], // Velocity > 50/mo, < 1000 total
|
|
43
|
+
outdated: [], // > 10 years, < 50 citations
|
|
44
|
+
unknown: []
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Process in batches to avoid rate limits
|
|
48
|
+
const BATCH_SIZE = 5;
|
|
49
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
50
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
51
|
+
await Promise.all(batch.map(async (entry) => {
|
|
52
|
+
try {
|
|
53
|
+
// Search by title to get metrics (most reliable if DOI missing)
|
|
54
|
+
// Ideally we'd use DOI if we parsed it.
|
|
55
|
+
const papers = await s2.search(entry.title, { limit: 1 });
|
|
56
|
+
if (papers && papers.length > 0) {
|
|
57
|
+
const p = papers[0];
|
|
58
|
+
const velocity = ranker.calculateVelocity(p.citationCount, p.year);
|
|
59
|
+
const age = new Date().getFullYear() - (p.year || new Date().getFullYear());
|
|
60
|
+
|
|
61
|
+
const enriched = {
|
|
62
|
+
key: entry.key,
|
|
63
|
+
title: p.title,
|
|
64
|
+
year: p.year,
|
|
65
|
+
citations: p.citationCount,
|
|
66
|
+
velocity: velocity.toFixed(1)
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (p.citationCount > 1000) {
|
|
70
|
+
results.seminal.push(enriched);
|
|
71
|
+
} else if (velocity > 10 && p.citationCount < 1000) { // Threshold adjusted: 50/mo is very high. 10/mo is solid.
|
|
72
|
+
results.rising.push(enriched);
|
|
73
|
+
} else if (age > 15 && p.citationCount < 50) {
|
|
74
|
+
results.outdated.push({ ...enriched, concern: `${age} years old, low citations` });
|
|
75
|
+
} else {
|
|
76
|
+
results.unknown.push(enriched);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
results.unknown.push({ key: entry.key, title: entry.title, error: "Not found" });
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
results.unknown.push({ key: entry.key, title: entry.title, error: e.message });
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
// Small delay between batches
|
|
86
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sort
|
|
90
|
+
results.seminal.sort((a,b) => b.citations - a.citations);
|
|
91
|
+
results.rising.sort((a,b) => b.velocity - a.velocity);
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (require.main === module) {
|
|
97
|
+
const file = process.argv[2];
|
|
98
|
+
if (!file) {
|
|
99
|
+
console.error("Usage: node analyze-impact.js <bibfile>");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
analyze(file).then(r => console.log(JSON.stringify(r, null, 2)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { analyze };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WTF-P BibTeX Formatter
|
|
5
|
+
* Standardizes BibTeX entries to the project's strict template.
|
|
6
|
+
* Handles missing fields and assigns provenance metadata.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// --- Parsing Logic ---
|
|
10
|
+
|
|
11
|
+
function parseField(text, field) {
|
|
12
|
+
// Regex to find field="value" or field={value}
|
|
13
|
+
const regex = new RegExp(`\\b${field}\\s*=\\s*[{"']([\\s\\S]*?)[}"']\\s*[,}]`, 'i');
|
|
14
|
+
const match = text.match(regex);
|
|
15
|
+
return match ? match[1].trim() : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseEntryType(text) {
|
|
19
|
+
const match = text.match(/@(\w+)\s*{/);
|
|
20
|
+
return match ? match[1].toLowerCase() : 'misc';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseKey(text) {
|
|
24
|
+
const match = text.match(/@\w+\s*{\s*([^,]+),/);
|
|
25
|
+
return match ? match[1].trim() : 'unknown_key';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parse(rawEntry) {
|
|
29
|
+
const entryType = parseEntryType(rawEntry);
|
|
30
|
+
const key = parseKey(rawEntry);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
key,
|
|
34
|
+
entryType,
|
|
35
|
+
author: parseField(rawEntry, 'author'),
|
|
36
|
+
title: parseField(rawEntry, 'title'),
|
|
37
|
+
booktitle: parseField(rawEntry, 'booktitle') || parseField(rawEntry, 'journal'),
|
|
38
|
+
year: parseField(rawEntry, 'year'),
|
|
39
|
+
month: parseField(rawEntry, 'month'),
|
|
40
|
+
abstract: parseField(rawEntry, 'abstract'),
|
|
41
|
+
publisher: parseField(rawEntry, 'publisher'),
|
|
42
|
+
volume: parseField(rawEntry, 'volume'),
|
|
43
|
+
number: parseField(rawEntry, 'number'),
|
|
44
|
+
pages: parseField(rawEntry, 'pages'),
|
|
45
|
+
doi: parseField(rawEntry, 'doi'),
|
|
46
|
+
url: parseField(rawEntry, 'url'),
|
|
47
|
+
keywords: parseField(rawEntry, 'keywords'),
|
|
48
|
+
abbr: parseField(rawEntry, 'abbr'),
|
|
49
|
+
bibtex_show: parseField(rawEntry, 'bibtex_show'),
|
|
50
|
+
selected: parseField(rawEntry, 'selected'),
|
|
51
|
+
projects: parseField(rawEntry, 'projects')
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Formatting Logic ---
|
|
56
|
+
|
|
57
|
+
function format(data, provenance = {}) {
|
|
58
|
+
// Determine Status
|
|
59
|
+
let status = provenance.wtfp_status || 'official';
|
|
60
|
+
const missingFields = [];
|
|
61
|
+
|
|
62
|
+
if (!data.doi) {
|
|
63
|
+
status = 'incomplete';
|
|
64
|
+
missingFields.push('doi');
|
|
65
|
+
}
|
|
66
|
+
if (!data.author || data.author === '{MISSING_AUTHOR}') {
|
|
67
|
+
status = 'incomplete';
|
|
68
|
+
missingFields.push('author');
|
|
69
|
+
}
|
|
70
|
+
if (!data.booktitle || data.booktitle === '{MISSING_VENUE}') {
|
|
71
|
+
missingFields.push('venue');
|
|
72
|
+
}
|
|
73
|
+
if (!data.abstract) {
|
|
74
|
+
if (status !== 'incomplete') status = 'partial';
|
|
75
|
+
missingFields.push('abstract');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Use provided provenance or defaults
|
|
79
|
+
const wtfp_source = provenance.wtfp_source || '';
|
|
80
|
+
const wtfp_citations = provenance.wtfp_citations || '';
|
|
81
|
+
const wtfp_velocity = provenance.wtfp_velocity || '';
|
|
82
|
+
const wtfp_s2_id = provenance.wtfp_s2_id || '';
|
|
83
|
+
const wtfp_scholar_id = provenance.wtfp_scholar_id || '';
|
|
84
|
+
const wtfp_fetched = provenance.wtfp_fetched || new Date().toISOString().split('T')[0];
|
|
85
|
+
|
|
86
|
+
const entryType = data.entryType === 'inproceedings' ? 'conference' : (data.entryType === 'article' ? 'journal' : data.entryType || 'misc');
|
|
87
|
+
|
|
88
|
+
// Clean fields
|
|
89
|
+
const clean = (val, fallback = "") => val || fallback;
|
|
90
|
+
|
|
91
|
+
return `@${entryType}{${data.key || 'unknown'},
|
|
92
|
+
abbr="${clean(data.abbr)}",
|
|
93
|
+
entry_type="${entryType}",
|
|
94
|
+
author="${clean(data.author, "{MISSING_AUTHOR}")}",
|
|
95
|
+
abstract="${clean(data.abstract)}",
|
|
96
|
+
booktitle="${clean(data.booktitle, "{MISSING_VENUE}")}",
|
|
97
|
+
title="${clean(data.title, "{MISSING_TITLE}")}",
|
|
98
|
+
year="${clean(data.year, "{????}")}",
|
|
99
|
+
month="${clean(data.month)}",
|
|
100
|
+
publisher="${clean(data.publisher)}",
|
|
101
|
+
volume="${clean(data.volume)}",
|
|
102
|
+
number="${clean(data.number)}",
|
|
103
|
+
pages="${clean(data.pages)}",
|
|
104
|
+
keywords="${clean(data.keywords)}",
|
|
105
|
+
doi="${clean(data.doi)}",
|
|
106
|
+
url="${clean(data.url, data.doi ? `https://doi.org/${data.doi}` : "")}",
|
|
107
|
+
html="${clean(data.html, data.doi ? `https://doi.org/${data.doi}` : "")}",
|
|
108
|
+
pdf="${clean(data.pdf, "paper.pdf")}",
|
|
109
|
+
google_scholar_id="${clean(data.google_scholar_id)}",
|
|
110
|
+
additional_info="${clean(data.additional_info)}",
|
|
111
|
+
bibtex_show="${clean(data.bibtex_show, "true")}",
|
|
112
|
+
selected="${clean(data.selected, "false")}",
|
|
113
|
+
projects="${clean(data.projects)}",
|
|
114
|
+
wtfp_status="${status}",
|
|
115
|
+
wtfp_source="${wtfp_source}",
|
|
116
|
+
wtfp_citations="${wtfp_citations}",
|
|
117
|
+
wtfp_velocity="${wtfp_velocity}",
|
|
118
|
+
wtfp_s2_id="${wtfp_s2_id}",
|
|
119
|
+
wtfp_scholar_id="${wtfp_scholar_id}",
|
|
120
|
+
wtfp_fetched="${wtfp_fetched}",
|
|
121
|
+
wtfp_missing="${missingFields.join(',')}"
|
|
122
|
+
}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- CLI Handling ---
|
|
126
|
+
|
|
127
|
+
if (require.main === module) {
|
|
128
|
+
const MODE = process.argv[2];
|
|
129
|
+
const INPUT = process.argv[3];
|
|
130
|
+
const KEY_ARG = process.argv[4];
|
|
131
|
+
|
|
132
|
+
if (!MODE) {
|
|
133
|
+
console.error('Usage: node bib-format.js <raw_string|--file> <content|filepath> [key]');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let rawEntry = '';
|
|
138
|
+
|
|
139
|
+
if (MODE === '--file') {
|
|
140
|
+
if (!fs.existsSync(INPUT)) {
|
|
141
|
+
console.error(`File not found: ${INPUT}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const content = fs.readFileSync(INPUT, 'utf8');
|
|
145
|
+
const regex = new RegExp(`@\w+\s*{\s*${KEY_ARG}\s*,[\s\S]*?\n}`, 'm');
|
|
146
|
+
const match = content.match(regex);
|
|
147
|
+
if (match) {
|
|
148
|
+
rawEntry = match[0];
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`Key ${KEY_ARG} not found in ${INPUT}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
rawEntry = INPUT;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parsed = parse(rawEntry);
|
|
158
|
+
console.log(format(parsed));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { parse, format };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WTF-P Bibliography Indexer
|
|
5
|
+
* Indexes and retrieves BibTeX entries without loading the entire file into context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ENTRY_REGEX = /@(\w+)\s*\{\s*([^,]+),([^@]*)\}/g;
|
|
9
|
+
|
|
10
|
+
function parseEntries(text) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
let match;
|
|
13
|
+
// Reset regex index just in case
|
|
14
|
+
ENTRY_REGEX.lastIndex = 0;
|
|
15
|
+
|
|
16
|
+
while ((match = ENTRY_REGEX.exec(text)) !== null) {
|
|
17
|
+
const fullText = match[0];
|
|
18
|
+
const type = match[1];
|
|
19
|
+
const key = match[2].trim();
|
|
20
|
+
const body = match[3];
|
|
21
|
+
|
|
22
|
+
// Extract title specifically for indexing
|
|
23
|
+
const titleMatch = body.match(/title\s*=\s*[{"'](.+?)[}"']/i);
|
|
24
|
+
const title = titleMatch ? titleMatch[1] : 'Unknown Title';
|
|
25
|
+
|
|
26
|
+
// Extract year
|
|
27
|
+
const yearMatch = body.match(/year\s*=\s*[{"']?(\d+)[}"']?/i);
|
|
28
|
+
const year = yearMatch ? yearMatch[1] : '????';
|
|
29
|
+
|
|
30
|
+
entries.push({ key, type, title, year, fullText: `@${type}{${key},${body}}` });
|
|
31
|
+
}
|
|
32
|
+
return entries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function index(content) {
|
|
36
|
+
const entries = parseEntries(content);
|
|
37
|
+
return JSON.stringify(entries.map(e => ({ key: e.key, title: e.title, year: e.year })), null, 2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getEntry(content, key) {
|
|
41
|
+
const entries = parseEntries(content);
|
|
42
|
+
const entry = entries.find(e => e.key === key);
|
|
43
|
+
return entry ? entry.fullText : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function search(content, query) {
|
|
47
|
+
const entries = parseEntries(content);
|
|
48
|
+
const q = query.toLowerCase();
|
|
49
|
+
const results = entries.filter(e =>
|
|
50
|
+
e.title.toLowerCase().includes(q) ||
|
|
51
|
+
e.key.toLowerCase().includes(q) ||
|
|
52
|
+
e.fullText.toLowerCase().includes(q)
|
|
53
|
+
);
|
|
54
|
+
return JSON.stringify(results.map(e => ({ key: e.key, title: e.title, year: e.year })), null, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- CLI Handling ---
|
|
58
|
+
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
const COMMAND = process.argv[2];
|
|
61
|
+
const BIB_FILE = process.argv[3];
|
|
62
|
+
const ARG = process.argv[4];
|
|
63
|
+
|
|
64
|
+
if (!COMMAND || !BIB_FILE) {
|
|
65
|
+
console.error('Usage: node bib-index.js <command> <bib_file> [arg]');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(BIB_FILE)) {
|
|
70
|
+
console.error(`Error: Bibliography file not found: ${BIB_FILE}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = fs.readFileSync(BIB_FILE, 'utf8');
|
|
75
|
+
|
|
76
|
+
if (COMMAND === 'index') {
|
|
77
|
+
console.log(index(content));
|
|
78
|
+
}
|
|
79
|
+
else if (COMMAND === 'get') {
|
|
80
|
+
if (!ARG) {
|
|
81
|
+
console.error('Error: Missing citation key');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const result = getEntry(content, ARG);
|
|
85
|
+
if (result) console.log(result);
|
|
86
|
+
else {
|
|
87
|
+
console.error(`Error: Entry '${ARG}' not found.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (COMMAND === 'search') {
|
|
92
|
+
if (!ARG) {
|
|
93
|
+
console.error('Error: Missing search query');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
console.log(search(content, ARG));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error(`Unknown command: ${COMMAND}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { index, getEntry, search, parseEntries };
|