wtf-p 0.2.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 +24 -2
- package/bin/commands/doctor.js +100 -0
- package/bin/commands/install-logic.js +231 -0
- package/bin/commands/list.js +88 -0
- package/bin/commands/status.js +85 -0
- package/bin/commands/update.js +50 -0
- package/bin/install.js +78 -629
- 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 +59 -0
- package/bin/lib/scholar-lookup.js +188 -0
- package/bin/lib/semantic-scholar.js +184 -0
- package/bin/lib/utils.js +37 -17
- package/bin/lib/wcn-compiler.js +226 -0
- package/bin/uninstall.js +51 -17
- package/core/write-the-f-paper/templates/posters/basic.md +34 -0
- package/core/write-the-f-paper/templates/slides/basic.md +31 -0
- package/package.json +12 -4
- package/vendors/claude/.claude-plugin/plugin.json +12 -0
- 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/{commands → vendors/claude/commands}/wtfp/analyze-bib.md +41 -52
- package/{commands → vendors/claude/commands}/wtfp/check-refs.md +11 -9
- package/vendors/claude/commands/wtfp/create-poster.md +42 -0
- package/vendors/claude/commands/wtfp/create-slides.md +41 -0
- package/{commands → vendors/claude/commands}/wtfp/help.md +3 -0
- package/{commands → vendors/claude/commands}/wtfp/new-paper.md +42 -81
- package/{commands → 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/vendors/claude/skills/wtfp/echarts/SKILL.md +47 -0
- package/vendors/claude/skills/wtfp/marp/SKILL.md +49 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/checkpoints.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/continuation-format.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/git-integration.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/github-contrib.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/imrad-structure.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/length-estimation.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/plan-format.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/principles.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/questioning.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/references/research-pitfalls.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/config.json +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/context.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/continue-here.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/data-inventory.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/discovery.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/issues.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/manuscript.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/narrative-arc.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/outline.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/phase-prompt.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/prior-drafts.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/argument-structure.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/bibliography.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/gaps.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/imrad.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/peer-review.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/style-guide.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/tools.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/project.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/research.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/review-comments.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/revision-issues.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/roadmap.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-archive.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-context.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/state.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/templates/summary.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/acm-cs.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/arxiv-ml.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/ieee-cs.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/nature.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/venues/thesis-chapter.yaml +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.wcn.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.md +0 -0
- /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.wcn.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/contribute.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/create-outline.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/discuss-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/export-latex.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/insert-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/list-assumptions.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/map-project.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/pause-writing.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/plan-revision.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/plan-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/polish-prose.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/progress.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/remove-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/report-bug.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/request-feature.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/resume-writing.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/review-section.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/submit-milestone.md +0 -0
- /package/{commands → vendors/claude/commands}/wtfp/write-section.md +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation Ranker
|
|
3
|
+
*
|
|
4
|
+
* Ranks papers based on multidimensional impact score:
|
|
5
|
+
* - Citation Count (Log scaled)
|
|
6
|
+
* - Velocity (Citations per month)
|
|
7
|
+
* - Recency (Decay over time)
|
|
8
|
+
* - Venue (Tiered scoring)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Venue Tiers
|
|
12
|
+
const VENUE_TIERS = {
|
|
13
|
+
TIER_1: [
|
|
14
|
+
// AI/ML
|
|
15
|
+
"neurips", "nips", "icml", "iclr", "cvpr", "iccv", "eccv", "acl", "emnlp", "naacl",
|
|
16
|
+
"aaai", "ijcai", "kdd", "www", "sigir", "chi",
|
|
17
|
+
// Systems
|
|
18
|
+
"osdi", "sosp", "nsdi", "eurosys", "atc", "fast", "sigcomm", "mobicom",
|
|
19
|
+
// Journals
|
|
20
|
+
"nature", "science", "pnas", "cell", "lancet", "nejm", "jama",
|
|
21
|
+
"ieee transactions", "acm computing surveys", "jmlr", "journal of machine learning research"
|
|
22
|
+
],
|
|
23
|
+
TIER_2: [
|
|
24
|
+
"coling", "wsdm", "cikm", "pakdd", "ijcnn", "icra", "iros",
|
|
25
|
+
"middleware", "cloud", "sc", "hpdc", "cluster"
|
|
26
|
+
],
|
|
27
|
+
PREPRINT: [
|
|
28
|
+
"arxiv", "biorxiv", "medrxiv", "ssrn", "workshop"
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Score a venue name
|
|
34
|
+
* @param {string} venue
|
|
35
|
+
* @returns {number} 0.0 to 1.0
|
|
36
|
+
*/
|
|
37
|
+
function scoreVenue(venue) {
|
|
38
|
+
if (!venue) return 0.2;
|
|
39
|
+
const v = venue.toLowerCase();
|
|
40
|
+
|
|
41
|
+
if (VENUE_TIERS.TIER_1.some(t => v.includes(t))) return 1.0;
|
|
42
|
+
if (VENUE_TIERS.TIER_2.some(t => v.includes(t))) return 0.7;
|
|
43
|
+
if (VENUE_TIERS.PREPRINT.some(t => v.includes(t))) return 0.3;
|
|
44
|
+
|
|
45
|
+
return 0.5; // Unknown venue default
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate citation velocity
|
|
50
|
+
* @param {number} citations
|
|
51
|
+
* @param {number} year
|
|
52
|
+
* @returns {number} Citations per month
|
|
53
|
+
*/
|
|
54
|
+
function calculateVelocity(citations, year) {
|
|
55
|
+
if (!citations || !year) return 0;
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const currentYear = now.getFullYear();
|
|
58
|
+
const currentMonth = now.getMonth(); // 0-11
|
|
59
|
+
|
|
60
|
+
// Calculate months since publication (assuming Jan 1st of year)
|
|
61
|
+
// If current year, use months passed. If past year, (diff * 12) + currentMonth.
|
|
62
|
+
let monthsSince = (currentYear - year) * 12 + currentMonth;
|
|
63
|
+
|
|
64
|
+
// Guard against divide by zero or negative (future dates)
|
|
65
|
+
monthsSince = Math.max(1, monthsSince);
|
|
66
|
+
|
|
67
|
+
return citations / monthsSince;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Calculate impact score for a paper
|
|
72
|
+
* @param {Object} paper
|
|
73
|
+
* @param {string} intent "seminal" | "recent" | "specific" | "balanced"
|
|
74
|
+
*/
|
|
75
|
+
function calculateScore(paper, intent = "balanced") {
|
|
76
|
+
const now = new Date().getFullYear();
|
|
77
|
+
const year = paper.year || now;
|
|
78
|
+
const age = Math.max(0, now - year);
|
|
79
|
+
const citations = paper.citationCount || 0;
|
|
80
|
+
const velocity = calculateVelocity(citations, year);
|
|
81
|
+
|
|
82
|
+
// Normalize factors (approximate ranges)
|
|
83
|
+
// Citations: log10(100,000) = 5. So score is 0-1.
|
|
84
|
+
const citationScore = Math.min(1, Math.log10(citations + 1) / 5);
|
|
85
|
+
|
|
86
|
+
// Velocity: 100 cites/month is huge. Normalize to 0-1.
|
|
87
|
+
const velocityScore = Math.min(1, velocity / 100);
|
|
88
|
+
|
|
89
|
+
// Recency: Linear decay over 10 years. 0 if > 10 years old.
|
|
90
|
+
const recencyScore = Math.max(0, 1 - (age / 10));
|
|
91
|
+
|
|
92
|
+
const venueScore = scoreVenue(paper.venue);
|
|
93
|
+
|
|
94
|
+
// Weights based on intent
|
|
95
|
+
const weights = {
|
|
96
|
+
seminal: { citation: 0.6, velocity: 0.2, recency: 0.1, venue: 0.1 },
|
|
97
|
+
recent: { citation: 0.2, velocity: 0.3, recency: 0.4, venue: 0.1 },
|
|
98
|
+
specific: { citation: 0.1, velocity: 0.1, recency: 0.1, venue: 0.1 }, // Ranking less important for specific lookup
|
|
99
|
+
balanced: { citation: 0.4, velocity: 0.3, recency: 0.2, venue: 0.1 }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const w = weights[intent] || weights.balanced;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
citationScore * w.citation +
|
|
106
|
+
velocityScore * w.velocity +
|
|
107
|
+
recencyScore * w.recency +
|
|
108
|
+
venueScore * w.venue
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Rank a list of papers
|
|
114
|
+
* @param {Array} papers
|
|
115
|
+
* @param {string} intent
|
|
116
|
+
* @returns {Array} Sorted papers with score attached
|
|
117
|
+
*/
|
|
118
|
+
function rank(papers, intent = "balanced") {
|
|
119
|
+
const scored = papers.map(p => ({
|
|
120
|
+
...p,
|
|
121
|
+
wtfp_score: calculateScore(p, intent),
|
|
122
|
+
wtfp_velocity: calculateVelocity(p.citationCount, p.year)
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
return scored.sort((a, b) => b.wtfp_score - a.wtfp_score);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
rank,
|
|
130
|
+
calculateScore,
|
|
131
|
+
calculateVelocity,
|
|
132
|
+
scoreVenue
|
|
133
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
4
|
+
|
|
5
|
+
const MANIFEST = {
|
|
6
|
+
claude: {
|
|
7
|
+
name: 'Claude Code',
|
|
8
|
+
configDirEnv: 'CLAUDE_CONFIG_DIR',
|
|
9
|
+
defaultDir: '.claude',
|
|
10
|
+
components: [
|
|
11
|
+
{
|
|
12
|
+
id: 'workflows',
|
|
13
|
+
src: path.join(ROOT, 'core', 'write-the-f-paper'),
|
|
14
|
+
dest: 'write-the-f-paper',
|
|
15
|
+
type: 'dir'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'commands',
|
|
19
|
+
src: path.join(ROOT, 'vendors', 'claude', 'commands', 'wtfp'),
|
|
20
|
+
dest: 'commands/wtfp',
|
|
21
|
+
type: 'dir'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'skills',
|
|
25
|
+
src: path.join(ROOT, 'vendors', 'claude', 'skills', 'wtfp'),
|
|
26
|
+
dest: 'skills/wtfp',
|
|
27
|
+
type: 'dir'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'agents',
|
|
31
|
+
src: path.join(ROOT, 'vendors', 'claude', 'agents', 'wtfp'),
|
|
32
|
+
dest: 'agents/wtfp',
|
|
33
|
+
type: 'dir'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'mcp',
|
|
37
|
+
src: path.join(ROOT, 'vendors', 'claude', 'mcp'),
|
|
38
|
+
dest: 'mcp',
|
|
39
|
+
type: 'dir'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'scripts',
|
|
43
|
+
src: path.join(ROOT, 'bin', 'lib'),
|
|
44
|
+
dest: 'bin',
|
|
45
|
+
type: 'dir'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'plugin',
|
|
49
|
+
src: path.join(ROOT, 'vendors', 'claude', '.claude-plugin'),
|
|
50
|
+
dest: '.claude-plugin',
|
|
51
|
+
type: 'dir'
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
// Future:
|
|
56
|
+
// gemini: { ... }
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = MANIFEST;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const querystring = require('querystring');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SerpAPI Google Scholar Wrapper
|
|
6
|
+
*
|
|
7
|
+
* Provides access to Google Scholar data via SerpAPI.
|
|
8
|
+
* Used for high-value "seminal" queries to get citation velocity and cluster IDs.
|
|
9
|
+
*
|
|
10
|
+
* Env:
|
|
11
|
+
* - SERPAPI_KEY: Required for operation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const HOST = 'serpapi.com';
|
|
15
|
+
const PATH = '/search';
|
|
16
|
+
|
|
17
|
+
const usage = {
|
|
18
|
+
queries: 0,
|
|
19
|
+
// Default budget: 100 queries/month (~$5)
|
|
20
|
+
monthlyBudget: 100,
|
|
21
|
+
|
|
22
|
+
canQuery() {
|
|
23
|
+
return this.queries < this.monthlyBudget;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
recordQuery() {
|
|
27
|
+
this.queries++;
|
|
28
|
+
if (this.queries % 10 === 0) {
|
|
29
|
+
// console.error(`[Scholar] API usage: ${this.queries}/${this.monthlyBudget}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function getApiKey() {
|
|
35
|
+
return process.env.SERPAPI_KEY;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isAvailable() {
|
|
39
|
+
return !!getApiKey();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function request(params) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const key = getApiKey();
|
|
45
|
+
if (!key) {
|
|
46
|
+
return reject(new Error('SERPAPI_KEY not set'));
|
|
47
|
+
}
|
|
48
|
+
if (!usage.canQuery()) {
|
|
49
|
+
return reject(new Error('Monthly budget exceeded'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
usage.recordQuery();
|
|
53
|
+
|
|
54
|
+
const q = querystring.stringify({
|
|
55
|
+
...params,
|
|
56
|
+
api_key: key,
|
|
57
|
+
engine: 'google_scholar'
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const options = {
|
|
61
|
+
hostname: HOST,
|
|
62
|
+
path: `${PATH}?${q}`,
|
|
63
|
+
method: 'GET'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const req = https.request(options, (res) => {
|
|
67
|
+
let data = '';
|
|
68
|
+
res.on('data', c => data += c);
|
|
69
|
+
res.on('end', () => {
|
|
70
|
+
if (res.statusCode !== 200) {
|
|
71
|
+
return reject(new Error(`SerpAPI Error: ${res.statusCode} ${data}`));
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
resolve(JSON.parse(data));
|
|
75
|
+
} catch (e) {
|
|
76
|
+
reject(e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
req.on('error', reject);
|
|
82
|
+
req.end();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Search Google Scholar
|
|
88
|
+
* @param {string} query
|
|
89
|
+
* @param {Object} options { yearLow, yearHigh, limit }
|
|
90
|
+
*/
|
|
91
|
+
async function search(query, options = {}) {
|
|
92
|
+
const params = {
|
|
93
|
+
q: query,
|
|
94
|
+
num: options.limit || 10
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (options.yearLow) params.as_ylo = options.yearLow;
|
|
98
|
+
if (options.yearHigh) params.as_yhi = options.yearHigh;
|
|
99
|
+
|
|
100
|
+
const data = await request(params);
|
|
101
|
+
return (data.organic_results || []).map(mapResult);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get paper by Cluster ID
|
|
106
|
+
* @param {string} clusterId
|
|
107
|
+
*/
|
|
108
|
+
async function getByClusterId(clusterId) {
|
|
109
|
+
const data = await request({ cluster: clusterId });
|
|
110
|
+
// When searching by cluster, it returns list of versions.
|
|
111
|
+
// We usually want the first one or metadata about the cluster.
|
|
112
|
+
// SerpAPI "cluster" usually returns "organic_results" which are the versions.
|
|
113
|
+
const results = data.organic_results || [];
|
|
114
|
+
if (results.length > 0) return mapResult(results[0]);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get citing papers
|
|
120
|
+
* @param {string} clusterId
|
|
121
|
+
* @param {number} limit
|
|
122
|
+
*/
|
|
123
|
+
async function getCitingPapers(clusterId, limit = 10) {
|
|
124
|
+
const data = await request({
|
|
125
|
+
cites: clusterId,
|
|
126
|
+
num: limit
|
|
127
|
+
});
|
|
128
|
+
return (data.organic_results || []).map(mapResult);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mapResult(item) {
|
|
132
|
+
// Extract cluster ID
|
|
133
|
+
let clusterId = null;
|
|
134
|
+
// Try to find it in links or specific fields if SerpAPI provides it directly
|
|
135
|
+
// SerpAPI usually puts it in inline_links -> cited_by -> serpapi_link (cites=ID)
|
|
136
|
+
// or versions -> serpapi_link (cluster=ID)
|
|
137
|
+
|
|
138
|
+
if (item.inline_links && item.inline_links.cited_by && item.inline_links.cited_by.serpapi_link) {
|
|
139
|
+
const match = item.inline_links.cited_by.serpapi_link.match(/cites=(\w+)/);
|
|
140
|
+
if (match) clusterId = match[1];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
source: 'google_scholar',
|
|
145
|
+
title: item.title,
|
|
146
|
+
clusterId: clusterId || item.cluster_id, // Sometimes provided directly
|
|
147
|
+
link: item.link,
|
|
148
|
+
snippet: item.snippet,
|
|
149
|
+
publication_info: item.publication_info,
|
|
150
|
+
citationCount: item.inline_links && item.inline_links.cited_by ? item.inline_links.cited_by.total : 0,
|
|
151
|
+
year: extractYear(item.publication_info ? item.publication_info.summary : ''),
|
|
152
|
+
authors: extractAuthors(item.publication_info ? item.publication_info.summary : ''),
|
|
153
|
+
venue: extractVenue(item.publication_info ? item.publication_info.summary : '')
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractYear(summary) {
|
|
158
|
+
const match = summary.match(/\b(19|20)\d{2}\b/);
|
|
159
|
+
return match ? parseInt(match[0]) : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractAuthors(summary) {
|
|
163
|
+
// "A Author, B Author - Venue, 2020 - publisher"
|
|
164
|
+
if (!summary) return [];
|
|
165
|
+
const parts = summary.split('-');
|
|
166
|
+
if (parts.length > 0) {
|
|
167
|
+
return parts[0].split(',').map(s => ({ name: s.trim() }));
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function extractVenue(summary) {
|
|
173
|
+
if (!summary) return null;
|
|
174
|
+
const parts = summary.split('-');
|
|
175
|
+
if (parts.length > 1) {
|
|
176
|
+
// "Venue, Year" usually
|
|
177
|
+
return parts[1].split(',')[0].trim();
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
isAvailable,
|
|
184
|
+
search,
|
|
185
|
+
getByClusterId,
|
|
186
|
+
getCitingPapers,
|
|
187
|
+
usage
|
|
188
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const querystring = require('querystring');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semantic Scholar API Wrapper
|
|
6
|
+
*
|
|
7
|
+
* Provides search and lookup capabilities for Semantic Scholar Graph API v1.
|
|
8
|
+
* Handles rate limiting with exponential backoff.
|
|
9
|
+
*
|
|
10
|
+
* Env:
|
|
11
|
+
* - S2_API_KEY: Optional API key for higher rate limits.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const API_KEY = process.env.S2_API_KEY;
|
|
15
|
+
const BASE_HOST = 'api.semanticscholar.org';
|
|
16
|
+
const BASE_PATH = '/graph/v1';
|
|
17
|
+
|
|
18
|
+
const RETRY_CONFIG = {
|
|
19
|
+
maxRetries: 5,
|
|
20
|
+
baseDelay: 1000,
|
|
21
|
+
maxDelay: 16000
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class SemanticScholarError extends Error {
|
|
25
|
+
constructor(message, statusCode, retryAfter) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'SemanticScholarError';
|
|
28
|
+
this.statusCode = statusCode;
|
|
29
|
+
this.retryAfter = retryAfter;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sleep helper
|
|
35
|
+
*/
|
|
36
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Make an HTTP request with retry logic
|
|
40
|
+
*/
|
|
41
|
+
async function request(endpoint, params = {}) {
|
|
42
|
+
const query = querystring.stringify(params);
|
|
43
|
+
const path = `${BASE_PATH}${endpoint}?${query}`;
|
|
44
|
+
|
|
45
|
+
let attempt = 0;
|
|
46
|
+
|
|
47
|
+
while (attempt <= RETRY_CONFIG.maxRetries) {
|
|
48
|
+
try {
|
|
49
|
+
return await doRequest(path);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof SemanticScholarError && error.statusCode === 429) {
|
|
52
|
+
attempt++;
|
|
53
|
+
if (attempt > RETRY_CONFIG.maxRetries) throw error;
|
|
54
|
+
|
|
55
|
+
// Use Retry-After header if available, else exponential backoff
|
|
56
|
+
const delay = error.retryAfter
|
|
57
|
+
? parseInt(error.retryAfter, 10) * 1000
|
|
58
|
+
: Math.min(RETRY_CONFIG.maxDelay, RETRY_CONFIG.baseDelay * Math.pow(2, attempt - 1));
|
|
59
|
+
|
|
60
|
+
// console.error(`[S2] Rate limited. Retrying in ${delay}ms...`);
|
|
61
|
+
await sleep(delay);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function doRequest(path) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const options = {
|
|
72
|
+
hostname: BASE_HOST,
|
|
73
|
+
path: path,
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
'User-Agent': 'WTF-P/0.4.0 (citation-expert)',
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (API_KEY) {
|
|
81
|
+
options.headers['x-api-key'] = API_KEY;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const req = https.request(options, (res) => {
|
|
85
|
+
let data = '';
|
|
86
|
+
|
|
87
|
+
res.on('data', (chunk) => data += chunk);
|
|
88
|
+
|
|
89
|
+
res.on('end', () => {
|
|
90
|
+
if (res.statusCode === 429) {
|
|
91
|
+
const retryAfter = res.headers['retry-after'];
|
|
92
|
+
return reject(new SemanticScholarError('Rate limit exceeded', 429, retryAfter));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (res.statusCode >= 400) {
|
|
96
|
+
return reject(new SemanticScholarError(`API Error: ${res.statusCode} ${data}`, res.statusCode));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const json = JSON.parse(data);
|
|
101
|
+
resolve(json);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
reject(new Error(`Failed to parse response: ${e.message}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
req.on('error', (e) => reject(new Error(`Request failed: ${e.message}`)));
|
|
109
|
+
req.on('timeout', () => {
|
|
110
|
+
req.destroy();
|
|
111
|
+
reject(new Error('Request timed out'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Set a reasonable timeout (10s)
|
|
115
|
+
req.setTimeout(10000);
|
|
116
|
+
req.end();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Search for papers
|
|
122
|
+
* @param {string} query
|
|
123
|
+
* @param {Object} options
|
|
124
|
+
*/
|
|
125
|
+
async function search(query, options = {}) {
|
|
126
|
+
const limit = Math.min(options.limit || 10, 100);
|
|
127
|
+
const fields = options.fields || [
|
|
128
|
+
'paperId', 'externalIds', 'title', 'abstract', 'venue', 'year',
|
|
129
|
+
'citationCount', 'influentialCitationCount', 'authors',
|
|
130
|
+
'publicationTypes', 'openAccessPdf'
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const params = {
|
|
134
|
+
query,
|
|
135
|
+
limit,
|
|
136
|
+
fields: fields.join(',')
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (options.year) params.year = options.year;
|
|
140
|
+
|
|
141
|
+
const response = await request('/paper/search', params);
|
|
142
|
+
return response.data || [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get paper by ID
|
|
147
|
+
* @param {string} id - S2 PaperId, DOI, ArXivId, etc.
|
|
148
|
+
* @param {string[]} fields
|
|
149
|
+
*/
|
|
150
|
+
async function getPaper(id, fields = []) {
|
|
151
|
+
const defaultFields = [
|
|
152
|
+
'paperId', 'externalIds', 'title', 'abstract', 'venue', 'year',
|
|
153
|
+
'citationCount', 'influentialCitationCount', 'authors',
|
|
154
|
+
'publicationTypes', 'openAccessPdf'
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const params = {
|
|
158
|
+
fields: (fields.length > 0 ? fields : defaultFields).join(',')
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return await request(`/paper/${id}`, params);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get citations for a paper
|
|
166
|
+
* @param {string} paperId
|
|
167
|
+
* @param {number} limit
|
|
168
|
+
*/
|
|
169
|
+
async function getCitations(paperId, limit = 20) {
|
|
170
|
+
const params = {
|
|
171
|
+
limit,
|
|
172
|
+
fields: 'title,year,citationCount,authors' // Minimal fields for citations
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const response = await request(`/paper/${paperId}/citations`, params);
|
|
176
|
+
return response.data || [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
search,
|
|
181
|
+
getPaper,
|
|
182
|
+
getCitations,
|
|
183
|
+
SemanticScholarError
|
|
184
|
+
};
|
package/bin/lib/utils.js
CHANGED
|
@@ -62,17 +62,25 @@ function isValidPath(inputPath) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Get the
|
|
65
|
+
* Get the configuration directory for a specific vendor
|
|
66
66
|
*/
|
|
67
|
-
function
|
|
67
|
+
function getVendorDir(vendor, explicitConfigDir, isGlobal = true) {
|
|
68
68
|
if (!isGlobal) {
|
|
69
|
-
return path.join(process.cwd(),
|
|
69
|
+
return path.join(process.cwd(), `.${vendor}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const envVar = `${vendor.toUpperCase()}_CONFIG_DIR`;
|
|
72
73
|
const configDir = normalizePath(explicitConfigDir) ||
|
|
73
|
-
normalizePath(process.env
|
|
74
|
+
normalizePath(process.env[envVar]);
|
|
74
75
|
|
|
75
|
-
return configDir || path.join(os.homedir(),
|
|
76
|
+
return configDir || path.join(os.homedir(), `.${vendor}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the Claude config directory (respects CLAUDE_CONFIG_DIR env var)
|
|
81
|
+
*/
|
|
82
|
+
function getClaudeDir(explicitConfigDir, isGlobal = true) {
|
|
83
|
+
return getVendorDir('claude', explicitConfigDir, isGlobal);
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
@@ -109,6 +117,9 @@ function readInstalledVersion(claudeDir) {
|
|
|
109
117
|
* Write version tracking file
|
|
110
118
|
*/
|
|
111
119
|
function writeVersionFile(claudeDir, version, installedFiles) {
|
|
120
|
+
if (!fs.existsSync(claudeDir)) {
|
|
121
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
122
|
+
}
|
|
112
123
|
const versionFile = path.join(claudeDir, VERSION_FILE);
|
|
113
124
|
const data = {
|
|
114
125
|
version,
|
|
@@ -144,47 +155,56 @@ function simpleChecksum(filePath) {
|
|
|
144
155
|
/**
|
|
145
156
|
* Detect installation state
|
|
146
157
|
*/
|
|
147
|
-
function detectInstallation(
|
|
158
|
+
function detectInstallation(vendorDir) {
|
|
148
159
|
const result = {
|
|
149
160
|
hasCommands: false,
|
|
150
|
-
|
|
161
|
+
hasWorkflows: false,
|
|
162
|
+
hasSkills: false,
|
|
151
163
|
version: null,
|
|
152
164
|
partial: false,
|
|
153
165
|
corrupt: false,
|
|
154
166
|
commandFiles: [],
|
|
167
|
+
workflowFiles: [],
|
|
155
168
|
skillFiles: []
|
|
156
169
|
};
|
|
157
170
|
|
|
158
|
-
const commandsDir = path.join(
|
|
159
|
-
const
|
|
171
|
+
const commandsDir = path.join(vendorDir, 'commands', 'wtfp');
|
|
172
|
+
const workflowsDir = path.join(vendorDir, 'write-the-f-paper');
|
|
173
|
+
const skillsDir = path.join(vendorDir, 'skills', 'wtfp');
|
|
160
174
|
|
|
161
175
|
result.hasCommands = fs.existsSync(commandsDir);
|
|
162
|
-
result.
|
|
176
|
+
result.hasWorkflows = fs.existsSync(workflowsDir);
|
|
177
|
+
result.hasSkills = fs.existsSync(skillsDir);
|
|
163
178
|
|
|
164
179
|
if (result.hasCommands) {
|
|
165
180
|
result.commandFiles = collectFiles(commandsDir);
|
|
166
181
|
}
|
|
167
|
-
if (result.
|
|
168
|
-
result.
|
|
182
|
+
if (result.hasWorkflows) {
|
|
183
|
+
result.workflowFiles = collectFiles(workflowsDir);
|
|
184
|
+
}
|
|
185
|
+
if (result.hasSkills) {
|
|
186
|
+
result.skillFiles = collectFiles(skillsDir);
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
// Read version info
|
|
172
|
-
const versionData = readInstalledVersion(
|
|
190
|
+
const versionData = readInstalledVersion(vendorDir);
|
|
173
191
|
if (versionData) {
|
|
174
192
|
result.version = versionData.version;
|
|
175
193
|
result.corrupt = versionData.corrupt || false;
|
|
176
194
|
|
|
177
|
-
// Check for partial install
|
|
195
|
+
// Check for partial install
|
|
178
196
|
if (!versionData.corrupt) {
|
|
179
197
|
const expectedHasCommands = versionData.manifest?.some(f => f.path.includes('commands/wtfp'));
|
|
180
|
-
const
|
|
198
|
+
const expectedHasWorkflows = versionData.manifest?.some(f => f.path.includes('write-the-f-paper'));
|
|
199
|
+
const expectedHasSkills = versionData.manifest?.some(f => f.path.includes('skills/wtfp'));
|
|
181
200
|
|
|
182
201
|
if ((expectedHasCommands && !result.hasCommands) ||
|
|
183
|
-
(
|
|
202
|
+
(expectedHasWorkflows && !result.hasWorkflows) ||
|
|
203
|
+
(expectedHasSkills && !result.hasSkills)) {
|
|
184
204
|
result.partial = true;
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
|
-
} else if (result.hasCommands || result.
|
|
207
|
+
} else if (result.hasCommands || result.hasWorkflows || result.hasSkills) {
|
|
188
208
|
// Files exist but no version file - legacy install
|
|
189
209
|
result.version = 'legacy';
|
|
190
210
|
}
|