@tikoci/rosetta 0.6.6 → 0.6.8

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.
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * extract-skills.ts — Extract agent skill guides from tikoci/routeros-skills.
4
+ *
5
+ * Fetches SKILL.md files and reference documents from GitHub (or a local path),
6
+ * parses YAML frontmatter, and populates skills + skill_references tables.
7
+ *
8
+ * Usage:
9
+ * bun run src/extract-skills.ts # Fetch from GitHub API
10
+ * bun run src/extract-skills.ts /path/to/routeros-skills # Use local directory
11
+ * bun run src/extract-skills.ts --from-cache # Re-extract from cached skills/ dir
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { db, initDb } from "./db.ts";
17
+
18
+ // ── Configuration ──
19
+
20
+ const PROJECT_ROOT = join(import.meta.dirname, "..");
21
+ const CACHE_DIR = join(PROJECT_ROOT, "skills");
22
+
23
+ const GITHUB_REPO = "tikoci/routeros-skills";
24
+ const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_REPO}`;
25
+ const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
26
+
27
+ const FROM_CACHE = process.argv.includes("--from-cache");
28
+
29
+ /** Local path override: first non-flag CLI arg */
30
+ const localPath = process.argv.slice(2).find((a) => !a.startsWith("--"));
31
+
32
+ // ── Types ──
33
+
34
+ interface SkillData {
35
+ name: string;
36
+ description: string;
37
+ content: string;
38
+ sourceUrl: string;
39
+ references: Array<{
40
+ path: string;
41
+ filename: string;
42
+ content: string;
43
+ }>;
44
+ }
45
+
46
+ // ── YAML frontmatter parsing (minimal — no dependency) ──
47
+
48
+ function parseFrontmatter(markdown: string): { meta: Record<string, string>; content: string } {
49
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
50
+ if (!match) return { meta: {}, content: markdown };
51
+
52
+ const meta: Record<string, string> = {};
53
+ for (const line of match[1].split("\n")) {
54
+ const colonIdx = line.indexOf(":");
55
+ if (colonIdx === -1) continue;
56
+ const key = line.slice(0, colonIdx).trim();
57
+ let value = line.slice(colonIdx + 1).trim();
58
+ // Strip surrounding quotes
59
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
60
+ value = value.slice(1, -1);
61
+ }
62
+ meta[key] = value;
63
+ }
64
+ return { meta, content: match[2] };
65
+ }
66
+
67
+ function countWords(text: string): number {
68
+ return text.split(/\s+/).filter(Boolean).length;
69
+ }
70
+
71
+ // ── GitHub API fetching ──
72
+
73
+ async function getDefaultBranchSha(): Promise<string> {
74
+ const res = await fetch(`${GITHUB_API_BASE}/commits/HEAD`, {
75
+ headers: { Accept: "application/vnd.github.v3+json" },
76
+ });
77
+ if (!res.ok) throw new Error(`Failed to get HEAD SHA: HTTP ${res.status}`);
78
+ const data = (await res.json()) as { sha: string };
79
+ return data.sha;
80
+ }
81
+
82
+ async function listSkillDirs(sha: string): Promise<string[]> {
83
+ const res = await fetch(`${GITHUB_API_BASE}/contents/?ref=${sha}`, {
84
+ headers: { Accept: "application/vnd.github.v3+json" },
85
+ });
86
+ if (!res.ok) throw new Error(`Failed to list repo contents: HTTP ${res.status}`);
87
+ const entries = (await res.json()) as Array<{ name: string; type: string }>;
88
+ return entries
89
+ .filter((e) => e.type === "dir" && e.name.startsWith("routeros-"))
90
+ .map((e) => e.name);
91
+ }
92
+
93
+ async function fetchRawFile(sha: string, path: string): Promise<string | null> {
94
+ const url = `${GITHUB_RAW_BASE}/${sha}/${path}`;
95
+ const res = await fetch(url);
96
+ if (!res.ok) {
97
+ if (res.status === 404) return null;
98
+ throw new Error(`Failed to fetch ${path}: HTTP ${res.status}`);
99
+ }
100
+ return res.text();
101
+ }
102
+
103
+ async function listReferences(sha: string, skillName: string): Promise<string[]> {
104
+ const res = await fetch(`${GITHUB_API_BASE}/contents/${skillName}/references?ref=${sha}`, {
105
+ headers: { Accept: "application/vnd.github.v3+json" },
106
+ });
107
+ if (!res.ok) {
108
+ if (res.status === 404) return [];
109
+ throw new Error(`Failed to list references for ${skillName}: HTTP ${res.status}`);
110
+ }
111
+ const entries = (await res.json()) as Array<{ name: string; type: string }>;
112
+ return entries.filter((e) => e.type === "file" && e.name.endsWith(".md")).map((e) => e.name);
113
+ }
114
+
115
+ async function extractFromGitHub(): Promise<{ skills: SkillData[]; sha: string }> {
116
+ console.log(`Fetching skills from GitHub: ${GITHUB_REPO}`);
117
+ const sha = await getDefaultBranchSha();
118
+ console.log(` HEAD SHA: ${sha.slice(0, 12)}`);
119
+
120
+ const skillDirs = await listSkillDirs(sha);
121
+ console.log(` Found ${skillDirs.length} skill directories`);
122
+
123
+ const skills: SkillData[] = [];
124
+
125
+ for (const dir of skillDirs) {
126
+ const skillMd = await fetchRawFile(sha, `${dir}/SKILL.md`);
127
+ if (!skillMd) {
128
+ console.warn(` ⚠ ${dir}/SKILL.md not found, skipping`);
129
+ continue;
130
+ }
131
+
132
+ const { meta, content } = parseFrontmatter(skillMd);
133
+ const skill: SkillData = {
134
+ name: meta.name || dir,
135
+ description: meta.description || "",
136
+ content,
137
+ sourceUrl: `https://github.com/${GITHUB_REPO}/blob/${sha}/${dir}/SKILL.md`,
138
+ references: [],
139
+ };
140
+
141
+ // Fetch references
142
+ const refFiles = await listReferences(sha, dir);
143
+ for (const refFile of refFiles) {
144
+ const refContent = await fetchRawFile(sha, `${dir}/references/${refFile}`);
145
+ if (refContent) {
146
+ skill.references.push({
147
+ path: `references/${refFile}`,
148
+ filename: refFile,
149
+ content: refContent,
150
+ });
151
+ }
152
+ }
153
+
154
+ skills.push(skill);
155
+ console.log(` ✓ ${skill.name} (${countWords(content)} words, ${skill.references.length} refs)`);
156
+ }
157
+
158
+ // Cache to skills/ directory
159
+ cacheSkills(skills, sha);
160
+
161
+ return { skills, sha };
162
+ }
163
+
164
+ // ── Local path extraction ──
165
+
166
+ function extractFromLocal(dirPath: string): { skills: SkillData[]; sha: string } {
167
+ console.log(`Extracting skills from local path: ${dirPath}`);
168
+
169
+ // Try to get git SHA
170
+ let sha = "local";
171
+ try {
172
+ const gitResult = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: dirPath });
173
+ if (gitResult.exitCode === 0) {
174
+ sha = gitResult.stdout.toString().trim();
175
+ console.log(` Git SHA: ${sha.slice(0, 12)}`);
176
+ }
177
+ } catch { /* not a git repo, that's fine */ }
178
+
179
+ const entries = readdirSync(dirPath, { withFileTypes: true });
180
+ const skillDirs = entries.filter((e) => e.isDirectory() && e.name.startsWith("routeros-")).map((e) => e.name);
181
+ console.log(` Found ${skillDirs.length} skill directories`);
182
+
183
+ const skills: SkillData[] = [];
184
+
185
+ for (const dir of skillDirs) {
186
+ const skillPath = join(dirPath, dir, "SKILL.md");
187
+ if (!existsSync(skillPath)) {
188
+ console.warn(` ⚠ ${dir}/SKILL.md not found, skipping`);
189
+ continue;
190
+ }
191
+
192
+ const skillMd = readFileSync(skillPath, "utf-8");
193
+ const { meta, content } = parseFrontmatter(skillMd);
194
+ const skill: SkillData = {
195
+ name: meta.name || dir,
196
+ description: meta.description || "",
197
+ content,
198
+ sourceUrl: `https://github.com/${GITHUB_REPO}/blob/${sha}/${dir}/SKILL.md`,
199
+ references: [],
200
+ };
201
+
202
+ // Load references
203
+ const refsDir = join(dirPath, dir, "references");
204
+ if (existsSync(refsDir)) {
205
+ const refEntries = readdirSync(refsDir, { withFileTypes: true });
206
+ for (const ref of refEntries) {
207
+ if (ref.isFile() && ref.name.endsWith(".md")) {
208
+ skill.references.push({
209
+ path: `references/${ref.name}`,
210
+ filename: ref.name,
211
+ content: readFileSync(join(refsDir, ref.name), "utf-8"),
212
+ });
213
+ }
214
+ }
215
+ }
216
+
217
+ skills.push(skill);
218
+ console.log(` ✓ ${skill.name} (${countWords(content)} words, ${skill.references.length} refs)`);
219
+ }
220
+
221
+ // Cache to skills/ directory
222
+ cacheSkills(skills, sha);
223
+
224
+ return { skills, sha };
225
+ }
226
+
227
+ // ── Cache management ──
228
+
229
+ function cacheSkills(skills: SkillData[], sha: string) {
230
+ mkdirSync(CACHE_DIR, { recursive: true });
231
+
232
+ // Write metadata
233
+ const metadata = {
234
+ sha,
235
+ extracted_at: new Date().toISOString(),
236
+ skills: skills.map((s) => ({
237
+ name: s.name,
238
+ description: s.description,
239
+ word_count: countWords(s.content),
240
+ ref_count: s.references.length,
241
+ })),
242
+ };
243
+ writeFileSync(join(CACHE_DIR, "metadata.json"), JSON.stringify(metadata, null, 2));
244
+
245
+ // Write each skill
246
+ for (const skill of skills) {
247
+ const skillDir = join(CACHE_DIR, skill.name);
248
+ mkdirSync(skillDir, { recursive: true });
249
+
250
+ // Write SKILL.md with frontmatter reconstructed
251
+ const frontmatter = `---\nname: ${skill.name}\ndescription: "${skill.description}"\n---\n`;
252
+ writeFileSync(join(skillDir, "SKILL.md"), frontmatter + skill.content);
253
+
254
+ // Write references
255
+ if (skill.references.length > 0) {
256
+ const refsDir = join(skillDir, "references");
257
+ mkdirSync(refsDir, { recursive: true });
258
+ for (const ref of skill.references) {
259
+ writeFileSync(join(refsDir, ref.filename), ref.content);
260
+ }
261
+ }
262
+ }
263
+
264
+ console.log(` Cached ${skills.length} skills to ${CACHE_DIR}/`);
265
+ }
266
+
267
+ function extractFromCache(): { skills: SkillData[]; sha: string } {
268
+ console.log(`Extracting skills from cache: ${CACHE_DIR}/`);
269
+
270
+ if (!existsSync(join(CACHE_DIR, "metadata.json"))) {
271
+ throw new Error(`No cached skills found at ${CACHE_DIR}/metadata.json — run without --from-cache first`);
272
+ }
273
+
274
+ const metadata = JSON.parse(readFileSync(join(CACHE_DIR, "metadata.json"), "utf-8"));
275
+ const sha = metadata.sha || "cached";
276
+
277
+ const skills: SkillData[] = [];
278
+ const entries = readdirSync(CACHE_DIR, { withFileTypes: true });
279
+
280
+ for (const entry of entries) {
281
+ if (!entry.isDirectory() || !entry.name.startsWith("routeros-")) continue;
282
+
283
+ const skillPath = join(CACHE_DIR, entry.name, "SKILL.md");
284
+ if (!existsSync(skillPath)) continue;
285
+
286
+ const skillMd = readFileSync(skillPath, "utf-8");
287
+ const { meta, content } = parseFrontmatter(skillMd);
288
+ const skill: SkillData = {
289
+ name: meta.name || entry.name,
290
+ description: meta.description || "",
291
+ content,
292
+ sourceUrl: `https://github.com/${GITHUB_REPO}/blob/${sha}/${entry.name}/SKILL.md`,
293
+ references: [],
294
+ };
295
+
296
+ // Load cached references
297
+ const refsDir = join(CACHE_DIR, entry.name, "references");
298
+ if (existsSync(refsDir)) {
299
+ const refEntries = readdirSync(refsDir, { withFileTypes: true });
300
+ for (const ref of refEntries) {
301
+ if (ref.isFile() && ref.name.endsWith(".md")) {
302
+ skill.references.push({
303
+ path: `references/${ref.name}`,
304
+ filename: ref.name,
305
+ content: readFileSync(join(refsDir, ref.name), "utf-8"),
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ skills.push(skill);
312
+ console.log(` ✓ ${skill.name} (${countWords(content)} words, ${skill.references.length} refs)`);
313
+ }
314
+
315
+ return { skills, sha };
316
+ }
317
+
318
+ // ── Database population ──
319
+
320
+ function populateDb(skills: SkillData[], sha: string) {
321
+ const now = new Date().toISOString();
322
+
323
+ // Idempotent: delete existing data (respect FK order)
324
+ db.run("DELETE FROM skill_references");
325
+ db.run("DELETE FROM skills");
326
+
327
+ const insertSkill = db.prepare(`
328
+ INSERT INTO skills (name, description, content, source_repo, source_sha, source_url, word_count, extracted_at)
329
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
330
+ `);
331
+
332
+ const insertRef = db.prepare(`
333
+ INSERT INTO skill_references (skill_id, path, filename, content, word_count)
334
+ VALUES (?, ?, ?, ?, ?)
335
+ `);
336
+
337
+ const getSkillId = db.prepare("SELECT id FROM skills WHERE name = ?");
338
+
339
+ let totalWords = 0;
340
+ let totalRefs = 0;
341
+
342
+ for (const skill of skills) {
343
+ const wordCount = countWords(skill.content);
344
+ totalWords += wordCount;
345
+
346
+ insertSkill.run(
347
+ skill.name,
348
+ skill.description,
349
+ skill.content,
350
+ GITHUB_REPO,
351
+ sha,
352
+ skill.sourceUrl,
353
+ wordCount,
354
+ now,
355
+ );
356
+
357
+ const row = getSkillId.get(skill.name) as { id: number };
358
+ const skillId = row.id;
359
+
360
+ for (const ref of skill.references) {
361
+ const refWordCount = countWords(ref.content);
362
+ totalWords += refWordCount;
363
+ totalRefs++;
364
+ insertRef.run(skillId, ref.path, ref.filename, ref.content, refWordCount);
365
+ }
366
+ }
367
+
368
+ console.log(`\nPopulated DB: ${skills.length} skills, ${totalRefs} references, ${totalWords} total words`);
369
+ }
370
+
371
+ // ── Main ──
372
+
373
+ async function main() {
374
+ initDb();
375
+
376
+ let skills: SkillData[];
377
+ let sha: string;
378
+
379
+ if (FROM_CACHE) {
380
+ ({ skills, sha } = extractFromCache());
381
+ } else if (localPath) {
382
+ ({ skills, sha } = extractFromLocal(localPath));
383
+ } else {
384
+ ({ skills, sha } = await extractFromGitHub());
385
+ }
386
+
387
+ populateDb(skills, sha);
388
+ }
389
+
390
+ await main();
package/src/mcp.ts CHANGED
@@ -183,6 +183,8 @@ const {
183
183
  exportDeviceTestsCsv,
184
184
  fetchCurrentVersions,
185
185
  getPage,
186
+ getSkill,
187
+ listSkills,
186
188
  lookupProperty,
187
189
  searchCallouts,
188
190
  searchChangelogs,
@@ -437,6 +439,81 @@ ORDER BY version, sort_order;
437
439
  }),
438
440
  );
439
441
 
442
+ // ── Skills resources (community-created agent guides from tikoci/routeros-skills) ──
443
+
444
+ server.registerResource(
445
+ "skills-list",
446
+ "rosetta://skills",
447
+ {
448
+ title: "RouterOS Agent Skills",
449
+ description: "List of available RouterOS agent skill guides — community-created, AI-generated/human-reviewed supplemental content from tikoci/routeros-skills. NOT official MikroTik documentation.",
450
+ mimeType: "text/markdown",
451
+ },
452
+ async () => {
453
+ const skills = listSkills();
454
+ const lines = [
455
+ "# RouterOS Agent Skills",
456
+ "",
457
+ "⚠️ Community-created content from tikoci/routeros-skills — NOT official MikroTik documentation.",
458
+ "AI-generated, human-reviewed. May contain errors. Verify with routeros_search/routeros_get_page.",
459
+ "",
460
+ `${skills.length} skills available:`,
461
+ "",
462
+ ...skills.map(s => `- **${s.name}** — ${s.description} (${s.word_count} words, ${s.ref_count} refs) → \`rosetta://skills/${s.name}\``),
463
+ ];
464
+ return {
465
+ contents: [{
466
+ uri: "rosetta://skills",
467
+ mimeType: "text/markdown",
468
+ text: lines.join("\n"),
469
+ }],
470
+ };
471
+ },
472
+ );
473
+
474
+ // Register individual skill resources using resource templates
475
+ // MCP resource templates allow `rosetta://skills/{name}` pattern matching
476
+ {
477
+ const skills = listSkills();
478
+ for (const skill of skills) {
479
+ server.registerResource(
480
+ `skill-${skill.name}`,
481
+ `rosetta://skills/${skill.name}`,
482
+ {
483
+ title: `Skill: ${skill.name}`,
484
+ description: skill.description,
485
+ mimeType: "text/markdown",
486
+ },
487
+ async () => {
488
+ const detail = getSkill(skill.name);
489
+ if (!detail) {
490
+ return { contents: [{ uri: `rosetta://skills/${skill.name}`, mimeType: "text/plain", text: `Skill '${skill.name}' not found.` }] };
491
+ }
492
+ const lines = [
493
+ detail.provenance,
494
+ "",
495
+ `# ${detail.name}`,
496
+ "",
497
+ detail.content,
498
+ ];
499
+ if (detail.references.length > 0) {
500
+ lines.push("", "---", "", "## Reference Files", "");
501
+ for (const ref of detail.references) {
502
+ lines.push(`### ${ref.filename}`, "", ref.content, "");
503
+ }
504
+ }
505
+ return {
506
+ contents: [{
507
+ uri: `rosetta://skills/${detail.name}`,
508
+ mimeType: "text/markdown",
509
+ text: lines.join("\n"),
510
+ }],
511
+ };
512
+ },
513
+ );
514
+ }
515
+ }
516
+
440
517
  // ---- routeros_search ----
441
518
 
442
519
  server.registerTool(
@@ -651,6 +728,7 @@ arguments). Each child includes its type and linked documentation page if availa
651
728
  Useful for discovering what's available under a command path.
652
729
 
653
730
  Optionally filter by RouterOS version to check what exists in a specific release.
731
+ Optionally filter by CPU architecture (x86/arm64) to see platform-specific commands.
654
732
  Command data covers versions 7.9–7.23beta2. No v6 data.
655
733
 
656
734
  Workflow — combine with other tools:
@@ -662,7 +740,8 @@ Workflow — combine with other tools:
662
740
  Examples:
663
741
  - path: "/ip" → address, arp, dhcp-client, dhcp-server, firewall, route, etc.
664
742
  - path: "/ip/firewall" → filter, nat, mangle, raw, address-list, etc.
665
- - path: "", version: "7.15" → top-level menus as of RouterOS 7.15`,
743
+ - path: "", version: "7.15" → top-level menus as of RouterOS 7.15
744
+ - path: "/interface", arch: "arm64" → shows arm64-specific interfaces (wifi-qcom, ethernet/switch)`,
666
745
  inputSchema: {
667
746
  path: z
668
747
  .string()
@@ -673,13 +752,17 @@ Examples:
673
752
  .string()
674
753
  .optional()
675
754
  .describe("RouterOS version to filter by (e.g., '7.15'). Omit for latest."),
755
+ arch: z
756
+ .string()
757
+ .optional()
758
+ .describe("Filter by CPU architecture: 'x86' or 'arm64'. Omit to show all (including arch-specific nodes)."),
676
759
  },
677
760
  },
678
- async ({ path, version }) => {
761
+ async ({ path, version, arch }) => {
679
762
  const cmdPath = path?.trim() || "";
680
763
  const results = version
681
- ? browseCommandsAtVersion(cmdPath, version)
682
- : browseCommands(cmdPath);
764
+ ? browseCommandsAtVersion(cmdPath, version, arch)
765
+ : browseCommands(cmdPath, arch);
683
766
 
684
767
  if (results.length === 0) {
685
768
  return {
@@ -700,7 +783,10 @@ server.registerTool(
700
783
  description: `Get database statistics for the RouterOS documentation index.
701
784
 
702
785
  Returns page count, property count, callout count, changelog count, command count, link coverage,
703
- version range, and documentation export date.
786
+ version range, documentation export date, and available agent skills.
787
+
788
+ Skills: Community-created agent guides from tikoci/routeros-skills are available as MCP resources
789
+ at rosetta://skills/{name}. Use the resource listing to browse available skills.
704
790
 
705
791
  Knowledge boundaries:
706
792
  - Documentation: March 2026 Confluence HTML export (317 pages), aligned with long-term ~7.22
@@ -713,8 +799,17 @@ Knowledge boundaries:
713
799
  },
714
800
  async () => {
715
801
  const stats = getDbStats();
802
+ const skills = listSkills();
803
+ const statsWithSkills = {
804
+ ...stats,
805
+ skills: {
806
+ count: skills.length,
807
+ available: skills.map(s => s.name),
808
+ note: "Community-created agent guides from tikoci/routeros-skills. Access via rosetta://skills/{name} resources.",
809
+ },
810
+ };
716
811
  return {
717
- content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
812
+ content: [{ type: "text", text: JSON.stringify(statsWithSkills, null, 2) }],
718
813
  };
719
814
  },
720
815
  );
@@ -1103,10 +1198,14 @@ Without a prefix, a major-version diff can list hundreds of added paths.
1103
1198
  .string()
1104
1199
  .optional()
1105
1200
  .describe("Optional: scope the diff to a command subtree (e.g., '/ip/firewall', '/routing/bgp', '/interface/bridge')"),
1201
+ arch: z
1202
+ .string()
1203
+ .optional()
1204
+ .describe("Filter by CPU architecture: 'x86' or 'arm64'. Omit to diff all commands regardless of architecture."),
1106
1205
  },
1107
1206
  },
1108
- async ({ from_version, to_version, path_prefix }) => {
1109
- const result = diffCommandVersions(from_version, to_version, path_prefix);
1207
+ async ({ from_version, to_version, path_prefix, arch }) => {
1208
+ const result = diffCommandVersions(from_version, to_version, path_prefix, arch);
1110
1209
  if (result.added_count === 0 && result.removed_count === 0) {
1111
1210
  const hint = [
1112
1211
  result.note ?? null,
package/src/paths.ts CHANGED
@@ -55,11 +55,18 @@ export function resolveBaseDir(srcDir: string): string {
55
55
 
56
56
  /**
57
57
  * Resolve the full path to ros-help.db.
58
- * DB_PATH env var overrides all detection logic.
58
+ * Priority: DB_PATH env var --db CLI flag → auto-detect.
59
59
  */
60
60
  export function resolveDbPath(srcDir: string): string {
61
61
  const envPath = process.env.DB_PATH?.trim();
62
62
  if (envPath) return envPath;
63
+
64
+ // --db flag parsed at init time so ESM static imports don't race it
65
+ const dbArgIdx = process.argv.indexOf("--db");
66
+ if (dbArgIdx !== -1 && process.argv[dbArgIdx + 1]) {
67
+ return process.argv[dbArgIdx + 1];
68
+ }
69
+
63
70
  return path.join(resolveBaseDir(srcDir), "ros-help.db");
64
71
  }
65
72
 
@@ -79,7 +86,7 @@ export function detectMode(srcDir: string): InvocationMode {
79
86
  * Stamped into the DB via `PRAGMA user_version` by initDb() and checked at MCP
80
87
  * startup to detect stale DBs for bunx users who auto-update the package.
81
88
  */
82
- export const SCHEMA_VERSION = 2;
89
+ export const SCHEMA_VERSION = 4;
83
90
 
84
91
  /**
85
92
  * Resolve the version string.