project-librarian 0.2.0 → 0.2.1

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.
@@ -39,6 +39,8 @@ const os = __importStar(require("node:os"));
39
39
  const path = __importStar(require("node:path"));
40
40
  const args_1 = require("./args");
41
41
  const skillName = "project-librarian";
42
+ const allAgentTargets = ["codex", "claude", "cursor", "gemini"];
43
+ const legacyBothAgentTargets = ["codex", "claude"];
42
44
  const packageFiles = [
43
45
  "SKILL.md",
44
46
  "dist",
@@ -61,19 +63,23 @@ function installScope() {
61
63
  return fail(`invalid --scope: ${scope}; expected user or project`);
62
64
  }
63
65
  function installAgents() {
64
- const value = (0, args_1.argValue)("--agents") || "both";
66
+ const value = (0, args_1.argValue)("--agents") || "all";
65
67
  const parts = value.split(",").map((item) => item.trim()).filter(Boolean);
66
68
  const agents = new Set();
67
69
  for (const part of parts) {
68
- if (part === "both" || part === "all") {
69
- agents.add("codex");
70
- agents.add("claude");
70
+ if (part === "all") {
71
+ for (const agent of allAgentTargets)
72
+ agents.add(agent);
71
73
  }
72
- else if (part === "codex" || part === "claude") {
74
+ else if (part === "both") {
75
+ for (const agent of legacyBothAgentTargets)
76
+ agents.add(agent);
77
+ }
78
+ else if (allAgentTargets.includes(part)) {
73
79
  agents.add(part);
74
80
  }
75
81
  else {
76
- return fail(`invalid --agents entry: ${part}; expected codex, claude, or both`);
82
+ return fail(`invalid --agents entry: ${part}; expected codex, claude, cursor, gemini, all, or legacy both`);
77
83
  }
78
84
  }
79
85
  return Array.from(agents);
@@ -85,10 +91,23 @@ function userAgentRoot(agent) {
85
91
  const home = os.homedir();
86
92
  if (agent === "codex")
87
93
  return process.env.CODEX_HOME || path.join(home, ".codex");
88
- return process.env.CLAUDE_HOME || path.join(home, ".claude");
94
+ if (agent === "claude")
95
+ return process.env.CLAUDE_HOME || path.join(home, ".claude");
96
+ if (agent === "cursor")
97
+ return process.env.CURSOR_HOME || path.join(home, ".cursor");
98
+ return process.env.GEMINI_HOME || path.join(home, ".gemini");
99
+ }
100
+ function projectAgentRoot(agent) {
101
+ if (agent === "codex")
102
+ return ".codex";
103
+ if (agent === "claude")
104
+ return ".claude";
105
+ if (agent === "cursor")
106
+ return ".cursor";
107
+ return ".gemini";
89
108
  }
90
109
  function installTarget(agent, scope) {
91
- const base = scope === "user" ? userAgentRoot(agent) : path.join(process.cwd(), agent === "codex" ? ".codex" : ".claude");
110
+ const base = scope === "user" ? userAgentRoot(agent) : path.join(process.cwd(), projectAgentRoot(agent));
92
111
  return path.join(base, "skills", skillName);
93
112
  }
94
113
  function sameFile(source, target) {
@@ -134,7 +153,7 @@ function runInstallSkillMode() {
134
153
  console.log(`Project Librarian skill ${dryRun ? "install dry-run" : "install"} complete.`);
135
154
  console.log(`scope: ${scope}`);
136
155
  console.log(`agents: ${agents.join(", ")}`);
137
- console.log("note: install-skill only installs the reusable skill files; it does not create or update AGENTS.md, CLAUDE.md, wiki/, .codex/hooks.json, or .claude/settings.json.");
156
+ console.log("note: install-skill only installs the reusable skill files; it does not create or update AGENTS.md, CLAUDE.md, GEMINI.md, wiki/, .cursor/rules/, .cursor/hooks.json, .gemini/settings.json, .codex/hooks.json, or .claude/settings.json.");
138
157
  console.log("next: agents should run the installed local project-librarian runner from the target project root; direct shell users can still run `npx project-librarian` when registry access is available.");
139
158
  for (const [label, status] of rows) {
140
159
  console.log(`${status.padEnd(7)} ${label}`);
package/dist/migration.js CHANGED
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.classifyMarkdown = classifyMarkdown;
37
+ exports.extractMigrationUnits = extractMigrationUnits;
38
+ exports.collectMigrationCoverageDiagnostics = collectMigrationCoverageDiagnostics;
37
39
  exports.markdownTableRows = markdownTableRows;
38
40
  exports.buildInbox = buildInbox;
39
41
  exports.timestampSuffix = timestampSuffix;
@@ -67,6 +69,157 @@ function classifyMarkdown(relativePath, text) {
67
69
  function markdownTableCell(value) {
68
70
  return String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
69
71
  }
72
+ function slugPart(value) {
73
+ return value.toLowerCase().replace(/[^a-z0-9가-힣ぁ-んァ-ン一-龥]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "unit";
74
+ }
75
+ function unitSummary(value) {
76
+ return value.replace(/\s+/g, " ").trim().slice(0, 180);
77
+ }
78
+ function nextUnitId(legacyPath, index, summary) {
79
+ return `${legacyPath}#u${String(index).padStart(3, "0")}-${slugPart(summary)}`;
80
+ }
81
+ function extractMigrationUnits(legacyPath, text) {
82
+ const body = (0, workspace_1.stripMetadataHeader)(text);
83
+ const lines = body.split(/\r?\n/);
84
+ const units = [];
85
+ let heading = "";
86
+ let paragraph = [];
87
+ let inCodeFence = false;
88
+ let codeBlock = [];
89
+ const pushUnit = (type, value) => {
90
+ const summary = unitSummary(value);
91
+ if (!summary)
92
+ return;
93
+ units.push({
94
+ id: nextUnitId(legacyPath, units.length + 1, summary),
95
+ legacyPath,
96
+ type,
97
+ heading,
98
+ summary,
99
+ });
100
+ };
101
+ const flushParagraph = () => {
102
+ if (paragraph.length === 0)
103
+ return;
104
+ pushUnit("paragraph", paragraph.join(" "));
105
+ paragraph = [];
106
+ };
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ if (/^```/.test(trimmed)) {
110
+ if (inCodeFence) {
111
+ codeBlock.push(line);
112
+ pushUnit("code-block", codeBlock.join("\n"));
113
+ codeBlock = [];
114
+ inCodeFence = false;
115
+ }
116
+ else {
117
+ flushParagraph();
118
+ inCodeFence = true;
119
+ codeBlock = [line];
120
+ }
121
+ continue;
122
+ }
123
+ if (inCodeFence) {
124
+ codeBlock.push(line);
125
+ continue;
126
+ }
127
+ if (!trimmed) {
128
+ flushParagraph();
129
+ continue;
130
+ }
131
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
132
+ if (headingMatch?.[2]) {
133
+ flushParagraph();
134
+ heading = headingMatch[2].trim();
135
+ pushUnit("heading", heading);
136
+ continue;
137
+ }
138
+ if (/^\|.+\|$/.test(trimmed) && !/^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(trimmed)) {
139
+ flushParagraph();
140
+ pushUnit("table-row", trimmed);
141
+ continue;
142
+ }
143
+ if (/^([-*+]|\d+[.)])\s+/.test(trimmed)) {
144
+ flushParagraph();
145
+ pushUnit("list-item", trimmed);
146
+ continue;
147
+ }
148
+ paragraph.push(trimmed);
149
+ }
150
+ if (inCodeFence && codeBlock.length > 0)
151
+ pushUnit("code-block", codeBlock.join("\n"));
152
+ flushParagraph();
153
+ return units;
154
+ }
155
+ function coverageTableRows(units) {
156
+ if (units.length === 0)
157
+ return "| none | - | - | - | - | pending | - | - |\n";
158
+ return units.map((unit) => `| ${markdownTableCell(unit.id)} | ${markdownTableCell(unit.legacyPath)} | ${unit.type} | ${markdownTableCell(unit.heading || "-")} | ${markdownTableCell(unit.summary)} | pending | - | - |`).join("\n") + "\n";
159
+ }
160
+ function isMigrationCoverageStatus(value) {
161
+ return ["adopted", "merged", "superseded", "rejected", "resolved", "needs-human-review", "pending"].includes(value);
162
+ }
163
+ function legacyWikiRoots() {
164
+ if (!fs.existsSync(workspace_1.root))
165
+ return [];
166
+ return fs.readdirSync(workspace_1.root, { withFileTypes: true })
167
+ .filter((entry) => entry.isDirectory() && /^wiki_legacy(?:_|$)/.test(entry.name))
168
+ .map((entry) => entry.name)
169
+ .sort();
170
+ }
171
+ function expectedMigrationUnits() {
172
+ return legacyWikiRoots()
173
+ .flatMap((legacyRoot) => (0, wiki_files_1.walkMarkdownFiles)((0, workspace_1.abs)(legacyRoot), [], (0, workspace_1.abs)(legacyRoot)))
174
+ .flatMap((file) => extractMigrationUnits(file.basePath, (0, workspace_1.read)(file.path)));
175
+ }
176
+ function collectMigrationCoverageDiagnostics() {
177
+ const units = expectedMigrationUnits();
178
+ if (units.length === 0)
179
+ return [];
180
+ if (!(0, workspace_1.exists)("wiki/migration/coverage.md")) {
181
+ return [{
182
+ code: "migration-coverage-missing",
183
+ severity: "error",
184
+ file: "wiki/migration/coverage.md",
185
+ message: "migration unit coverage ledger is missing; run --migrate to account for legacy meaning units",
186
+ }];
187
+ }
188
+ const diagnostics = [];
189
+ const expectedIds = new Set(units.map((unit) => unit.id));
190
+ const seenIds = new Set();
191
+ const rows = (0, wiki_files_1.parseMarkdownTableRows)((0, workspace_1.read)("wiki/migration/coverage.md"), 8).filter((cells) => cells[0] !== "Unit ID");
192
+ for (const cells of rows) {
193
+ const id = cells[0] || "";
194
+ const status = String(cells[5] || "").trim().toLowerCase();
195
+ const target = String(cells[6] || "").trim();
196
+ if (seenIds.has(id)) {
197
+ diagnostics.push({ code: "migration-duplicate-unit", severity: "error", file: "wiki/migration/coverage.md", message: `duplicate migration unit row: ${id}` });
198
+ }
199
+ seenIds.add(id);
200
+ if (!expectedIds.has(id)) {
201
+ diagnostics.push({ code: "migration-stale-unit", severity: "warn", file: "wiki/migration/coverage.md", message: `coverage row does not match current legacy units: ${id}` });
202
+ }
203
+ if (!isMigrationCoverageStatus(status)) {
204
+ diagnostics.push({ code: "migration-invalid-status", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} has invalid status: ${status || "(blank)"}` });
205
+ }
206
+ if (["adopted", "merged"].includes(status) && !/^wiki\/(canonical|decisions|sources|meta)\//.test(target)) {
207
+ diagnostics.push({ code: "migration-missing-target", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} is ${status} but target is not a new wiki page` });
208
+ }
209
+ if (/\bwiki_legacy(?:_|\b|\/)/.test(target)) {
210
+ diagnostics.push({ code: "migration-legacy-target", severity: "error", file: "wiki/migration/coverage.md", message: `unit ${id} targets wiki_legacy* instead of migrated new-wiki truth` });
211
+ }
212
+ if (status === "pending") {
213
+ diagnostics.push({ code: "migration-pending-unit", severity: "warn", file: "wiki/migration/coverage.md", message: `unit ${id} is still pending migration review` });
214
+ }
215
+ }
216
+ for (const unit of units) {
217
+ if (!seenIds.has(unit.id)) {
218
+ diagnostics.push({ code: "migration-unaccounted-unit", severity: "error", file: unit.legacyPath, message: `legacy meaning unit missing from coverage ledger: ${unit.id}` });
219
+ }
220
+ }
221
+ return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
222
+ }
70
223
  function markdownTableRows(items) {
71
224
  if (items.length === 0)
72
225
  return "| none | - | - | - |\n";
@@ -87,6 +240,22 @@ function buildInbox(title, description, items) {
87
240
  | --- | --- | --- | --- |
88
241
  ${markdownTableRows(items)}`;
89
242
  }
243
+ function migrationBatchScope(legacyRoot) {
244
+ return `${workspace_1.today} migration batch${legacyRoot && legacyRoot !== "none" ? ` from ${legacyRoot}` : ""}`;
245
+ }
246
+ function semanticCompletionValue(complete, batchScope) {
247
+ if (complete)
248
+ return `yes, for the ${batchScope} only`;
249
+ return `no, the ${batchScope} still has unresolved rows`;
250
+ }
251
+ function completionScopeSection(batchScope) {
252
+ return `## Completion Scope
253
+
254
+ - This page records the ${batchScope} only.
255
+ - It does not mean future requests to build a new wiki from the existing wiki should reuse current \`wiki/\` in place.
256
+ - For a fresh rebuild request, treat current \`wiki/\` as the legacy source unless the user says otherwise: preserve it as \`wiki_legacy*\`, create a fresh standard \`wiki/\`, migrate/adopt content from the preserved legacy source, then refresh routing and diagnostics.
257
+ `;
258
+ }
90
259
  function timestampSuffix() {
91
260
  return new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "_");
92
261
  }
@@ -156,6 +325,22 @@ function runMigrationMode(migrationState) {
156
325
  | Legacy Source | Classification | Title | Size (bytes) | Summary |
157
326
  | --- | --- | --- | ---: | --- |
158
327
  ${inventoryRows}`;
328
+ const units = items.flatMap((item) => extractMigrationUnits(item.legacyPath, (0, workspace_1.read)(item.path)));
329
+ const coverage = `${(0, templates_1.metadata)("migration-coverage", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration unit coverage statuses change")}
330
+ # Migration Coverage Ledger
331
+
332
+ ## TL;DR
333
+
334
+ - Generated: ${workspace_1.today}
335
+ - Legacy root: ${legacyPath || "none"}
336
+ - Legacy meaning units: ${units.length}
337
+ - Every legacy heading, paragraph, list item, table row, and code block should remain accounted for.
338
+ - Status values: pending, adopted, merged, superseded, rejected, resolved, needs-human-review.
339
+ - \`adopted\` and \`merged\` rows require a new-wiki target under \`wiki/canonical/\`, \`wiki/decisions/\`, \`wiki/sources/\`, or \`wiki/meta/\`.
340
+
341
+ | Unit ID | Legacy Source | Type | Heading | Summary | Status | Target | Note |
342
+ | --- | --- | --- | --- | --- | --- | --- | --- |
343
+ ${coverageTableRows(units)}`;
159
344
  const plan = `${(0, templates_1.metadata)("migration-plan", "short", "wiki/meta/wiki-ops-v1-decisions.md", "migration procedure or status changes")}
160
345
  # Migration Plan
161
346
 
@@ -189,6 +374,8 @@ ${inventoryRows}`;
189
374
  - coverage: ${items.length === markdownFiles.length ? "pass" : "fail"}
190
375
  - This verifies file coverage only. Semantic completeness is confirmed after inbox statuses are resolved.
191
376
 
377
+ ${completionScopeSection(migrationBatchScope(legacyPath || "none"))}
378
+
192
379
  | Legacy Source | Classification | New Wiki Target | Coverage | Semantic Status |
193
380
  | --- | --- | --- | --- | --- |
194
381
  ${verificationRows}`;
@@ -198,12 +385,13 @@ ${verificationRows}`;
198
385
  - ${workspace_1.today}: preserved existing wiki at \`${legacyPath || "no wiki_legacy"}\` and regenerated the standard wiki structure.
199
386
  - Scanned ${items.length} legacy markdown files and created migration inventory, plan, verification, and inbox files.
200
387
  - Do not delete \`${legacyPath || "wiki_legacy"}\` until all migration inbox items are adopted/rejected/resolved and needs-human-review is 0.
388
+ - Migration completion status is scoped to this batch only. For a future fresh rebuild request, treat current \`wiki/\` as the legacy source unless the user says otherwise.
201
389
  <!-- PROJECT-WIKI-MIGRATION:END -->`;
202
390
  const migrationIndexBlock = `<!-- PROJECT-WIKI-MIGRATION:START -->
203
391
  ## Migration
204
392
 
205
393
  - [[migration/plan]]
206
- - Read when: migration procedure or status matters.
394
+ - Read when: migration procedure, fresh rebuild procedure, or status matters.
207
395
  - Update when: migration procedure or state changes.
208
396
  - Token budget: short.
209
397
  - [[migration/inventory]]
@@ -214,6 +402,10 @@ ${verificationRows}`;
214
402
  - Read when: legacy file coverage or semantic migration status matters.
215
403
  - Update when: migration inbox statuses change.
216
404
  - Token budget: on-demand.
405
+ - [[migration/coverage]]
406
+ - Read when: checking whether legacy meaning units were adopted, merged, superseded, rejected, resolved, or marked for review.
407
+ - Update when: unit-level migration coverage statuses, targets, or notes change.
408
+ - Token budget: on-demand.
217
409
  - [[migration/review]]
218
410
  - Read when: semantic migration review status matters.
219
411
  - Update when: \`--review-migration\` syncs migration state.
@@ -234,6 +426,7 @@ ${verificationRows}`;
234
426
  const results = [];
235
427
  (0, workspace_1.mkdirp)("wiki/migration");
236
428
  results.push(["wiki/migration/inventory.md", (0, workspace_1.writeManaged)("wiki/migration/inventory.md", inventory)]);
429
+ results.push(["wiki/migration/coverage.md", (0, workspace_1.writeManaged)("wiki/migration/coverage.md", coverage)]);
237
430
  results.push(["wiki/migration/plan.md", (0, workspace_1.writeManaged)("wiki/migration/plan.md", plan)]);
238
431
  results.push(["wiki/migration/verification.md", (0, workspace_1.writeManaged)("wiki/migration/verification.md", verification)]);
239
432
  results.push(["wiki/canonical/migration-inbox.md", (0, workspace_1.writeManaged)("wiki/canonical/migration-inbox.md", buildInbox("Canonical Migration Inbox", "Legacy content that may belong in current project truth.", byKind.canonical.concat(byKind.other)))]);
@@ -305,6 +498,9 @@ function runReviewMigrationMode() {
305
498
  const pending = counts.pending || 0;
306
499
  const needsHuman = counts["needs-human-review"] || 0;
307
500
  const complete = pending === 0 && needsHuman === 0;
501
+ const legacyRoot = (verificationText.match(/^- legacy root:\s*(.+)$/m) || [])[1] || "unknown";
502
+ const batchScope = migrationBatchScope(legacyRoot);
503
+ const completionValue = semanticCompletionValue(complete, batchScope);
308
504
  const reviewRows = reviewedRows.length === 0
309
505
  ? "| none | - | - | - | - |\n"
310
506
  : reviewedRows.map((row) => `| ${markdownTableCell(row.legacyPath)} | ${row.kind} | ${row.inboxStatus} | ${row.semanticStatus} | ${markdownTableCell(row.note)} |`).join("\n") + "\n";
@@ -320,7 +516,9 @@ function runReviewMigrationMode() {
320
516
  - resolved: ${counts.resolved || 0}
321
517
  - pending: ${pending}
322
518
  - needs-human-review: ${needsHuman}
323
- - semantic migration complete: ${complete ? "yes" : "no"}
519
+ - semantic migration complete: ${completionValue}
520
+
521
+ ${completionScopeSection(batchScope)}
324
522
 
325
523
  | Legacy Source | Classification | Inbox Status | Semantic Status | Evidence |
326
524
  | --- | --- | --- | --- | --- |
@@ -328,7 +526,6 @@ ${reviewRows}`;
328
526
  const verificationRowsText = reviewedRows.length === 0
329
527
  ? "| none | - | - | pass | - |\n"
330
528
  : reviewedRows.map((row) => `| ${markdownTableCell(row.legacyPath)} | ${row.kind} | ${row.target} | ${row.coverage} | ${row.semanticStatus} |`).join("\n") + "\n";
331
- const legacyRoot = (verificationText.match(/^- legacy root:\s*(.+)$/m) || [])[1] || "unknown";
332
529
  const verification = `${(0, templates_1.metadata)("migration-verification", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "migration inbox items are adopted, rejected, resolved, or marked needs-human-review")}
333
530
  # Migration Verification
334
531
 
@@ -338,10 +535,12 @@ ${reviewRows}`;
338
535
  - legacy markdown files: ${reviewedRows.length}
339
536
  - mapped files: ${reviewedRows.filter((row) => row.coverage === "mapped").length}
340
537
  - coverage: ${reviewedRows.every((row) => row.coverage === "mapped") ? "pass" : "fail"}
341
- - semantic migration complete: ${complete ? "yes" : "no"}
538
+ - semantic migration complete: ${completionValue}
342
539
  - pending: ${pending}
343
540
  - needs-human-review: ${needsHuman}
344
541
 
542
+ ${completionScopeSection(batchScope)}
543
+
345
544
  | Legacy Source | Classification | New Wiki Target | Coverage | Semantic Status |
346
545
  | --- | --- | --- | --- | --- |
347
546
  ${verificationRowsText}`;