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.
Files changed (122) hide show
  1. package/README.md +24 -2
  2. package/bin/commands/doctor.js +100 -0
  3. package/bin/commands/install-logic.js +231 -0
  4. package/bin/commands/list.js +88 -0
  5. package/bin/commands/status.js +85 -0
  6. package/bin/commands/update.js +50 -0
  7. package/bin/install.js +78 -629
  8. package/bin/lib/analyze-impact.js +105 -0
  9. package/bin/lib/bib-format.js +161 -0
  10. package/bin/lib/bib-index.js +104 -0
  11. package/bin/lib/citation-fetcher.js +299 -0
  12. package/bin/lib/citation-ranker.js +133 -0
  13. package/bin/lib/manifest.js +59 -0
  14. package/bin/lib/scholar-lookup.js +188 -0
  15. package/bin/lib/semantic-scholar.js +184 -0
  16. package/bin/lib/utils.js +37 -17
  17. package/bin/lib/wcn-compiler.js +226 -0
  18. package/bin/uninstall.js +51 -17
  19. package/core/write-the-f-paper/templates/posters/basic.md +34 -0
  20. package/core/write-the-f-paper/templates/slides/basic.md +31 -0
  21. package/package.json +12 -4
  22. package/vendors/claude/.claude-plugin/plugin.json +12 -0
  23. package/vendors/claude/agents/wtfp/citation-expert.md +45 -0
  24. package/vendors/claude/agents/wtfp/citation-formatter.md +42 -0
  25. package/vendors/claude/agents/wtfp/citation-retriever.md +31 -0
  26. package/{commands → vendors/claude/commands}/wtfp/analyze-bib.md +41 -52
  27. package/{commands → vendors/claude/commands}/wtfp/check-refs.md +11 -9
  28. package/vendors/claude/commands/wtfp/create-poster.md +42 -0
  29. package/vendors/claude/commands/wtfp/create-slides.md +41 -0
  30. package/{commands → vendors/claude/commands}/wtfp/help.md +3 -0
  31. package/{commands → vendors/claude/commands}/wtfp/new-paper.md +42 -81
  32. package/{commands → vendors/claude/commands}/wtfp/research-gap.md +3 -4
  33. package/vendors/claude/mcp/research-server/package.json +13 -0
  34. package/vendors/claude/mcp/research-server/src/index.js +133 -0
  35. package/vendors/claude/skills/wtfp/echarts/SKILL.md +47 -0
  36. package/vendors/claude/skills/wtfp/marp/SKILL.md +49 -0
  37. /package/{write-the-f-paper → core/write-the-f-paper}/references/checkpoints.md +0 -0
  38. /package/{write-the-f-paper → core/write-the-f-paper}/references/continuation-format.md +0 -0
  39. /package/{write-the-f-paper → core/write-the-f-paper}/references/git-integration.md +0 -0
  40. /package/{write-the-f-paper → core/write-the-f-paper}/references/github-contrib.md +0 -0
  41. /package/{write-the-f-paper → core/write-the-f-paper}/references/imrad-structure.md +0 -0
  42. /package/{write-the-f-paper → core/write-the-f-paper}/references/length-estimation.md +0 -0
  43. /package/{write-the-f-paper → core/write-the-f-paper}/references/plan-format.md +0 -0
  44. /package/{write-the-f-paper → core/write-the-f-paper}/references/principles.md +0 -0
  45. /package/{write-the-f-paper → core/write-the-f-paper}/references/questioning.md +0 -0
  46. /package/{write-the-f-paper → core/write-the-f-paper}/references/research-pitfalls.md +0 -0
  47. /package/{write-the-f-paper → core/write-the-f-paper}/templates/config.json +0 -0
  48. /package/{write-the-f-paper → core/write-the-f-paper}/templates/context.md +0 -0
  49. /package/{write-the-f-paper → core/write-the-f-paper}/templates/continue-here.md +0 -0
  50. /package/{write-the-f-paper → core/write-the-f-paper}/templates/data-inventory.md +0 -0
  51. /package/{write-the-f-paper → core/write-the-f-paper}/templates/discovery.md +0 -0
  52. /package/{write-the-f-paper → core/write-the-f-paper}/templates/issues.md +0 -0
  53. /package/{write-the-f-paper → core/write-the-f-paper}/templates/manuscript.md +0 -0
  54. /package/{write-the-f-paper → core/write-the-f-paper}/templates/narrative-arc.md +0 -0
  55. /package/{write-the-f-paper → core/write-the-f-paper}/templates/outline.md +0 -0
  56. /package/{write-the-f-paper → core/write-the-f-paper}/templates/phase-prompt.md +0 -0
  57. /package/{write-the-f-paper → core/write-the-f-paper}/templates/prior-drafts.md +0 -0
  58. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/argument-structure.md +0 -0
  59. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/bibliography.md +0 -0
  60. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/gaps.md +0 -0
  61. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/imrad.md +0 -0
  62. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/peer-review.md +0 -0
  63. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/style-guide.md +0 -0
  64. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project-context/tools.md +0 -0
  65. /package/{write-the-f-paper → core/write-the-f-paper}/templates/project.md +0 -0
  66. /package/{write-the-f-paper → core/write-the-f-paper}/templates/research.md +0 -0
  67. /package/{write-the-f-paper → core/write-the-f-paper}/templates/review-comments.md +0 -0
  68. /package/{write-the-f-paper → core/write-the-f-paper}/templates/revision-issues.md +0 -0
  69. /package/{write-the-f-paper → core/write-the-f-paper}/templates/roadmap.md +0 -0
  70. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-archive.md +0 -0
  71. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section-context.md +0 -0
  72. /package/{write-the-f-paper → core/write-the-f-paper}/templates/section.md +0 -0
  73. /package/{write-the-f-paper → core/write-the-f-paper}/templates/state.md +0 -0
  74. /package/{write-the-f-paper → core/write-the-f-paper}/templates/summary.md +0 -0
  75. /package/{write-the-f-paper → core/write-the-f-paper}/venues/acm-cs.yaml +0 -0
  76. /package/{write-the-f-paper → core/write-the-f-paper}/venues/arxiv-ml.yaml +0 -0
  77. /package/{write-the-f-paper → core/write-the-f-paper}/venues/ieee-cs.yaml +0 -0
  78. /package/{write-the-f-paper → core/write-the-f-paper}/venues/nature.yaml +0 -0
  79. /package/{write-the-f-paper → core/write-the-f-paper}/venues/thesis-chapter.yaml +0 -0
  80. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.md +0 -0
  81. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/create-outline.wcn.md +0 -0
  82. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.md +0 -0
  83. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/discuss-section.wcn.md +0 -0
  84. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.md +0 -0
  85. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/execute-section.wcn.md +0 -0
  86. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.md +0 -0
  87. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/list-assumptions.wcn.md +0 -0
  88. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.md +0 -0
  89. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/lit-review-phase.wcn.md +0 -0
  90. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.md +0 -0
  91. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/map-project.wcn.md +0 -0
  92. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.md +0 -0
  93. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/plan-section.wcn.md +0 -0
  94. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.md +0 -0
  95. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/research-gap.wcn.md +0 -0
  96. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.md +0 -0
  97. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/resume-paper.wcn.md +0 -0
  98. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.md +0 -0
  99. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/review-section.wcn.md +0 -0
  100. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.md +0 -0
  101. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/submit-draft.wcn.md +0 -0
  102. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.md +0 -0
  103. /package/{write-the-f-paper → core/write-the-f-paper}/workflows/transition.wcn.md +0 -0
  104. /package/{commands → vendors/claude/commands}/wtfp/contribute.md +0 -0
  105. /package/{commands → vendors/claude/commands}/wtfp/create-outline.md +0 -0
  106. /package/{commands → vendors/claude/commands}/wtfp/discuss-section.md +0 -0
  107. /package/{commands → vendors/claude/commands}/wtfp/export-latex.md +0 -0
  108. /package/{commands → vendors/claude/commands}/wtfp/insert-section.md +0 -0
  109. /package/{commands → vendors/claude/commands}/wtfp/list-assumptions.md +0 -0
  110. /package/{commands → vendors/claude/commands}/wtfp/map-project.md +0 -0
  111. /package/{commands → vendors/claude/commands}/wtfp/pause-writing.md +0 -0
  112. /package/{commands → vendors/claude/commands}/wtfp/plan-revision.md +0 -0
  113. /package/{commands → vendors/claude/commands}/wtfp/plan-section.md +0 -0
  114. /package/{commands → vendors/claude/commands}/wtfp/polish-prose.md +0 -0
  115. /package/{commands → vendors/claude/commands}/wtfp/progress.md +0 -0
  116. /package/{commands → vendors/claude/commands}/wtfp/remove-section.md +0 -0
  117. /package/{commands → vendors/claude/commands}/wtfp/report-bug.md +0 -0
  118. /package/{commands → vendors/claude/commands}/wtfp/request-feature.md +0 -0
  119. /package/{commands → vendors/claude/commands}/wtfp/resume-writing.md +0 -0
  120. /package/{commands → vendors/claude/commands}/wtfp/review-section.md +0 -0
  121. /package/{commands → vendors/claude/commands}/wtfp/submit-milestone.md +0 -0
  122. /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 Claude config directory (respects CLAUDE_CONFIG_DIR env var)
65
+ * Get the configuration directory for a specific vendor
66
66
  */
67
- function getClaudeDir(explicitConfigDir, isGlobal = true) {
67
+ function getVendorDir(vendor, explicitConfigDir, isGlobal = true) {
68
68
  if (!isGlobal) {
69
- return path.join(process.cwd(), '.claude');
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.CLAUDE_CONFIG_DIR);
74
+ normalizePath(process.env[envVar]);
74
75
 
75
- return configDir || path.join(os.homedir(), '.claude');
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(claudeDir) {
158
+ function detectInstallation(vendorDir) {
148
159
  const result = {
149
160
  hasCommands: false,
150
- hasSkill: false,
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(claudeDir, 'commands', 'wtfp');
159
- const skillDir = path.join(claudeDir, 'write-the-f-paper');
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.hasSkill = fs.existsSync(skillDir);
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.hasSkill) {
168
- result.skillFiles = collectFiles(skillDir);
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(claudeDir);
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 (version file exists but missing dirs)
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 expectedHasSkill = versionData.manifest?.some(f => f.path.includes('write-the-f-paper'));
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
- (expectedHasSkill && !result.hasSkill)) {
202
+ (expectedHasWorkflows && !result.hasWorkflows) ||
203
+ (expectedHasSkills && !result.hasSkills)) {
184
204
  result.partial = true;
185
205
  }
186
206
  }
187
- } else if (result.hasCommands || result.hasSkill) {
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
  }