@tikoci/rosetta 0.8.13 → 0.9.2
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/README.md +5 -5
- package/package.json +1 -1
- package/src/extract-skills.test.ts +39 -0
- package/src/extract-skills.ts +13 -4
- package/src/mcp-stdio-client.test.ts +1 -4
- package/src/release.test.ts +31 -3
- package/src/setup.ts +7 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rosetta
|
|
2
2
|
|
|
3
|
-
MCP server that gives AI assistants searchable access to
|
|
3
|
+
MCP server that gives AI assistants searchable access to MikroTik RouterOS documentation — 317 legacy Confluence-export pages, 4,860 properties, 40,000-entry command tree, hardware specs for 144 products, 518 YouTube video transcripts, and direct links to source docs. MikroTik's current help system is the Docusaurus site at <https://manual.mikrotik.com>; rosetta's prose-doc extraction still needs a major migration away from the retired Confluence export.
|
|
4
4
|
|
|
5
5
|
If you need MikroTik docs, you likely have a MikroTik. Install rosetta once as a container on your router using [RouterOS /app](#install-on-mikrotik-app), and any AI assistant on the network can use it. Or [run it locally](#install-locally-with-bun) on your workstation. **No AI required** — rosetta includes a [terminal browser](#browse-without-ai) for searching the database directly.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Instead of vector embeddings, rosetta uses **SQLite [FTS5](https://www.sqlite.or
|
|
|
12
12
|
|
|
13
13
|
| Data Source | Coverage |
|
|
14
14
|
|-------------|----------|
|
|
15
|
-
| Documentation pages | 317 pages (~515K words) from help.mikrotik.com |
|
|
15
|
+
| Documentation pages | 317 pages (~515K words) from the retired help.mikrotik.com Confluence export |
|
|
16
16
|
| Property definitions | 4,860 with types, defaults, descriptions |
|
|
17
17
|
| Command tree | 5,114 commands, 551 dirs, 34K arguments |
|
|
18
18
|
| Version history | 46 RouterOS versions tracked (7.9–7.23beta2) |
|
|
@@ -21,13 +21,13 @@ Instead of vector embeddings, rosetta uses **SQLite [FTS5](https://www.sqlite.or
|
|
|
21
21
|
| YouTube transcripts | 518 videos, ~1,890 chapter-level segments |
|
|
22
22
|
| Callout blocks | 1,034 warnings, notes, and tips |
|
|
23
23
|
|
|
24
|
-
Documentation covers RouterOS **v7 only**, aligned with the long-term release (~7.22) at export time.
|
|
24
|
+
Documentation covers RouterOS **v7 only**, aligned with the long-term release (~7.22) at the March 2026 Confluence-export time. Future official doc updates are expected on <https://manual.mikrotik.com>, including a Docusaurus CLI Reference generated from `/console/inspect` data.
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
## Install on MikroTik (/app)
|
|
29
29
|
|
|
30
|
-
RouterOS 7.22+ includes the [/app](https://
|
|
30
|
+
RouterOS 7.22+ includes the [/app](https://manual.mikrotik.com/docs/CLI%20Reference/container/app) feature for running containers directly on the router. This is the simplest way to deploy rosetta — install once, and any AI assistant on your network can connect to the MCP endpoint URL shown in the router UI.
|
|
31
31
|
|
|
32
32
|
**Requirements:** RouterOS 7.22+, x86 or ARM64 architecture (CCR, RB5009, hAP ax series, CHR, etc.), container package installed, device-mode enabled.
|
|
33
33
|
|
|
@@ -49,7 +49,7 @@ After reboot:
|
|
|
49
49
|
/system/device-mode/update mode=advanced container=yes
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
See MikroTik's [Container documentation](https://
|
|
52
|
+
See MikroTik's [Container documentation](https://manual.mikrotik.com/docs/Extended%20features/Container/) for full prerequisites and troubleshooting.
|
|
53
53
|
|
|
54
54
|
### 2. Add the rosetta app
|
|
55
55
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
process.env.DB_PATH = ":memory:";
|
|
4
|
+
|
|
5
|
+
const { githubApiHeaders } = await import("./extract-skills.ts");
|
|
6
|
+
|
|
7
|
+
describe("extract-skills GitHub API headers", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
delete process.env.GITHUB_TOKEN;
|
|
10
|
+
delete process.env.GH_TOKEN;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("keeps unauthenticated headers when no token is configured", () => {
|
|
14
|
+
delete process.env.GITHUB_TOKEN;
|
|
15
|
+
delete process.env.GH_TOKEN;
|
|
16
|
+
|
|
17
|
+
expect(githubApiHeaders()).toEqual({
|
|
18
|
+
Accept: "application/vnd.github.v3+json",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("uses GITHUB_TOKEN for authenticated API requests", () => {
|
|
23
|
+
process.env.GITHUB_TOKEN = "test-token";
|
|
24
|
+
|
|
25
|
+
expect(githubApiHeaders()).toEqual({
|
|
26
|
+
Accept: "application/vnd.github.v3+json",
|
|
27
|
+
Authorization: "Bearer test-token",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("falls back to GH_TOKEN for local authenticated runs", () => {
|
|
32
|
+
process.env.GH_TOKEN = "gh-token";
|
|
33
|
+
|
|
34
|
+
expect(githubApiHeaders()).toEqual({
|
|
35
|
+
Accept: "application/vnd.github.v3+json",
|
|
36
|
+
Authorization: "Bearer gh-token",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/extract-skills.ts
CHANGED
|
@@ -70,9 +70,16 @@ function countWords(text: string): number {
|
|
|
70
70
|
|
|
71
71
|
// ── GitHub API fetching ──
|
|
72
72
|
|
|
73
|
+
export function githubApiHeaders(): Record<string, string> {
|
|
74
|
+
const headers: Record<string, string> = { Accept: "application/vnd.github.v3+json" };
|
|
75
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
76
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
77
|
+
return headers;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
async function getDefaultBranchSha(): Promise<string> {
|
|
74
81
|
const res = await fetch(`${GITHUB_API_BASE}/commits/HEAD`, {
|
|
75
|
-
headers:
|
|
82
|
+
headers: githubApiHeaders(),
|
|
76
83
|
});
|
|
77
84
|
if (!res.ok) throw new Error(`Failed to get HEAD SHA: HTTP ${res.status}`);
|
|
78
85
|
const data = (await res.json()) as { sha: string };
|
|
@@ -81,7 +88,7 @@ async function getDefaultBranchSha(): Promise<string> {
|
|
|
81
88
|
|
|
82
89
|
async function listSkillDirs(sha: string): Promise<string[]> {
|
|
83
90
|
const res = await fetch(`${GITHUB_API_BASE}/contents/?ref=${sha}`, {
|
|
84
|
-
headers:
|
|
91
|
+
headers: githubApiHeaders(),
|
|
85
92
|
});
|
|
86
93
|
if (!res.ok) throw new Error(`Failed to list repo contents: HTTP ${res.status}`);
|
|
87
94
|
const entries = (await res.json()) as Array<{ name: string; type: string }>;
|
|
@@ -102,7 +109,7 @@ async function fetchRawFile(sha: string, path: string): Promise<string | null> {
|
|
|
102
109
|
|
|
103
110
|
async function listReferences(sha: string, skillName: string): Promise<string[]> {
|
|
104
111
|
const res = await fetch(`${GITHUB_API_BASE}/contents/${skillName}/references?ref=${sha}`, {
|
|
105
|
-
headers:
|
|
112
|
+
headers: githubApiHeaders(),
|
|
106
113
|
});
|
|
107
114
|
if (!res.ok) {
|
|
108
115
|
if (res.status === 404) return [];
|
|
@@ -387,4 +394,6 @@ async function main() {
|
|
|
387
394
|
populateDb(skills, sha);
|
|
388
395
|
}
|
|
389
396
|
|
|
390
|
-
|
|
397
|
+
if (import.meta.main) {
|
|
398
|
+
await main();
|
|
399
|
+
}
|
|
@@ -109,10 +109,7 @@ describe.skipIf(!hasTestDb && !dbWasExplicitlyConfigured)(
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
const transportErrors: Error[] = [];
|
|
112
|
-
|
|
113
|
-
const closeSeen = new Promise<void>((resolve) => {
|
|
114
|
-
resolveClosed = resolve;
|
|
115
|
-
});
|
|
112
|
+
const { promise: closeSeen, resolve: resolveClosed } = Promise.withResolvers<void>();
|
|
116
113
|
let closeCount = 0;
|
|
117
114
|
|
|
118
115
|
client = new Client({ name: "stdio-test-client", version: "1.0.0" });
|
package/src/release.test.ts
CHANGED
|
@@ -411,7 +411,13 @@ describe("release.yml", () => {
|
|
|
411
411
|
|
|
412
412
|
test("extracts agent skills in CI", () => {
|
|
413
413
|
const src = readText(".github/workflows/release.yml");
|
|
414
|
-
|
|
414
|
+
const skillsIdx = mustIndex(src, "Extract agent skills from GitHub");
|
|
415
|
+
const linkIdx = mustIndex(src, "Link commands to pages");
|
|
416
|
+
const skillsBlock = src.slice(skillsIdx, linkIdx);
|
|
417
|
+
|
|
418
|
+
expect(skillsBlock).toContain("extract-skills.ts");
|
|
419
|
+
expect(skillsBlock).toContain("GITHUB_TOKEN");
|
|
420
|
+
expect(skillsBlock).toContain("$" + "{{ github.token }}");
|
|
415
421
|
});
|
|
416
422
|
|
|
417
423
|
test("runs quality gate before release", () => {
|
|
@@ -436,6 +442,26 @@ describe("release.yml", () => {
|
|
|
436
442
|
expect(earlyTestIdx).toBeLessThan(downloadIdx);
|
|
437
443
|
});
|
|
438
444
|
|
|
445
|
+
test("preflights npm publish access before release side effects", () => {
|
|
446
|
+
const src = readText(".github/workflows/release.yml");
|
|
447
|
+
const preflightIdx = mustIndex(src, "Verify npm publish access");
|
|
448
|
+
const downloadIdx = mustIndex(src, "Download HTML export");
|
|
449
|
+
const ociIdx = mustIndex(src, "Build and push OCI images");
|
|
450
|
+
const releaseIdx = mustIndex(src, "Create or update GitHub Release");
|
|
451
|
+
const publishIdx = mustIndex(src, "Publish to npm");
|
|
452
|
+
const preflightBlock = src.slice(preflightIdx, downloadIdx);
|
|
453
|
+
|
|
454
|
+
expect(preflightIdx).toBeLessThan(downloadIdx);
|
|
455
|
+
expect(preflightIdx).toBeLessThan(ociIdx);
|
|
456
|
+
expect(preflightIdx).toBeLessThan(releaseIdx);
|
|
457
|
+
expect(preflightIdx).toBeLessThan(publishIdx);
|
|
458
|
+
expect(preflightBlock).toContain("NODE_AUTH_TOKEN");
|
|
459
|
+
expect(preflightBlock).toContain("npm whoami");
|
|
460
|
+
expect(preflightBlock).toContain("npm access list collaborators");
|
|
461
|
+
expect(preflightBlock).toContain("read-write access");
|
|
462
|
+
expect(preflightBlock).toContain("npm view");
|
|
463
|
+
});
|
|
464
|
+
|
|
439
465
|
test("keeps post-extraction DB-wipe guard after extraction", () => {
|
|
440
466
|
const src = readText(".github/workflows/release.yml");
|
|
441
467
|
const linkIdx = mustIndex(src, "Link commands to pages");
|
|
@@ -510,6 +536,8 @@ describe("release.yml", () => {
|
|
|
510
536
|
const clobberIdx = mustIndex(src, "gh release upload");
|
|
511
537
|
expect(src.slice(clobberIdx, clobberIdx + 120)).toContain("--clobber");
|
|
512
538
|
expect(republishBranchIdx).toBeLessThan(clobberIdx);
|
|
539
|
+
expect(src).toContain('elif gh release view "$VERSION"');
|
|
540
|
+
expect(src).toContain("updated before npm publish retry");
|
|
513
541
|
|
|
514
542
|
expect(src).toContain("if: inputs.republish_assets != true");
|
|
515
543
|
expect(src).toContain("if: inputs.republish_assets == true");
|
|
@@ -542,7 +570,7 @@ describe("release.yml", () => {
|
|
|
542
570
|
|
|
543
571
|
test("publishes to npm", () => {
|
|
544
572
|
const src = readText(".github/workflows/release.yml");
|
|
545
|
-
expect(src).toContain("npm publish");
|
|
573
|
+
expect(src).toContain("npm publish --access public --registry https://registry.npmjs.org/");
|
|
546
574
|
expect(src).toContain("NPM_TOKEN");
|
|
547
575
|
});
|
|
548
576
|
|
|
@@ -566,7 +594,7 @@ describe("release.yml", () => {
|
|
|
566
594
|
const validateIdx = src.indexOf("Validate DB has expected content");
|
|
567
595
|
const buildIdx = src.indexOf("Build release artifacts");
|
|
568
596
|
const releaseIdx = src.indexOf("gh release create");
|
|
569
|
-
const npmIdx = src.indexOf("npm
|
|
597
|
+
const npmIdx = src.indexOf("Publish to npm");
|
|
570
598
|
expect(validateIdx).toBeGreaterThan(0);
|
|
571
599
|
expect(validateIdx).toBeLessThan(buildIdx);
|
|
572
600
|
expect(validateIdx).toBeLessThan(releaseIdx);
|
package/src/setup.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { execSync } from "node:child_process";
|
|
|
11
11
|
import {
|
|
12
12
|
closeSync,
|
|
13
13
|
existsSync,
|
|
14
|
+
fstatSync,
|
|
14
15
|
openSync,
|
|
15
16
|
readdirSync,
|
|
16
17
|
readSync,
|
|
@@ -24,6 +25,7 @@ import { gunzipSync } from "bun";
|
|
|
24
25
|
import { detectMode, resolveBaseDir, resolveDbPath, resolveVersion, SCHEMA_VERSION } from "./paths.ts";
|
|
25
26
|
|
|
26
27
|
declare const REPO_URL: string;
|
|
28
|
+
const REPLACE_DB_TIMEOUT_MS = 30_000;
|
|
27
29
|
|
|
28
30
|
const GITHUB_REPO =
|
|
29
31
|
typeof REPO_URL !== "undefined" ? REPO_URL : "tikoci/rosetta";
|
|
@@ -72,14 +74,12 @@ function dbHasData(dbPath: string): boolean {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
function looksLikeSqliteFile(dbPath: string): boolean {
|
|
75
|
-
if (!existsSync(dbPath)) return false;
|
|
76
|
-
|
|
77
77
|
let fd: number | null = null;
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
fd = openSync(dbPath, "r");
|
|
80
|
+
const stats = fstatSync(fd);
|
|
80
81
|
if (!stats.isFile() || stats.size < SQLITE_MAGIC.length) return false;
|
|
81
82
|
|
|
82
|
-
fd = openSync(dbPath, "r");
|
|
83
83
|
const header = Buffer.alloc(SQLITE_MAGIC.length);
|
|
84
84
|
const bytesRead = readSync(fd, header, 0, header.byteLength, 0);
|
|
85
85
|
return bytesRead === header.byteLength && header.toString("utf8") === SQLITE_MAGIC;
|
|
@@ -301,7 +301,7 @@ function isReplaceRaceError(e: unknown): boolean {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
async function replaceDbFile(tmpPath: string, dbPath: string): Promise<void> {
|
|
304
|
-
const deadline = Date.now() +
|
|
304
|
+
const deadline = Date.now() + REPLACE_DB_TIMEOUT_MS;
|
|
305
305
|
let lastError: unknown = null;
|
|
306
306
|
|
|
307
307
|
while (Date.now() <= deadline) {
|
|
@@ -310,7 +310,6 @@ async function replaceDbFile(tmpPath: string, dbPath: string): Promise<void> {
|
|
|
310
310
|
return;
|
|
311
311
|
} catch (e) {
|
|
312
312
|
if (!isReplaceRaceError(e)) throw e;
|
|
313
|
-
lastError = e;
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
tryUnlink(dbPath);
|
|
@@ -475,8 +474,8 @@ export async function downloadDb(
|
|
|
475
474
|
cleanupDbArtifacts(tmpPath);
|
|
476
475
|
lastError = new Error(
|
|
477
476
|
`Downloaded DB schema=${probe.schemaVersion} does not match this rosetta build (expected ${SCHEMA_VERSION}). ` +
|
|
478
|
-
`This usually means
|
|
479
|
-
|
|
477
|
+
`This usually means your MCP client is still using a cached older package version. ` +
|
|
478
|
+
`Restart the MCP client to let bunx re-resolve the latest package, or run: bunx @tikoci/rosetta@latest --refresh`,
|
|
480
479
|
);
|
|
481
480
|
if (isLast) throw lastError;
|
|
482
481
|
log(` ${lastError.message}`);
|