lumina-wiki 1.6.1 → 1.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,55 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.7.0] - 2026-06-16
9
+
10
+ ### Added
11
+
12
+ - **Advanced paper ranking** via the new research-pack skill
13
+ `/lumi-research-rank`. It scores an already-ingested paper and records the
14
+ results on its source page, both as a machine-readable `ranking:` frontmatter
15
+ block and a human-readable `## Ranking` scorecard. Re-running refreshes the
16
+ ranking and preserves any notes inside `<!-- user-edited -->` markers.
17
+ - **Citation influence signal**: surfaces Semantic Scholar's influential-citation
18
+ count alongside the raw citation count (reuses the existing `fetch_s2.py`; no
19
+ new key required).
20
+ - **4C qualitative rubric** (Correctness, Clarity, Contribution, Context, each
21
+ scored 1-5) produced with a three-pass reading method to keep the assessment
22
+ efficient. Scores are explicitly recorded as LLM-assessed with a timestamp.
23
+ - **Venue prestige** recorded from the agent's own knowledge and explicitly
24
+ flagged as an estimate (`venue_source: llm-estimated`) — no live API or
25
+ bundled dataset.
26
+ - **Optional, key-gated influence fetchers** `fetch_scite.py` (Scite.ai Smart
27
+ Citation tallies) and `fetch_altmetric.py` (Altmetric attention score). Both
28
+ degrade gracefully: with no key set they exit with a clear message and the
29
+ skill simply skips that signal. New `.env` keys `SCITE_API_KEY` and
30
+ `ALTMETRIC_API_KEY`.
31
+
32
+ ### Changed
33
+
34
+ - Source page schema gains an optional `ranking` frontmatter object (no change
35
+ required for existing un-ranked pages).
36
+
37
+ ## [1.6.2] - 2026-06-15
38
+
39
+ ### Fixed
40
+
41
+ - Repaired stale Claude skill links during upgrades by validating their real
42
+ targets instead of trusting the previously recorded link strategy.
43
+ - Made POSIX skill links relative so copied, moved, or renamed workspaces can
44
+ be upgraded without retaining links to their old absolute location.
45
+ - Reconciled removed packs and IDE targets by deleting obsolete
46
+ installer-managed skills, tools, links, and unchanged generated stubs while
47
+ preserving modified or user-owned files.
48
+ - Made `npx lumina-wiki install` detect and upgrade an enclosing workspace when
49
+ invoked from a nested directory, while explicit `--directory` and `--cwd`
50
+ targets remain exact.
51
+ - Fixed interactive locale switching for existing and legacy workspaces,
52
+ including default-language cascading and confirmation binding to the final
53
+ resolved locale.
54
+ - Made installation fail clearly when required Claude skill links cannot be
55
+ created instead of writing successful state for a partial install.
56
+
8
57
  ## [1.6.1] - 2026-05-18
9
58
 
10
59
  ### Fixed
package/README.md CHANGED
@@ -93,6 +93,8 @@ The agent will help you check the research tools and save keys to a local `.env`
93
93
 
94
94
  If you reinstall Lumina-Wiki on a project that already has a `wiki/` from an earlier version, just run `npx lumina-wiki install` again. The installer updates scripts, schemas, and skills; **your content in `wiki/`, `raw/`, and `log.md` is not modified**.
95
95
 
96
+ You can run the command from the project root or one of its subfolders. If you remove a pack or AI tool from the setup, Lumina removes its old managed commands and unchanged setup files. Files you edited are kept with a warning. If the whole project was copied, moved, or renamed, Lumina repairs its managed links during the upgrade.
97
+
96
98
  If the installer warns that older entries are missing newer frontmatter fields, you have two ways to backfill them:
97
99
 
98
100
  - **Recommended:** open your AI chat and run `/lumi-migrate-legacy`.
@@ -183,6 +185,7 @@ These are the commands you can use when chatting with your AI agent.
183
185
  | | `/lumi-research-survey` | Create a survey or summary from existing knowledge. |
184
186
  | | `/lumi-research-prefill` | Seed foundational concepts to avoid duplicates. |
185
187
  | | `/lumi-research-topic` | Create a topic page at `wiki/topics/<slug>.md` by gathering related concepts and sources already in your wiki. The AI proposes what to include and you confirm before anything is written. Use this after several `/lumi-ingest` runs when you want to give a theme its own page. |
188
+ | | `/lumi-research-rank` | Score a paper you have already ingested so you know what to read first. It looks up how influential the paper is (citation signals), estimates how respected its venue is, and rates its quality on four points — Correctness, Clarity, Contribution, and Context — then adds a clear scorecard to the paper's page. Measured numbers and the AI's own estimates are always kept separate. |
186
189
  | | `/lumi-research-setup` | Help configure API keys for research tools. |
187
190
  | | `/lumi-research-watch-run` | Run one scheduled-discovery pass over your watchlist (topics + RSS / Atom feeds). Polls only when you ask. |
188
191
  | **Reading** | `/lumi-reading-chapter-ingest` | Ingest a book chapter by chapter. |
@@ -207,7 +210,7 @@ Lumina-Wiki is evolving rapidly. Here is our user-facing roadmap:
207
210
  - [x] **Improved CI/CD:** Native support for Bun and Node 22 environments. *(shipped in v1.2)*
208
211
  - [x] **Global Source Expansion:** Direct integration with OpenAlex, CORE, and Unpaywall for reliable DOI-to-PDF resolution. *(shipped in v1.6)*
209
212
  - [x] **RSS & Blog Monitoring:** Automatically surface new papers from your favorite lab blogs and journals via `type: feed` watchlist items. *(shipped in v1.6)*
210
- - [ ] **Advanced Paper Ranking:** See influence scores and quality signals for your research papers.
213
+ - [x] **Advanced Paper Ranking:** See influence scores and quality signals for your research papers via `/lumi-research-rank`. *(shipped in v1.7)*
211
214
 
212
215
  **Long-term (Deep Research & Integration)**
213
216
  - [ ] **Image OCR & Scanned PDFs:** Ingest screenshots and scanned PDFs into your wiki.
package/README.vi.md CHANGED
@@ -93,6 +93,8 @@ Agent sẽ hướng dẫn bạn kiểm tra công cụ nghiên cứu và lưu key
93
93
 
94
94
  Nếu bạn cài lại Lumina-Wiki trên một dự án đã có `wiki/` từ phiên bản trước, cứ chạy lại `npx lumina-wiki install`. Installer cập nhật scripts, schemas và skills; **nội dung của bạn trong `wiki/`, `raw/`, `log.md` không bị chỉnh sửa**.
95
95
 
96
+ Bạn có thể chạy lệnh tại thư mục gốc hoặc một thư mục con của dự án. Nếu bỏ một gói hoặc công cụ AI khỏi thiết lập, Lumina sẽ dọn các lệnh cũ và những tệp thiết lập chưa được bạn sửa. Tệp đã chỉnh sửa được giữ lại kèm cảnh báo. Nếu toàn bộ dự án được sao chép, di chuyển hoặc đổi tên, Lumina sẽ sửa lại các liên kết do hệ thống quản lý khi nâng cấp.
97
+
96
98
  Nếu installer cảnh báo entry cũ thiếu frontmatter mới, có hai cách backfill:
97
99
 
98
100
  - **Khuyến nghị:** mở chat AI và chạy `/lumi-migrate-legacy`.
@@ -183,6 +185,7 @@ Xem [Hướng dẫn Nâng cao](docs/user-guide/advanced-qmd.vi.md) để biết
183
185
  | | `/lumi-research-survey` | Tạo một bài tổng quan/khảo sát từ kiến thức hiện có. |
184
186
  | | `/lumi-research-prefill` | Tạo trước các khái niệm nền tảng để tránh trùng lặp. |
185
187
  | | `/lumi-research-topic` | Gom các khái niệm và nguồn liên quan trong wiki thành một trang chủ đề tại `wiki/topics/<slug>.md`. AI đề xuất danh sách để bạn xem và xác nhận trước khi trang được tạo. Dùng sau khi đã nạp nhiều tài liệu và muốn tổng hợp một nhóm ý tưởng thành trang riêng. |
188
+ | | `/lumi-research-rank` | Chấm điểm một bài báo bạn đã nạp để biết nên đọc gì trước. Nó tra mức độ ảnh hưởng của bài (tín hiệu trích dẫn), ước lượng uy tín nơi công bố, và đánh giá chất lượng theo bốn tiêu chí — Tính đúng đắn, Sự rõ ràng, Đóng góp, và Bối cảnh — rồi thêm một bảng điểm rõ ràng vào trang của bài báo. Các con số đo được và ước lượng của AI luôn được tách bạch. |
186
189
  | | `/lumi-research-setup` | Giúp cấu hình API key cho các công cụ nghiên cứu. |
187
190
  | | `/lumi-research-watch-run` | Chạy một lượt khám phá theo lịch dựa trên watchlist (chủ đề + nguồn RSS / Atom). Chỉ chạy khi bạn yêu cầu. |
188
191
  | **Reading** | `/lumi-reading-chapter-ingest`| Nạp kiến thức sách theo từng chương. |
@@ -207,7 +210,7 @@ Lumina-Wiki đang phát triển nhanh chóng. Dưới đây là lộ trình hư
207
210
  - [x] **Cải thiện CI/CD:** Hỗ trợ chính thức cho môi trường Bun và Node 22. *(đã phát hành trong v1.2)*
208
211
  - [x] **Mở rộng nguồn dữ liệu toàn cầu:** Tích hợp trực tiếp với OpenAlex, CORE và Unpaywall để tra cứu DOI-to-PDF đáng tin cậy. *(ra mắt trong v1.6)*
209
212
  - [x] **Theo dõi RSS & Blog:** Tự động phát hiện bài báo mới từ các blog phòng thí nghiệm và tạp chí yêu thích qua các mục `type: feed` trong watchlist. *(ra mắt trong v1.6)*
210
- - [ ] **Xếp hạng bài báo nâng cao:** Xem điểm số ảnh hưởng và tín hiệu chất lượng cho các nghiên cứu của bạn.
213
+ - [x] **Xếp hạng bài báo nâng cao:** Xem điểm số ảnh hưởng và tín hiệu chất lượng cho các nghiên cứu của bạn qua `/lumi-research-rank`. *(ra mắt ở v1.7)*
211
214
 
212
215
  **Dài hạn (Nghiên cứu sâu & Tích hợp)**
213
216
  - [ ] **OCR ảnh & PDF scan:** Nạp ảnh chụp màn hình và PDF dạng scan vào wiki.
package/README.zh.md CHANGED
@@ -93,6 +93,8 @@ Agent 会引导您检查研究工具,并在需要时把 key 保存到本地 `.
93
93
 
94
94
  如果您在已经有旧版 `wiki/` 的项目上重新安装 Lumina-Wiki,直接再次运行 `npx lumina-wiki install` 即可。安装器会更新 scripts、schemas 和 skills;**您在 `wiki/`、`raw/`、`log.md` 中的内容不会被修改**。
95
95
 
96
+ 您可以在项目根目录或其子目录中运行该命令。如果从设置中移除某个包或 AI 工具,Lumina 会清理旧命令和未经您修改的设置文件。您修改过的文件会保留,并显示警告。如果整个项目被复制、移动或重命名,Lumina 会在升级时修复由系统管理的链接。
97
+
96
98
  如果安装器提示旧条目缺少新的 frontmatter 字段,可以用两种方式回填:
97
99
 
98
100
  - **推荐:** 打开 AI 对话并运行 `/lumi-migrate-legacy`。
@@ -184,6 +186,7 @@ npx skills add https://github.com/tobi/qmd --skill qmd
184
186
  | | `/lumi-research-survey` | 从现有知识创建综述/调研。 |
185
187
  | | `/lumi-research-prefill` | 预先生成基础概念,避免重复。 |
186
188
  | | `/lumi-research-topic` | 把 wiki 中已有的相关概念和来源汇聚成一个主题页,保存在 `wiki/topics/<slug>.md`。AI 会提议收录哪些内容,由你确认后再生成页面。多次 `/lumi-ingest` 之后,用它把一组相关想法整理成独立的主题页。 |
189
+ | | `/lumi-research-rank` | 给已经纳入的论文打分,帮你决定先读哪一篇。它会查询论文的影响力(引用信号)、估计发表场所的声望,并从四个方面评估质量——正确性、清晰度、贡献、背景——然后在论文页面上加上一份清晰的评分卡。实测数据与 AI 的估计始终分开标注。 |
187
190
  | | `/lumi-research-setup` | 帮助配置研究工具的 API key。 |
188
191
  | | `/lumi-research-watch-run` | 基于 watchlist 运行一次计划式发现(主题 + RSS / Atom 源)。仅在你要求时才运行。 |
189
192
  | **Reading** | `/lumi-reading-chapter-ingest`| 按章节导入书籍知识。 |
@@ -208,7 +211,7 @@ Lumina-Wiki 正在快速演进。这是我们的用户路线图:
208
211
  - [x] **改进的 CI/CD:** 正式支持 Bun 和 Node 22 环境。*(v1.2 已发布)*
209
212
  - [x] **全球数据源扩展:** 直接集成 OpenAlex、CORE 和 Unpaywall,实现可靠的 DOI-to-PDF 解析。*(在 v1.6 中发布)*
210
213
  - [x] **RSS 与博客监控:** 通过 watchlist 中的 `type: feed` 项,自动从您喜爱的实验室博客和期刊中发现新论文。*(在 v1.6 中发布)*
211
- - [ ] **高级论文排名:** 查看研究论文的影响力评分和质量信号。
214
+ - [x] **高级论文排名:** 通过 `/lumi-research-rank` 查看研究论文的影响力评分和质量信号。*(在 v1.7 中发布)*
212
215
 
213
216
  **长期计划(深度研究与集成)**
214
217
  - [ ] **图片 OCR 与扫描 PDF:** 将截图与扫描版 PDF 导入维基。
package/bin/lumina.js CHANGED
@@ -199,6 +199,12 @@ program
199
199
  .option('--force-locale-switch', 'allow switching installer locale during upgrade')
200
200
  .action(async (cmdOpts) => {
201
201
  const globalOpts = program.opts();
202
+ const hasExplicitDirectory = (
203
+ cmdOpts.directory != null
204
+ || cmdOpts.cwd != null
205
+ || globalOpts.directory != null
206
+ || globalOpts.cwd != null
207
+ );
202
208
  const mergedDir = cmdOpts.directory ?? cmdOpts.cwd ?? globalOpts.directory ?? globalOpts.cwd ?? process.cwd();
203
209
  const mergedYes = cmdOpts.yes ?? globalOpts.yes ?? false;
204
210
  const mergedReLink = cmdOpts.reLink ?? globalOpts.reLink ?? false;
@@ -220,6 +226,7 @@ program
220
226
  documentOutputLang: cmdOpts.documentOutputLanguage,
221
227
  lang: cmdOpts.lang,
222
228
  forceLocaleSwitch: Boolean(cmdOpts.forceLocaleSwitch),
229
+ searchParents: !hasExplicitDirectory,
223
230
  });
224
231
  } catch (err) {
225
232
  // Top-level catch: locale may not be resolved yet (pre-loadLocale path).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "lumina-wiki",
4
- "version": "1.6.1",
4
+ "version": "1.7.0",
5
5
  "description": "Domain-agnostic, multi-IDE wiki scaffolder — Karpathy's LLM-Wiki vision, cross-platform and pack-based.",
6
6
  "keywords": [
7
7
  "llm-wiki",
@@ -69,6 +69,8 @@
69
69
  "src/tools/fetch_core.py",
70
70
  "src/tools/resolve_pdf.py",
71
71
  "src/tools/fetch_rss.py",
72
+ "src/tools/fetch_scite.py",
73
+ "src/tools/fetch_altmetric.py",
72
74
  "src/tools/id_utils.py",
73
75
  "src/tools/requirements.txt",
74
76
  "CHANGELOG.md",
@@ -88,7 +90,8 @@
88
90
  "devDependencies": {},
89
91
  "scripts": {
90
92
  "test": "npm run test:installer",
91
- "test:installer": "node --test bin/lumina.flags.test.js bin/lumina.deprecations.test.js bin/lumina.cancel.test.js src/installer/commands.test.js src/installer/fs.test.js src/installer/locales.test.js src/installer/manifest.test.js src/installer/prompts.test.js src/installer/readme-templates.test.js src/installer/template-engine.test.js src/installer/update-check.test.js",
93
+ "test:installer": "npm run test:installer:commands && node --test bin/lumina.flags.test.js bin/lumina.deprecations.test.js bin/lumina.cancel.test.js src/installer/fs.test.js src/installer/locales.test.js src/installer/manifest.test.js src/installer/prompts.test.js src/installer/readme-templates.test.js src/installer/template-engine.test.js src/installer/update-check.test.js",
94
+ "test:installer:commands": "node src/installer/commands.test.js",
92
95
  "test:scripts": "node --test src/scripts/lint.test.mjs src/scripts/reset.test.mjs src/scripts/wiki.test.mjs src/scripts/discover-runner.test.mjs src/scripts/external-ids.test.mjs src/scripts/parse-ids.test.mjs src/scripts/merge-ids.test.mjs src/scripts/build-source.test.mjs src/scripts/wiki-yaml-object.test.mjs src/scripts/schemas.test.mjs",
93
96
  "test:python": "node scripts/run-pytest.mjs",
94
97
  "test:all": "npm run test:installer && npm run test:scripts && npm run test:python",
@@ -127,6 +127,37 @@ const LUMINA_DIRS = [
127
127
 
128
128
  const VALID_PACKS = new Set(['core', 'research', 'reading', 'learning']);
129
129
  const VALID_IDE_TARGETS = new Set(['claude_code', 'codex', 'cursor', 'gemini_cli', 'qwen', 'iflow', 'generic']);
130
+ const RESEARCH_TOOL_FILES = [
131
+ '_env.py', '_cache.py', 'discover.py', 'init_discovery.py', 'prepare_source.py',
132
+ 'fetch_arxiv.py', 'fetch_wikipedia.py', 'fetch_s2.py', 'fetch_deepxiv.py',
133
+ 'fetch_openalex.py', 'fetch_unpaywall.py', 'fetch_core.py', 'resolve_pdf.py',
134
+ 'fetch_rss.py', 'fetch_scite.py', 'fetch_altmetric.py',
135
+ ];
136
+
137
+ async function findEnclosingWorkspace(startDir) {
138
+ let current = resolve(startDir);
139
+ while (true) {
140
+ try {
141
+ await access(join(current, '_lumina', 'manifest.json'), fsConstants.F_OK);
142
+ return current;
143
+ } catch (err) {
144
+ if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') throw err;
145
+ }
146
+ const parent = dirname(current);
147
+ if (parent === current) return null;
148
+ current = parent;
149
+ }
150
+ }
151
+
152
+ async function readManifestForInstall(projectRoot) {
153
+ try {
154
+ return await readManifest(projectRoot);
155
+ } catch (err) {
156
+ const e = new Error(`MANIFEST_READ_FAILED: ${err.message} (path: ${projectRoot}/_lumina/manifest.json)`);
157
+ e.code = 2;
158
+ throw e;
159
+ }
160
+ }
130
161
 
131
162
  // ---------------------------------------------------------------------------
132
163
  // install command
@@ -144,32 +175,24 @@ const VALID_IDE_TARGETS = new Set(['claude_code', 'codex', 'cursor', 'gemini_cli
144
175
  * @param {string} [opts.projectName] - Hidden escape hatch; default = basename(directory)
145
176
  * @param {string} [opts.communicationLang]
146
177
  * @param {string} [opts.documentOutputLang]
178
+ * @param {boolean} [opts.searchParents] - Find an enclosing Lumina workspace when no directory flag was used
147
179
  */
148
180
  export async function installCommand(opts = {}) {
149
181
  const { yes = false, reLink = false } = opts;
150
182
  const initialDir = opts.directory ?? opts.cwd ?? process.cwd();
151
- let projectRoot = resolve(initialDir);
183
+ const requestedRoot = resolve(initialDir);
184
+ let projectRoot = opts.searchParents
185
+ ? (await findEnclosingWorkspace(requestedRoot) ?? requestedRoot)
186
+ : requestedRoot;
152
187
  const colors = await getColorFns();
153
188
 
154
189
  // 1. Read existing manifest at the initial path (upgrade detection)
155
190
  // Distinguish ENOENT (fresh install) from real I/O errors. Real errors must
156
191
  // bail loud — silently treating them as "fresh install" would let a transient
157
192
  // permission failure quietly nuke an existing install.
158
- let existingManifest = null;
159
- try {
160
- existingManifest = await readManifest(projectRoot);
161
- } catch (err) {
162
- if (err.code === 'ENOENT') {
163
- existingManifest = null;
164
- } else {
165
- // Pre-loadLocale path — intentionally EN-only and machine-readable.
166
- const e = new Error(`MANIFEST_READ_FAILED: ${err.message} (path: ${projectRoot}/_lumina/manifest.json)`);
167
- e.code = 2;
168
- throw e;
169
- }
170
- }
193
+ let existingManifest = await readManifestForInstall(projectRoot);
171
194
 
172
- const isUpgrade = existingManifest !== null;
195
+ let isUpgrade = existingManifest !== null;
173
196
 
174
197
  // 2. Collect answers (locale not yet loaded; prompts use EN fallback literals)
175
198
  let answers;
@@ -182,13 +205,27 @@ export async function installCommand(opts = {}) {
182
205
  cwd: projectRoot,
183
206
  existingManifest,
184
207
  defaultLocale: opts.lang ?? 'en',
208
+ resolveDestination: async (directory) => {
209
+ const manifest = await readManifestForInstall(directory);
210
+ if (!manifest) return null;
211
+ return {
212
+ existingManifest: manifest,
213
+ answers: await readAnswersFromConfig(directory, manifest),
214
+ };
215
+ },
185
216
  });
186
217
  // Re-resolve projectRoot from the directory the user typed.
187
218
  if (answers.directory) {
188
219
  projectRoot = resolve(answers.directory);
189
220
  }
221
+ existingManifest = await readManifestForInstall(projectRoot);
222
+ isUpgrade = existingManifest !== null;
190
223
  }
191
224
  answers = applyInstallOverrides(answers, opts);
225
+ const previousSkillRows = isUpgrade
226
+ ? previousManagedSkillRows(await readSkillsManifest(projectRoot), existingManifest)
227
+ : [];
228
+ const previousFileRows = isUpgrade ? await readFilesManifest(projectRoot) : [];
192
229
 
193
230
  // 2b. Load locale module ONCE after applyInstallOverrides resolves locale.
194
231
  const localeMod = await loadLocale(answers.locale ?? 'en');
@@ -209,7 +246,11 @@ export async function installCommand(opts = {}) {
209
246
  ?? migrateManifest(existingManifest, MANIFEST_SCHEMA_VERSION).locale
210
247
  ?? 'en'
211
248
  );
212
- if (installedLocale !== answers.locale && !opts.forceLocaleSwitch) {
249
+ if (
250
+ installedLocale !== answers.locale
251
+ && !opts.forceLocaleSwitch
252
+ && answers.localeSwitchConfirmedFor !== answers.locale
253
+ ) {
213
254
  const e = new Error(
214
255
  `LOCALE_SWITCH_REFUSED: installed locale '${installedLocale}' differs from resolved locale '${answers.locale}'. ` +
215
256
  `Pass --force-locale-switch to confirm (this will rewrite README.md and IDE stubs in the new locale).`,
@@ -223,6 +264,9 @@ export async function installCommand(opts = {}) {
223
264
  const hasResearch = packs.includes('research');
224
265
  const hasReading = packs.includes('reading');
225
266
  const hasLearning = packs.includes('learning');
267
+ const previousProjectRoot = existingManifest?.resolvedPaths?.projectRoot;
268
+ const relocated = Boolean(previousProjectRoot && resolve(previousProjectRoot) !== projectRoot);
269
+ const effectiveReLink = reLink || relocated;
226
270
 
227
271
  console.log('');
228
272
  if (isUpgrade) {
@@ -230,6 +274,9 @@ export async function installCommand(opts = {}) {
230
274
  } else {
231
275
  console.log(colors.bold(t('progress.installing', { dir: projectRoot })));
232
276
  }
277
+ if (relocated) {
278
+ console.log(colors.yellow(t('warn.relocated', { from: previousProjectRoot, to: projectRoot })));
279
+ }
233
280
 
234
281
  // 3. Scaffold directories
235
282
  const dirsToCreate = [
@@ -252,6 +299,20 @@ export async function installCommand(opts = {}) {
252
299
  await ensureDir(join(projectRoot, dir));
253
300
  }
254
301
 
302
+ await reconcileRemovedIdeTargets({
303
+ projectRoot,
304
+ previousIdeTargets: existingManifest?.ideTargets ?? [],
305
+ currentIdeTargets: ideTargets,
306
+ previousFileRows,
307
+ previousSkillRows,
308
+ colors,
309
+ t,
310
+ });
311
+
312
+ if (isUpgrade && existingManifest?.packs?.research && !hasResearch) {
313
+ await cleanupRemovedResearchPack(projectRoot, previousFileRows, colors, t);
314
+ }
315
+
255
316
  // 4. Template variables
256
317
  const templateVars = {
257
318
  project_name: projectName,
@@ -282,7 +343,10 @@ export async function installCommand(opts = {}) {
282
343
  await copyChangelog(projectRoot);
283
344
 
284
345
  // 9. Copy skills
285
- const skillRows = await copySkills(projectRoot, packs);
346
+ const skillRows = await copySkills(projectRoot, packs, {
347
+ claudeCode: ideTargets.includes('claude_code'),
348
+ });
349
+ await reconcileRemovedSkills(projectRoot, previousSkillRows, skillRows);
286
350
 
287
351
  // 10. Copy Python tools (core: extract_pdf; research pack: discovery/fetchers)
288
352
  await copyTools(projectRoot, { research: hasResearch });
@@ -307,10 +371,18 @@ export async function installCommand(opts = {}) {
307
371
  // 15. Create per-skill symlinks (.claude/skills/lumi-*) for Claude Code
308
372
  const symlinkStrategies = {};
309
373
  if (ideTargets.includes('claude_code')) {
310
- const { strategies } = await createSkillSymlinks(
311
- projectRoot, skillRows, existingManifest, reLink, colors, t
374
+ const { strategies, errors } = await createSkillSymlinks(
375
+ projectRoot, skillRows, existingManifest, effectiveReLink, colors, t
312
376
  );
313
377
  Object.assign(symlinkStrategies, strategies);
378
+ if (errors.length > 0) {
379
+ const e = new Error(
380
+ `SKILL_LINKS_INCOMPLETE: ${errors.length} of ${skillRows.length} Claude skill links failed: ` +
381
+ errors.map(item => `${item.skill}: ${item.error.message}`).join('; '),
382
+ );
383
+ e.code = 2;
384
+ throw e;
385
+ }
314
386
  }
315
387
 
316
388
  // 16. Build files-manifest rows
@@ -677,7 +749,9 @@ function applyInstallOverrides(answers, opts) {
677
749
  const next = { ...answers };
678
750
  const priorLocale = answers.locale ?? null;
679
751
 
680
- // Locale: --lang flag overrides; case-insensitive normalize.
752
+ // Locale: --lang overrides the interactive selector. Keep the installed
753
+ // locale in answers.locale until this point so default language values can
754
+ // cascade correctly when an existing destination is selected.
681
755
  // Pre-loadLocale error → EN-only string (chicken-and-egg, machine-readable).
682
756
  if (opts.lang !== undefined && opts.lang !== null && opts.lang !== '') {
683
757
  const normalized = String(opts.lang).toLowerCase().trim();
@@ -687,9 +761,12 @@ function applyInstallOverrides(answers, opts) {
687
761
  throw e;
688
762
  }
689
763
  next.locale = normalized;
764
+ } else if (next.selectedLocale) {
765
+ next.locale = next.selectedLocale;
690
766
  } else if (!next.locale) {
691
767
  next.locale = 'en';
692
768
  }
769
+ delete next.selectedLocale;
693
770
 
694
771
  // Cascade: if --lang changed locale and user didn't explicitly pass language
695
772
  // overrides, refresh the language defaults to match the new locale.
@@ -929,6 +1006,113 @@ function ideTargetStubFiles(ideTargets) {
929
1006
  .filter(Boolean);
930
1007
  }
931
1008
 
1009
+ async function removeManagedFileIfUnchanged(projectRoot, relPath, previousFileRows, colors, t) {
1010
+ const absPath = safePath(projectRoot, relPath);
1011
+ try {
1012
+ await access(absPath, fsConstants.F_OK);
1013
+ } catch (err) {
1014
+ if (err.code === 'ENOENT') return;
1015
+ throw err;
1016
+ }
1017
+
1018
+ const previous = previousFileRows.find(row => row.relative_path === relPath);
1019
+ if (previous?.sha256) {
1020
+ try {
1021
+ if (await fileHash(absPath) === previous.sha256) {
1022
+ await unlink(absPath);
1023
+ return;
1024
+ }
1025
+ } catch (_) {}
1026
+ }
1027
+
1028
+ console.log(colors.yellow(t('warn.preserved_modified_file', { path: relPath })));
1029
+ }
1030
+
1031
+ function previousManagedSkillRows(previousSkillRows, existingManifest) {
1032
+ const rowsById = new Map(previousSkillRows.map(row => [row.canonical_id, row]));
1033
+ const previousStrategies = existingManifest?.symlinkStrategies ?? {};
1034
+ const claudeWasSelected = existingManifest?.ideTargets?.includes('claude_code') ?? false;
1035
+ const previousPacks = Object.keys(existingManifest?.packs ?? {});
1036
+ for (const skill of getSkillDefs(previousPacks)) {
1037
+ if (!rowsById.has(skill.canonicalId)) {
1038
+ rowsById.set(skill.canonicalId, {
1039
+ canonical_id: skill.canonicalId,
1040
+ relative_path: join('.agents', 'skills', skill.canonicalId),
1041
+ target_link_path: existingManifest?.ideTargets?.includes('claude_code')
1042
+ ? join('.claude', 'skills', skill.canonicalId)
1043
+ : '',
1044
+ });
1045
+ }
1046
+ }
1047
+ for (const canonicalId of Object.keys(previousStrategies)) {
1048
+ if (!rowsById.has(canonicalId)) {
1049
+ rowsById.set(canonicalId, {
1050
+ canonical_id: canonicalId,
1051
+ relative_path: join('.agents', 'skills', canonicalId),
1052
+ target_link_path: join('.claude', 'skills', canonicalId),
1053
+ });
1054
+ }
1055
+ }
1056
+ return [...rowsById.values()].map(row => ({
1057
+ ...row,
1058
+ managed_link: claudeWasSelected
1059
+ || Object.prototype.hasOwnProperty.call(previousStrategies, row.canonical_id),
1060
+ }));
1061
+ }
1062
+
1063
+ async function removeManagedSkillLink(projectRoot, skill) {
1064
+ if (skill.managed_link === false) return;
1065
+ const relPath = skill.target_link_path || join('.claude', 'skills', skill.canonical_id);
1066
+ await rm(safePath(projectRoot, relPath), { recursive: true, force: true });
1067
+ }
1068
+
1069
+ async function reconcileRemovedIdeTargets({
1070
+ projectRoot,
1071
+ previousIdeTargets,
1072
+ currentIdeTargets,
1073
+ previousFileRows,
1074
+ previousSkillRows,
1075
+ colors,
1076
+ t,
1077
+ }) {
1078
+ const removedTargets = previousIdeTargets.filter(target => !currentIdeTargets.includes(target));
1079
+ for (const target of removedTargets) {
1080
+ if (target === 'claude_code') {
1081
+ await Promise.all(previousSkillRows.map(skill => removeManagedSkillLink(projectRoot, skill)));
1082
+ }
1083
+
1084
+ const relPath = ideTargetStubFiles([target])[0];
1085
+ if (relPath) {
1086
+ await removeManagedFileIfUnchanged(projectRoot, relPath, previousFileRows, colors, t);
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ async function reconcileRemovedSkills(projectRoot, previousSkillRows, currentSkillRows) {
1092
+ const currentIds = new Set(currentSkillRows.map(row => row.canonical_id));
1093
+ const obsolete = previousSkillRows.filter(row => !currentIds.has(row.canonical_id));
1094
+
1095
+ for (const skill of obsolete) {
1096
+ if (skill.relative_path) {
1097
+ await rm(safePath(projectRoot, skill.relative_path), { recursive: true, force: true });
1098
+ }
1099
+ await removeManagedSkillLink(projectRoot, skill);
1100
+ }
1101
+ }
1102
+
1103
+ async function cleanupRemovedResearchPack(projectRoot, previousFileRows, colors, t) {
1104
+ await Promise.all(RESEARCH_TOOL_FILES.map(file => (
1105
+ rm(safePath(projectRoot, join('_lumina', 'tools', file)), { force: true })
1106
+ )));
1107
+ await removeManagedFileIfUnchanged(
1108
+ projectRoot,
1109
+ '.env.example',
1110
+ previousFileRows,
1111
+ colors,
1112
+ t,
1113
+ );
1114
+ }
1115
+
932
1116
  function buildIdeStub(target, vars) {
933
1117
  const name = vars.project_name || 'this wiki';
934
1118
  switch (target) {
@@ -983,7 +1167,7 @@ async function copyChangelog(projectRoot) {
983
1167
  }
984
1168
  }
985
1169
 
986
- async function copySkills(projectRoot, packs) {
1170
+ async function copySkills(projectRoot, packs, { claudeCode = false } = {}) {
987
1171
  const skillRows = [];
988
1172
  const skillDefs = getSkillDefs(packs);
989
1173
 
@@ -1003,7 +1187,7 @@ async function copySkills(projectRoot, packs) {
1003
1187
  pack: skill.pack,
1004
1188
  source: 'built-in',
1005
1189
  relative_path: `.agents/skills/${skill.canonicalId}`,
1006
- target_link_path: join('.claude', 'skills', skill.canonicalId),
1190
+ target_link_path: claudeCode ? join('.claude', 'skills', skill.canonicalId) : '',
1007
1191
  version: PKG.version,
1008
1192
  });
1009
1193
  }
@@ -1050,6 +1234,7 @@ function getSkillDefs(packs) {
1050
1234
  { name: 'prefill', canonicalId: 'lumi-research-prefill', displayName: '/lumi-research-prefill' },
1051
1235
  { name: 'setup', canonicalId: 'lumi-research-setup', displayName: '/lumi-research-setup' },
1052
1236
  { name: 'topic', canonicalId: 'lumi-research-topic', displayName: '/lumi-research-topic' },
1237
+ { name: 'rank', canonicalId: 'lumi-research-rank', displayName: '/lumi-research-rank' },
1053
1238
  { name: 'watchlist', canonicalId: 'lumi-research-watchlist', displayName: '/lumi-research-watchlist' },
1054
1239
  { name: 'watch-run', canonicalId: 'lumi-research-watch-run', displayName: '/lumi-research-watch-run' },
1055
1240
  ];
@@ -1085,13 +1270,7 @@ function getSkillDefs(packs) {
1085
1270
  async function copyTools(projectRoot, { research }) {
1086
1271
  const destDir = join(projectRoot, '_lumina', 'tools');
1087
1272
  const coreTools = ['extract_pdf.py', 'fetch_pdf.py', 'id_utils.py'];
1088
- const researchTools = [
1089
- '_env.py', '_cache.py', 'discover.py', 'init_discovery.py', 'prepare_source.py',
1090
- 'fetch_arxiv.py', 'fetch_wikipedia.py', 'fetch_s2.py', 'fetch_deepxiv.py',
1091
- 'fetch_openalex.py', 'fetch_unpaywall.py', 'fetch_core.py', 'resolve_pdf.py',
1092
- 'fetch_rss.py',
1093
- ];
1094
- const toolFiles = research ? [...coreTools, ...researchTools] : coreTools;
1273
+ const toolFiles = research ? [...coreTools, ...RESEARCH_TOOL_FILES] : coreTools;
1095
1274
  // Parallelize: each copy is independent and destDir already exists.
1096
1275
  // Sequential awaits were the main Windows cold-start regression in v1.4
1097
1276
  // (~30 ms per file × 14 files dominates on NTFS + Defender).
@@ -1144,6 +1323,10 @@ async function renderEnvExample(projectRoot) {
1144
1323
  `OPENALEX_API_KEY=\n\n` +
1145
1324
  `# DeepXiv token (optional; enables full-text PDF access)\n` +
1146
1325
  `DEEPXIV_TOKEN=\n\n` +
1326
+ `# Scite.ai API key (optional; enables Smart Citation tallies in /lumi-research-rank)\n` +
1327
+ `SCITE_API_KEY=\n\n` +
1328
+ `# Altmetric API key (optional; enables attention scores in /lumi-research-rank)\n` +
1329
+ `ALTMETRIC_API_KEY=\n\n` +
1147
1330
  `# arXiv does not require an API key in v0.1\n`;
1148
1331
  }
1149
1332
  await atomicWrite(destPath, content);
@@ -1213,6 +1396,7 @@ async function seedWikiFiles(projectRoot) {
1213
1396
 
1214
1397
  async function createSkillSymlinks(projectRoot, skillRows, existingManifest, reLink, colors, t = null) {
1215
1398
  const strategies = {};
1399
+ const errors = [];
1216
1400
 
1217
1401
  for (const skill of skillRows) {
1218
1402
  const target = resolve(projectRoot, skill.relative_path);
@@ -1227,6 +1411,7 @@ async function createSkillSymlinks(projectRoot, skillRows, existingManifest, reL
1227
1411
  console.log(colors.yellow(` [warn] ${result.message}`));
1228
1412
  }
1229
1413
  } catch (err) {
1414
+ errors.push({ skill: skill.canonical_id, error: err });
1230
1415
  const msg = t
1231
1416
  ? t('error.symlink', { skill: skill.canonical_id, message: err.message })
1232
1417
  : ` [error] Failed to link ${skill.canonical_id}: ${err.message}`;
@@ -1234,7 +1419,7 @@ async function createSkillSymlinks(projectRoot, skillRows, existingManifest, reL
1234
1419
  }
1235
1420
  }
1236
1421
 
1237
- return { strategies };
1422
+ return { strategies, errors };
1238
1423
  }
1239
1424
 
1240
1425
  async function buildFilesManifest(projectRoot, packs, pkgVersion) {