research-copilot 0.2.16 → 0.2.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "research-copilot",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "AI-powered research assistant for scientists — literature search, data analysis, academic writing, and project management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "app/build/",
13
13
  "app/package.json",
14
14
  "lib/skills/",
15
+ "scripts/",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ // Backfill paper artifacts from existing literature-run review.json files.
3
+ // Usage: node scripts/backfill-papers.mjs <projectPath>
4
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'fs'
5
+ import { join } from 'path'
6
+ import { randomUUID } from 'crypto'
7
+
8
+ const projectPath = process.argv[2]
9
+ if (!projectPath) {
10
+ console.error('Usage: node scripts/backfill-papers.mjs <projectPath>')
11
+ process.exit(1)
12
+ }
13
+
14
+ const litRunsDir = join(projectPath, '.research-pilot/literature-runs')
15
+ const papersDir = join(projectPath, '.research-pilot/artifacts/papers')
16
+ mkdirSync(papersDir, { recursive: true })
17
+
18
+ // Load existing papers for dedup
19
+ const existingPapers = []
20
+ if (existsSync(papersDir)) {
21
+ for (const f of readdirSync(papersDir)) {
22
+ if (!f.endsWith('.json')) continue
23
+ try {
24
+ const p = JSON.parse(readFileSync(join(papersDir, f), 'utf-8'))
25
+ existingPapers.push(p)
26
+ } catch {}
27
+ }
28
+ }
29
+
30
+ function normalizeTitle(t) {
31
+ return t.toLowerCase().replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim()
32
+ }
33
+
34
+ function isDuplicate(title, year) {
35
+ const nt = normalizeTitle(title)
36
+ return existingPapers.some(p => {
37
+ const pt = normalizeTitle(p.title)
38
+ if (pt !== nt) return false
39
+ if (!year || !p.year) return true
40
+ return p.year === year
41
+ })
42
+ }
43
+
44
+ function generateCiteKey(authors, year) {
45
+ const firstAuthor = (authors?.[0] ?? 'unknown').split(/\s+/).pop()?.toLowerCase() ?? 'unknown'
46
+ return `${firstAuthor}${year ?? 'nd'}`
47
+ }
48
+
49
+ const AUTO_SAVE_THRESHOLD = 7
50
+ let saved = 0
51
+ let skippedDup = 0
52
+ let skippedScore = 0
53
+
54
+ if (!existsSync(litRunsDir)) {
55
+ console.error(`No literature-runs dir at ${litRunsDir}`)
56
+ process.exit(1)
57
+ }
58
+
59
+ const runs = readdirSync(litRunsDir)
60
+ for (const runId of runs) {
61
+ const reviewPath = join(litRunsDir, runId, 'review.json')
62
+ if (!existsSync(reviewPath)) continue
63
+
64
+ let data
65
+ try {
66
+ data = JSON.parse(readFileSync(reviewPath, 'utf-8'))
67
+ } catch { continue }
68
+
69
+ const papers = data.review?.relevantPapers ?? []
70
+ const subTopics = data.plan?.subTopics ?? []
71
+ const roundLabel = `R-${runId}`
72
+
73
+ for (const paper of papers) {
74
+ if ((paper.relevanceScore ?? 0) < AUTO_SAVE_THRESHOLD) {
75
+ skippedScore++
76
+ continue
77
+ }
78
+ if (isDuplicate(paper.title, paper.year)) {
79
+ skippedDup++
80
+ continue
81
+ }
82
+
83
+ const authors = paper.authors?.length > 0 ? paper.authors : ['Unknown']
84
+ const citeKey = generateCiteKey(authors, paper.year)
85
+ const doi = (paper.doi ?? '').trim() || `unknown:${citeKey}`
86
+ const now = new Date().toISOString()
87
+ const id = randomUUID()
88
+
89
+ // Match subtopic
90
+ const matchedSubTopic = subTopics.find(st =>
91
+ paper.relevanceJustification?.toLowerCase().includes(st.name.toLowerCase())
92
+ )?.name
93
+
94
+ const artifact = {
95
+ id,
96
+ type: 'paper',
97
+ title: paper.title,
98
+ authors,
99
+ abstract: paper.abstract ?? '',
100
+ citeKey,
101
+ doi,
102
+ bibtex: `@article{${citeKey},\n title = {${paper.title}},\n author = {${authors.join(' and ')}},${paper.year ? `\n year = {${paper.year}},` : ''}${paper.venue ? `\n journal = {${paper.venue}},` : ''}${doi ? `\n doi = {${doi}},` : ''}${paper.url ? `\n url = {${paper.url}},` : ''}\n}`,
103
+ year: paper.year ?? undefined,
104
+ venue: paper.venue ?? undefined,
105
+ url: paper.url ?? undefined,
106
+ tags: [],
107
+ provenance: {
108
+ source: 'agent',
109
+ sessionId: 'backfill',
110
+ agentId: 'literature-team',
111
+ extractedFrom: 'agent-response'
112
+ },
113
+ createdAt: now,
114
+ updatedAt: now,
115
+ externalSource: paper.source,
116
+ relevanceScore: paper.relevanceScore,
117
+ citationCount: paper.citationCount ?? undefined,
118
+ relevanceJustification: paper.relevanceJustification,
119
+ subTopic: matchedSubTopic,
120
+ addedInRound: roundLabel,
121
+ addedByTask: 'deep_literature_study',
122
+ identityConfidence: paper.doi ? 'high' : 'medium',
123
+ semanticScholarId: paper.source === 'semantic_scholar' ? paper.id : undefined,
124
+ arxivId: paper.source === 'arxiv' ? paper.id : undefined,
125
+ }
126
+
127
+ // Remove undefined keys for clean JSON
128
+ for (const [k, v] of Object.entries(artifact)) {
129
+ if (v === undefined) delete artifact[k]
130
+ }
131
+
132
+ const filePath = join(papersDir, `${id}.json`)
133
+ writeFileSync(filePath, JSON.stringify(artifact, null, 2), 'utf-8')
134
+
135
+ // Track for dedup within this run
136
+ existingPapers.push(artifact)
137
+ saved++
138
+ console.log(` + ${paper.title.slice(0, 80)} (score: ${paper.relevanceScore})`)
139
+ }
140
+ }
141
+
142
+ console.log(`\nDone: ${saved} papers saved, ${skippedDup} duplicates skipped, ${skippedScore} below threshold (< ${AUTO_SAVE_THRESHOLD})`)
@@ -0,0 +1,32 @@
1
+ import { chmodSync, readdirSync, statSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import path from 'node:path'
4
+
5
+ // spawn-helper is only used on Unix platforms; Windows uses conpty.
6
+ if (process.platform === 'win32') process.exit(0)
7
+
8
+ const require = createRequire(import.meta.url)
9
+
10
+ try {
11
+ // Resolve from package.json so we rely only on node-pty's public layout,
12
+ // not internal modules or runtime-loaded native bindings.
13
+ const ptyDir = path.dirname(require.resolve('node-pty/package.json'))
14
+ const prebuildsDir = path.join(ptyDir, 'prebuilds')
15
+
16
+ for (const entry of readdirSync(prebuildsDir, { withFileTypes: true })) {
17
+ if (!entry.isDirectory()) continue
18
+ const helper = path.join(prebuildsDir, entry.name, 'spawn-helper')
19
+ try {
20
+ const stat = statSync(helper)
21
+ if ((stat.mode & 0o111) !== 0o111) {
22
+ chmodSync(helper, stat.mode | 0o755)
23
+ console.log(`[postinstall] fixed node-pty helper permissions: ${helper}`)
24
+ }
25
+ } catch {
26
+ // Arch dirs without spawn-helper (e.g., win32-*) — skip silently.
27
+ }
28
+ }
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error)
31
+ console.warn(`[postinstall] skipped node-pty helper permission fix: ${message}`)
32
+ }