@thallylabs/mcp 0.7.1 → 0.7.3
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/dist/index.js +67 -918
- package/dist/tools.js +67 -918
- package/package.json +2 -1
package/dist/tools.js
CHANGED
|
@@ -527,867 +527,15 @@ async function handleUpdatePage(input) {
|
|
|
527
527
|
|
|
528
528
|
// src/tools/migrate-docs.ts
|
|
529
529
|
import { z as z6 } from "zod";
|
|
530
|
-
|
|
531
|
-
// src/lib/migrate/index.ts
|
|
532
|
-
import { mkdirSync as mkdirSync3, copyFileSync, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdtempSync, rmSync } from "fs";
|
|
533
|
-
import { join as join7, dirname as dirname2, resolve as resolve2 } from "path";
|
|
534
|
-
import { tmpdir } from "os";
|
|
535
|
-
import { execSync as execSync3 } from "child_process";
|
|
536
|
-
import pLimit from "p-limit";
|
|
537
|
-
|
|
538
|
-
// src/lib/migrate/github.ts
|
|
539
|
-
import { execSync as execSync2 } from "child_process";
|
|
540
|
-
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
541
|
-
import { join as join5, relative, extname, basename } from "path";
|
|
542
|
-
var OPENAPI_FILENAMES = [
|
|
543
|
-
"openapi.json",
|
|
544
|
-
"openapi.yaml",
|
|
545
|
-
"openapi.yml",
|
|
546
|
-
"swagger.json",
|
|
547
|
-
"swagger.yaml",
|
|
548
|
-
"swagger.yml"
|
|
549
|
-
];
|
|
550
|
-
function detectOpenApiSpec(cloneDir) {
|
|
551
|
-
return findOpenApiSpec(cloneDir, 0);
|
|
552
|
-
}
|
|
553
|
-
function findOpenApiSpec(dir, depth) {
|
|
554
|
-
if (depth > 3) return null;
|
|
555
|
-
let entries;
|
|
556
|
-
try {
|
|
557
|
-
entries = readdirSync2(dir);
|
|
558
|
-
} catch {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
for (const filename of OPENAPI_FILENAMES) {
|
|
562
|
-
if (entries.includes(filename)) {
|
|
563
|
-
return { absPath: join5(dir, filename), filename };
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
for (const entry of entries) {
|
|
567
|
-
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
568
|
-
const fullPath = join5(dir, entry);
|
|
569
|
-
try {
|
|
570
|
-
if (statSync(fullPath).isDirectory()) {
|
|
571
|
-
const found = findOpenApiSpec(fullPath, depth + 1);
|
|
572
|
-
if (found) return found;
|
|
573
|
-
}
|
|
574
|
-
} catch {
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
function parseGitHubUrl(rawUrl) {
|
|
580
|
-
let url;
|
|
581
|
-
try {
|
|
582
|
-
url = new URL(rawUrl);
|
|
583
|
-
} catch {
|
|
584
|
-
throw new Error(`Invalid URL: ${rawUrl}`);
|
|
585
|
-
}
|
|
586
|
-
if (url.hostname !== "github.com") {
|
|
587
|
-
throw new Error(`URL must be a github.com URL, got: ${url.hostname}`);
|
|
588
|
-
}
|
|
589
|
-
const parts = url.pathname.replace(/^\//, "").split("/");
|
|
590
|
-
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
591
|
-
throw new Error(`GitHub URL must include owner and repo: ${rawUrl}`);
|
|
592
|
-
}
|
|
593
|
-
const owner = parts[0];
|
|
594
|
-
const repo = parts[1];
|
|
595
|
-
let branch = "HEAD";
|
|
596
|
-
let docsDir = "";
|
|
597
|
-
if (parts.length >= 4 && parts[2] === "tree") {
|
|
598
|
-
branch = parts[3];
|
|
599
|
-
if (parts.length > 4) {
|
|
600
|
-
docsDir = parts.slice(4).join("/");
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
|
|
604
|
-
return { owner, repo, branch, docsDir, cloneUrl };
|
|
605
|
-
}
|
|
606
|
-
async function cloneRepo(source, targetDir) {
|
|
607
|
-
const parts = ["git", "clone", "--depth", "1"];
|
|
608
|
-
if (source.branch !== "HEAD") {
|
|
609
|
-
parts.push("--branch", source.branch);
|
|
610
|
-
}
|
|
611
|
-
parts.push(source.cloneUrl, targetDir);
|
|
612
|
-
const cmd = parts.join(" ");
|
|
613
|
-
try {
|
|
614
|
-
execSync2(cmd, { stdio: "pipe" });
|
|
615
|
-
} catch (err) {
|
|
616
|
-
const stderr = err.stderr?.toString().trim() ?? "";
|
|
617
|
-
const msg = stderr || (err instanceof Error ? err.message : String(err));
|
|
618
|
-
throw new Error(`Failed to clone ${source.cloneUrl}: ${msg}`);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
var MD_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
622
|
-
var ALL_DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".txt"]);
|
|
623
|
-
function hasMdFiles(dir) {
|
|
624
|
-
let entries;
|
|
625
|
-
try {
|
|
626
|
-
entries = readdirSync2(dir);
|
|
627
|
-
} catch {
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
for (const entry of entries) {
|
|
631
|
-
const fullPath = join5(dir, entry);
|
|
632
|
-
try {
|
|
633
|
-
const stat = statSync(fullPath);
|
|
634
|
-
if (stat.isDirectory()) {
|
|
635
|
-
if (hasMdFiles(fullPath)) return true;
|
|
636
|
-
} else if (MD_EXTENSIONS.has(extname(entry).toLowerCase())) {
|
|
637
|
-
return true;
|
|
638
|
-
}
|
|
639
|
-
} catch {
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
return false;
|
|
643
|
-
}
|
|
644
|
-
function detectDocsDir(cloneDir) {
|
|
645
|
-
const candidates = [
|
|
646
|
-
"docs",
|
|
647
|
-
"documentation",
|
|
648
|
-
"content",
|
|
649
|
-
"pages",
|
|
650
|
-
"src/content",
|
|
651
|
-
"src/pages",
|
|
652
|
-
"guide",
|
|
653
|
-
"guides",
|
|
654
|
-
""
|
|
655
|
-
];
|
|
656
|
-
for (const candidate of candidates) {
|
|
657
|
-
const fullPath = candidate ? join5(cloneDir, candidate) : cloneDir;
|
|
658
|
-
try {
|
|
659
|
-
const stat = statSync(fullPath);
|
|
660
|
-
if (stat.isDirectory() && hasMdFiles(fullPath)) {
|
|
661
|
-
return candidate;
|
|
662
|
-
}
|
|
663
|
-
} catch {
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
return "";
|
|
667
|
-
}
|
|
668
|
-
function slugifySegment(seg) {
|
|
669
|
-
return seg.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
670
|
-
}
|
|
671
|
-
function derivePageId(relPath) {
|
|
672
|
-
const normalized = relPath.replace(/\\/g, "/");
|
|
673
|
-
const parts = normalized.split("/");
|
|
674
|
-
const filename = parts[parts.length - 1];
|
|
675
|
-
const dirs = parts.slice(0, -1);
|
|
676
|
-
const base = basename(filename, extname(filename));
|
|
677
|
-
if (dirs.length === 0 && base.toLowerCase() === "readme") {
|
|
678
|
-
return "introduction";
|
|
679
|
-
}
|
|
680
|
-
if (base.toLowerCase() === "index") {
|
|
681
|
-
if (dirs.length === 0) return "introduction";
|
|
682
|
-
return dirs.map(slugifySegment).join("/");
|
|
683
|
-
}
|
|
684
|
-
return [...dirs, base].map(slugifySegment).join("/");
|
|
685
|
-
}
|
|
686
|
-
function scanDir(dir, baseDir, primaryOnly, results) {
|
|
687
|
-
let entries;
|
|
688
|
-
try {
|
|
689
|
-
entries = readdirSync2(dir);
|
|
690
|
-
} catch {
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
for (const entry of entries) {
|
|
694
|
-
if (entry.startsWith("_") || entry.startsWith(".") || entry === "node_modules") continue;
|
|
695
|
-
const fullPath = join5(dir, entry);
|
|
696
|
-
let stat;
|
|
697
|
-
try {
|
|
698
|
-
stat = statSync(fullPath);
|
|
699
|
-
} catch {
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (stat.isDirectory()) {
|
|
703
|
-
scanDir(fullPath, baseDir, primaryOnly, results);
|
|
704
|
-
} else {
|
|
705
|
-
const ext = extname(entry).toLowerCase();
|
|
706
|
-
const validExt = primaryOnly ? MD_EXTENSIONS.has(ext) : ALL_DOC_EXTENSIONS.has(ext);
|
|
707
|
-
if (!validExt) continue;
|
|
708
|
-
const relPath = relative(baseDir, fullPath);
|
|
709
|
-
const pageId = derivePageId(relPath);
|
|
710
|
-
results.push({ absPath: fullPath, relPath, pageId, ext });
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function findDocFiles(cloneDir, docsDir) {
|
|
715
|
-
const baseDir = docsDir ? join5(cloneDir, docsDir) : cloneDir;
|
|
716
|
-
const primaryResults = [];
|
|
717
|
-
scanDir(baseDir, baseDir, true, primaryResults);
|
|
718
|
-
if (primaryResults.length > 0) return primaryResults;
|
|
719
|
-
const allResults = [];
|
|
720
|
-
scanDir(baseDir, baseDir, false, allResults);
|
|
721
|
-
return allResults;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// src/lib/migrate/importer.ts
|
|
725
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
726
|
-
import { basename as basename2, extname as extname2 } from "path";
|
|
727
|
-
import matter2 from "gray-matter";
|
|
728
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
729
|
-
function titleFromFilename(relPath) {
|
|
730
|
-
const filename = relPath.replace(/\\/g, "/").split("/").pop() ?? relPath;
|
|
731
|
-
const base = basename2(filename, extname2(filename));
|
|
732
|
-
return base.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
733
|
-
}
|
|
734
|
-
function extractFirstParagraph(content) {
|
|
735
|
-
for (const line of content.split("\n")) {
|
|
736
|
-
const trimmed = line.trim();
|
|
737
|
-
if (!trimmed) continue;
|
|
738
|
-
if (trimmed.startsWith("#")) continue;
|
|
739
|
-
if (trimmed.startsWith("```") || trimmed.startsWith(":::") || trimmed.startsWith("<")) continue;
|
|
740
|
-
if (trimmed.startsWith("import ") || trimmed.startsWith("export ")) continue;
|
|
741
|
-
return trimmed.slice(0, 200);
|
|
742
|
-
}
|
|
743
|
-
return "";
|
|
744
|
-
}
|
|
745
|
-
function normalizeComponents(body) {
|
|
746
|
-
let result = body;
|
|
747
|
-
const importedComponents = /* @__PURE__ */ new Set();
|
|
748
|
-
result = result.replace(
|
|
749
|
-
/^import\s+(\w+|\{[^}]+\})\s+from\s+['"][^'"]+['"]\s*;?\s*$/gm,
|
|
750
|
-
(_, imported) => {
|
|
751
|
-
const name = imported.trim();
|
|
752
|
-
if (/^[A-Z]\w*$/.test(name)) importedComponents.add(name);
|
|
753
|
-
return "";
|
|
754
|
-
}
|
|
755
|
-
);
|
|
756
|
-
for (const name of importedComponents) {
|
|
757
|
-
result = result.replace(
|
|
758
|
-
new RegExp(`<${name}(?:\\s[^>]*)?\\/>`, "gm"),
|
|
759
|
-
`{/* <${name} /> \u2014 imported snippet component */}`
|
|
760
|
-
);
|
|
761
|
-
result = result.replace(
|
|
762
|
-
new RegExp(`<${name}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${name}>`, "g"),
|
|
763
|
-
`{/* <${name}> \u2014 imported snippet component */}`
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
result = result.replace(/<!--([\s\S]*?)-->/g, (_, inner) => `{/*${inner}*/}`);
|
|
767
|
-
result = result.replace(/<Tip>([\s\S]*?)<\/Tip>/g, (_, c) => `<Note>${c}</Note>`);
|
|
768
|
-
result = result.replace(/<Check>([\s\S]*?)<\/Check>/g, (_, c) => `<Note>${c}</Note>`);
|
|
769
|
-
result = result.replace(/<Danger>([\s\S]*?)<\/Danger>/g, (_, c) => `<Error>${c}</Error>`);
|
|
770
|
-
result = result.replace(/<Callout(?:\s[^>]*)?>([\s\S]*?)<\/Callout>/g, (_, c) => `<Note>${c}</Note>`);
|
|
771
|
-
result = result.replace(/:::(\w+)(?:\s+[^\n]*)?\n([\s\S]*?):::/g, (_, type, content) => {
|
|
772
|
-
const tag = mapAdmonitionToThallyTag(type.toLowerCase());
|
|
773
|
-
return `<${tag}>
|
|
774
|
-
${content.trim()}
|
|
775
|
-
</${tag}>`;
|
|
776
|
-
});
|
|
777
|
-
result = result.replace(
|
|
778
|
-
/\{%\s*hint\s+style="(\w+)"\s*%\}([\s\S]*?)\{%\s*endhint\s*%\}/g,
|
|
779
|
-
(_, style, content) => {
|
|
780
|
-
const tag = mapGitBookStyleToThallyTag(style.toLowerCase());
|
|
781
|
-
return `<${tag}>
|
|
782
|
-
${content.trim()}
|
|
783
|
-
</${tag}>`;
|
|
784
|
-
}
|
|
785
|
-
);
|
|
786
|
-
result = result.replace(/<AccordionGroup[^>]*>\n?([\s\S]*?)\n?<\/AccordionGroup>/g, (_, inner) => inner.trim());
|
|
787
|
-
result = result.replace(/<Expandable(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
788
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "Details";
|
|
789
|
-
return `<Accordion title="${title}">`;
|
|
790
|
-
});
|
|
791
|
-
result = result.replace(/<\/Expandable>/g, "</Accordion>");
|
|
792
|
-
result = result.replace(/<Latex>([\s\S]*?)<\/Latex>/g, (_, inner) => `\`${inner.trim()}\``);
|
|
793
|
-
result = result.replace(/<(?:ResponseField|ParamField)([^>]*)>/g, (_, attrs) => {
|
|
794
|
-
const name = attrs.match(/name="([^"]*)"/)?.[1] ?? "";
|
|
795
|
-
const type = attrs.match(/type="([^"]*)"/)?.[1] ?? "";
|
|
796
|
-
const required = /\brequired\b/.test(attrs);
|
|
797
|
-
const def = attrs.match(/default="([^"]*)"/)?.[1];
|
|
798
|
-
const deprecated = /\bdeprecated\b/.test(attrs);
|
|
799
|
-
const meta = [
|
|
800
|
-
type && `\`${type}\``,
|
|
801
|
-
required && "*(required)*",
|
|
802
|
-
deprecated && "*(deprecated)*",
|
|
803
|
-
def !== void 0 && `*(default: \`${def}\`)*`
|
|
804
|
-
].filter(Boolean).join(" ");
|
|
805
|
-
return `
|
|
806
|
-
**\`${name}\`** ${meta}
|
|
807
|
-
|
|
808
|
-
`;
|
|
809
|
-
});
|
|
810
|
-
result = result.replace(/<\/(?:ResponseField|ParamField)>/g, "\n");
|
|
811
|
-
result = result.replace(/<RequestExample[^>]*>/g, "<CodeGroup>");
|
|
812
|
-
result = result.replace(/<\/RequestExample>/g, "</CodeGroup>");
|
|
813
|
-
result = result.replace(/<ResponseExample[^>]*>/g, "<CodeGroup>");
|
|
814
|
-
result = result.replace(/<\/ResponseExample>/g, "</CodeGroup>");
|
|
815
|
-
result = result.replace(/<Panel[^>]*>([\s\S]*?)<\/Panel>/g, (_, inner) => inner.trim());
|
|
816
|
-
result = result.replace(/<Badge[^>]*>([\s\S]*?)<\/Badge>/g, (_, inner) => `**${inner.trim()}**`);
|
|
817
|
-
result = result.replace(/<Tile(\s[^>]*)?>/g, (_, attrs = "") => `<Card${attrs}>`);
|
|
818
|
-
result = result.replace(/<\/Tile>/g, "</Card>");
|
|
819
|
-
result = result.replace(/<View(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
820
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1] ?? "View";
|
|
821
|
-
return `<Tab title="${title}">`;
|
|
822
|
-
});
|
|
823
|
-
result = result.replace(/<\/View>/g, "</Tab>");
|
|
824
|
-
result = result.replace(/<Update(\s[^>]*)?>/g, (_, attrs = "") => {
|
|
825
|
-
const label = attrs.match(/label="([^"]*)"/)?.[1] ?? "";
|
|
826
|
-
const desc = attrs.match(/description="([^"]*)"/)?.[1] ?? "";
|
|
827
|
-
return `## ${label}${desc ? `
|
|
828
|
-
|
|
829
|
-
*${desc}*` : ""}
|
|
830
|
-
|
|
831
|
-
`;
|
|
832
|
-
});
|
|
833
|
-
result = result.replace(/<\/Update>/g, "\n");
|
|
834
|
-
result = result.replace(/<Prompt[^>]*>([\s\S]*?)<\/Prompt>/g, (_, inner) => {
|
|
835
|
-
return `\`\`\`text
|
|
836
|
-
${inner.trim()}
|
|
837
|
-
\`\`\``;
|
|
838
|
-
});
|
|
839
|
-
result = result.replace(/<Tree[^>]*>/g, "```\n");
|
|
840
|
-
result = result.replace(/<\/Tree>/g, "\n```");
|
|
841
|
-
result = result.replace(/<Tree\.Folder[^>]*name="([^"]*)"[^>]*>/g, (_, name) => `\u{1F4C1} ${name}/
|
|
842
|
-
`);
|
|
843
|
-
result = result.replace(/<\/Tree\.Folder>/g, "");
|
|
844
|
-
result = result.replace(new RegExp('<Tree\\.File[^>]*name="([^"]*)"[^>]*/>', "g"), (_, name) => ` ${name}
|
|
845
|
-
`);
|
|
846
|
-
result = result.replace(/<Color[^>]*>/g, "| Name | Value |\n|---|---|\n");
|
|
847
|
-
result = result.replace(/<\/Color>/g, "");
|
|
848
|
-
result = result.replace(/<Color\.Row[^>]*title="([^"]*)"[^>]*>/g, (_, title) => `**${title}**
|
|
849
|
-
`);
|
|
850
|
-
result = result.replace(/<\/Color\.Row>/g, "");
|
|
851
|
-
result = result.replace(
|
|
852
|
-
/<Color\.Item[^>]*name="([^"]*)"[^>]*value="([^"]*)"[^>]*\/>/g,
|
|
853
|
-
(_, name, value) => `| ${name} | \`${value}\` |
|
|
854
|
-
`
|
|
855
|
-
);
|
|
856
|
-
result = result.replace(/<Banner[^>]*>([\s\S]*?)<\/Banner>/g, "");
|
|
857
|
-
result = result.replace(/<Banner[^>]*\/>/g, "");
|
|
858
|
-
return result;
|
|
859
|
-
}
|
|
860
|
-
function mapAdmonitionToThallyTag(type) {
|
|
861
|
-
if (type === "warning" || type === "caution") return "Warning";
|
|
862
|
-
if (type === "danger") return "Error";
|
|
863
|
-
if (type === "info") return "Info";
|
|
864
|
-
return "Note";
|
|
865
|
-
}
|
|
866
|
-
function mapGitBookStyleToThallyTag(style) {
|
|
867
|
-
if (style === "warning") return "Warning";
|
|
868
|
-
if (style === "danger") return "Error";
|
|
869
|
-
if (style === "success") return "Note";
|
|
870
|
-
return "Info";
|
|
871
|
-
}
|
|
872
|
-
var RST_SYSTEM_PROMPT = `You are a documentation converter. Convert the given file content to clean MDX.
|
|
873
|
-
Respond with ONLY valid JSON \u2014 no prose, no markdown fences:
|
|
874
|
-
{
|
|
875
|
-
"frontmatter": { "title": "string", "description": "string", "keywords": ["..."] },
|
|
876
|
-
"body": "string \u2014 full MDX body"
|
|
877
|
-
}
|
|
878
|
-
Rules: preserve code blocks with language hints; convert tables to Markdown; convert callout
|
|
879
|
-
boxes to <Note> or <Warning>; preserve heading hierarchy; do not include page title as a heading.`;
|
|
880
|
-
function parseClaudeResponse(text) {
|
|
881
|
-
try {
|
|
882
|
-
return JSON.parse(text);
|
|
883
|
-
} catch {
|
|
884
|
-
const stripped = text.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
885
|
-
return JSON.parse(stripped);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
async function importFile(file, apiKey) {
|
|
889
|
-
const ext = file.ext.toLowerCase();
|
|
890
|
-
if (ext === ".md" || ext === ".mdx") {
|
|
891
|
-
const raw = readFileSync4(file.absPath, "utf8");
|
|
892
|
-
const parsed = matter2(raw);
|
|
893
|
-
const fmTitle = parsed.data.title ?? "";
|
|
894
|
-
const fmDesc = parsed.data.description ?? "";
|
|
895
|
-
const fmKeywords = parsed.data.keywords;
|
|
896
|
-
const title = fmTitle || titleFromFilename(file.relPath);
|
|
897
|
-
const description = fmDesc || extractFirstParagraph(parsed.content);
|
|
898
|
-
const keywords = Array.isArray(fmKeywords) ? fmKeywords : [];
|
|
899
|
-
const openapi = parsed.data.openapi;
|
|
900
|
-
const body = normalizeComponents(parsed.content);
|
|
901
|
-
if (openapi && !body.trim()) return null;
|
|
902
|
-
return { pageId: file.pageId, frontmatter: { title, description, keywords }, body };
|
|
903
|
-
}
|
|
904
|
-
if (!apiKey) {
|
|
905
|
-
throw new Error(`Skipping non-Markdown file (no API key): ${file.relPath}`);
|
|
906
|
-
}
|
|
907
|
-
const content = readFileSync4(file.absPath, "utf8");
|
|
908
|
-
const client = new Anthropic({ apiKey });
|
|
909
|
-
const message = await client.messages.create({
|
|
910
|
-
model: "claude-sonnet-4-6",
|
|
911
|
-
max_tokens: 4096,
|
|
912
|
-
system: RST_SYSTEM_PROMPT,
|
|
913
|
-
messages: [
|
|
914
|
-
{
|
|
915
|
-
role: "user",
|
|
916
|
-
content: `Convert this documentation file to MDX.
|
|
917
|
-
|
|
918
|
-
File: ${file.relPath}
|
|
919
|
-
|
|
920
|
-
Content:
|
|
921
|
-
${content.slice(0, 8e4)}`
|
|
922
|
-
}
|
|
923
|
-
]
|
|
924
|
-
});
|
|
925
|
-
const responseText = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
926
|
-
const claudeResult = parseClaudeResponse(responseText);
|
|
927
|
-
return {
|
|
928
|
-
pageId: file.pageId,
|
|
929
|
-
frontmatter: claudeResult.frontmatter,
|
|
930
|
-
body: claudeResult.body
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// src/lib/migrate/nav-builder.ts
|
|
935
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
936
|
-
import { join as join6 } from "path";
|
|
937
|
-
function titleCase(str) {
|
|
938
|
-
return str.split("-").map((word) => {
|
|
939
|
-
if (word.toLowerCase() === "api") return "API";
|
|
940
|
-
if (word.toLowerCase() === "sdk") return "SDK";
|
|
941
|
-
if (word.toLowerCase() === "cli") return "CLI";
|
|
942
|
-
if (word.toLowerCase() === "ui") return "UI";
|
|
943
|
-
if (word.toLowerCase() === "faq") return "FAQ";
|
|
944
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
945
|
-
}).join(" ");
|
|
946
|
-
}
|
|
947
|
-
function buildNavStructure(pages) {
|
|
948
|
-
const seen = /* @__PURE__ */ new Set();
|
|
949
|
-
const ordered = [];
|
|
950
|
-
for (const p of pages) {
|
|
951
|
-
if (!seen.has(p.pageId)) {
|
|
952
|
-
seen.add(p.pageId);
|
|
953
|
-
ordered.push(p.pageId);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
const depth1Segments = /* @__PURE__ */ new Set();
|
|
957
|
-
for (const id of ordered) {
|
|
958
|
-
const parts = id.split("/");
|
|
959
|
-
if (parts.length > 1) {
|
|
960
|
-
depth1Segments.add(parts[0]);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
const rootOnlyPages = ordered.filter((id) => !id.includes("/"));
|
|
964
|
-
const useSingleTab = depth1Segments.size === 0 || depth1Segments.size === 1 && rootOnlyPages.length === 0;
|
|
965
|
-
let tabs;
|
|
966
|
-
if (useSingleTab) {
|
|
967
|
-
const groups = buildGroups(ordered, null);
|
|
968
|
-
tabs = [{ tab: "Overview", groups }];
|
|
969
|
-
} else {
|
|
970
|
-
tabs = [];
|
|
971
|
-
if (rootOnlyPages.length > 0) {
|
|
972
|
-
const groups = buildGroups(rootOnlyPages, null);
|
|
973
|
-
tabs.push({ tab: "Overview", groups });
|
|
974
|
-
}
|
|
975
|
-
for (const seg of depth1Segments) {
|
|
976
|
-
const tabPages = ordered.filter((id) => id.startsWith(seg + "/") || id === seg);
|
|
977
|
-
const groups = buildGroups(tabPages, seg);
|
|
978
|
-
tabs.push({ tab: titleCase(seg), groups });
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
982
|
-
return { tabs };
|
|
983
|
-
}
|
|
984
|
-
function buildGroups(pageIds, tabSegment) {
|
|
985
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
986
|
-
for (const id of pageIds) {
|
|
987
|
-
let groupName;
|
|
988
|
-
if (tabSegment === null) {
|
|
989
|
-
groupName = "Overview";
|
|
990
|
-
} else {
|
|
991
|
-
const rel = id.startsWith(tabSegment + "/") ? id.slice(tabSegment.length + 1) : id;
|
|
992
|
-
const relParts = rel.split("/");
|
|
993
|
-
groupName = relParts.length === 1 ? titleCase(tabSegment) : titleCase(relParts[0]);
|
|
994
|
-
}
|
|
995
|
-
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
|
|
996
|
-
groupMap.get(groupName).push(id);
|
|
997
|
-
}
|
|
998
|
-
const groups = [];
|
|
999
|
-
for (const [groupName, groupPages] of groupMap) {
|
|
1000
|
-
const sorted = [...groupPages];
|
|
1001
|
-
const introIdx = sorted.indexOf("introduction");
|
|
1002
|
-
if (introIdx > 0) {
|
|
1003
|
-
sorted.splice(introIdx, 1);
|
|
1004
|
-
sorted.unshift("introduction");
|
|
1005
|
-
}
|
|
1006
|
-
groups.push({ group: groupName, pages: sorted });
|
|
1007
|
-
}
|
|
1008
|
-
return groups;
|
|
1009
|
-
}
|
|
1010
|
-
function detectPlatform(cloneDir) {
|
|
1011
|
-
if (existsSync5(join6(cloneDir, "mint.json"))) return "mintlify";
|
|
1012
|
-
if (existsSync5(join6(cloneDir, "docs.json"))) {
|
|
1013
|
-
try {
|
|
1014
|
-
const parsed = JSON.parse(readFileSync5(join6(cloneDir, "docs.json"), "utf8"));
|
|
1015
|
-
if (Array.isArray(parsed.tabs)) return "thally";
|
|
1016
|
-
const schema = parsed.$schema;
|
|
1017
|
-
if (schema?.includes("mintlify") || "navigation" in parsed) return "mintlify";
|
|
1018
|
-
} catch {
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
if (existsSync5(join6(cloneDir, "docusaurus.config.js")) || existsSync5(join6(cloneDir, "docusaurus.config.ts")) || existsSync5(join6(cloneDir, "docusaurus.config.mjs"))) return "docusaurus";
|
|
1022
|
-
if (existsSync5(join6(cloneDir, "SUMMARY.md"))) return "gitbook";
|
|
1023
|
-
if (existsSync5(join6(cloneDir, ".vitepress"))) return "vitepress";
|
|
1024
|
-
if (existsSync5(join6(cloneDir, "astro.config.mjs")) || existsSync5(join6(cloneDir, "astro.config.ts"))) return "starlight";
|
|
1025
|
-
if (existsSync5(join6(cloneDir, "_meta.json")) || existsSync5(join6(cloneDir, "pages", "_meta.json"))) return "nextra";
|
|
1026
|
-
return "unknown";
|
|
1027
|
-
}
|
|
1028
|
-
function slugify2(s) {
|
|
1029
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1030
|
-
}
|
|
1031
|
-
function normalizePageRef(ref, docsDir) {
|
|
1032
|
-
let r = ref;
|
|
1033
|
-
if (docsDir && r.startsWith(docsDir + "/")) r = r.slice(docsDir.length + 1);
|
|
1034
|
-
r = r.replace(/\.(mdx?|rst|txt)$/, "");
|
|
1035
|
-
const parts = r.split("/");
|
|
1036
|
-
const last = parts[parts.length - 1].toLowerCase();
|
|
1037
|
-
if (last === "index" || last === "readme") {
|
|
1038
|
-
return parts.length === 1 ? "introduction" : parts.slice(0, -1).map(slugify2).join("/");
|
|
1039
|
-
}
|
|
1040
|
-
return parts.map(slugify2).join("/");
|
|
1041
|
-
}
|
|
1042
|
-
function convertMintTabs(tabs, docsDir) {
|
|
1043
|
-
if (tabs.length === 0) return null;
|
|
1044
|
-
function convertPageRef(page) {
|
|
1045
|
-
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
1046
|
-
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
1047
|
-
const p = page;
|
|
1048
|
-
return {
|
|
1049
|
-
group: String(p.group),
|
|
1050
|
-
pages: (p.pages ?? []).map(convertPageRef)
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
return String(page);
|
|
1054
|
-
}
|
|
1055
|
-
const resultTabs = tabs.map((item) => {
|
|
1056
|
-
if (item.href) return { tab: String(item.tab), href: String(item.href) };
|
|
1057
|
-
const groups = (item.groups ?? []).map((g) => ({
|
|
1058
|
-
group: String(g.group),
|
|
1059
|
-
pages: (g.pages ?? []).map(convertPageRef)
|
|
1060
|
-
}));
|
|
1061
|
-
return { tab: String(item.tab), groups };
|
|
1062
|
-
});
|
|
1063
|
-
if (!resultTabs.some((t) => t.tab === "Changelog")) {
|
|
1064
|
-
resultTabs.push({ tab: "Changelog", href: "/changelog" });
|
|
1065
|
-
}
|
|
1066
|
-
return { tabs: resultTabs };
|
|
1067
|
-
}
|
|
1068
|
-
function parseMintConfig(config, docsDir) {
|
|
1069
|
-
const nav = config.navigation;
|
|
1070
|
-
if (nav && typeof nav === "object" && !Array.isArray(nav)) {
|
|
1071
|
-
const v3Tabs = nav.tabs;
|
|
1072
|
-
if (Array.isArray(v3Tabs) && v3Tabs.length > 0) return convertMintTabs(v3Tabs, docsDir);
|
|
1073
|
-
}
|
|
1074
|
-
if (!Array.isArray(nav) || nav.length === 0) return null;
|
|
1075
|
-
if ("tab" in nav[0]) return convertMintTabs(nav, docsDir);
|
|
1076
|
-
function convertPageRef(page) {
|
|
1077
|
-
if (typeof page === "string") return normalizePageRef(page, docsDir);
|
|
1078
|
-
if (page !== null && typeof page === "object" && "group" in page && "pages" in page) {
|
|
1079
|
-
const p = page;
|
|
1080
|
-
return { group: String(p.group), pages: (p.pages ?? []).map(convertPageRef) };
|
|
1081
|
-
}
|
|
1082
|
-
return String(page);
|
|
1083
|
-
}
|
|
1084
|
-
const groups = nav.map((item) => ({
|
|
1085
|
-
group: String(item.group ?? ""),
|
|
1086
|
-
pages: (item.pages ?? []).map(convertPageRef)
|
|
1087
|
-
}));
|
|
1088
|
-
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
1089
|
-
}
|
|
1090
|
-
function parseGitBookSummary(cloneDir, docsDir) {
|
|
1091
|
-
const candidates = [join6(cloneDir, "SUMMARY.md")];
|
|
1092
|
-
if (docsDir) candidates.push(join6(cloneDir, docsDir, "SUMMARY.md"));
|
|
1093
|
-
let raw = "";
|
|
1094
|
-
for (const p of candidates) {
|
|
1095
|
-
if (existsSync5(p)) {
|
|
1096
|
-
raw = readFileSync5(p, "utf8");
|
|
1097
|
-
break;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
if (!raw) return null;
|
|
1101
|
-
const groups = [];
|
|
1102
|
-
let currentGroupName = "Overview";
|
|
1103
|
-
let currentPages = [];
|
|
1104
|
-
for (const line of raw.split("\n")) {
|
|
1105
|
-
const groupMatch = line.match(/^##\s+(.+)/);
|
|
1106
|
-
if (groupMatch) {
|
|
1107
|
-
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
1108
|
-
currentGroupName = groupMatch[1].trim();
|
|
1109
|
-
currentPages = [];
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
const pageMatch = line.match(/^\*\s+\[.+?\]\((.+?)\)/);
|
|
1113
|
-
if (pageMatch) {
|
|
1114
|
-
const ref = pageMatch[1].trim();
|
|
1115
|
-
if (ref.startsWith("http")) continue;
|
|
1116
|
-
currentPages.push(normalizePageRef(ref, docsDir));
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
if (currentPages.length > 0) groups.push({ group: currentGroupName, pages: currentPages });
|
|
1120
|
-
if (groups.length === 0) return null;
|
|
1121
|
-
return { tabs: [{ tab: "Docs", groups }, { tab: "Changelog", href: "/changelog" }] };
|
|
1122
|
-
}
|
|
1123
|
-
function parseNextraMeta(cloneDir, docsDir) {
|
|
1124
|
-
const baseDir = docsDir ? join6(cloneDir, docsDir) : cloneDir;
|
|
1125
|
-
const metaPath = join6(baseDir, "_meta.json");
|
|
1126
|
-
if (!existsSync5(metaPath)) return null;
|
|
1127
|
-
try {
|
|
1128
|
-
const meta = JSON.parse(readFileSync5(metaPath, "utf8"));
|
|
1129
|
-
const pages = [];
|
|
1130
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
1131
|
-
if (typeof value === "object" && value !== null) {
|
|
1132
|
-
const v = value;
|
|
1133
|
-
if (v.type === "separator" || v.type === "menu") continue;
|
|
1134
|
-
}
|
|
1135
|
-
pages.push(key === "index" ? "introduction" : slugify2(key));
|
|
1136
|
-
}
|
|
1137
|
-
if (pages.length === 0) return null;
|
|
1138
|
-
return {
|
|
1139
|
-
tabs: [
|
|
1140
|
-
{ tab: "Docs", groups: [{ group: "Overview", pages }] },
|
|
1141
|
-
{ tab: "Changelog", href: "/changelog" }
|
|
1142
|
-
]
|
|
1143
|
-
};
|
|
1144
|
-
} catch {
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
var PLATFORM_LABELS = {
|
|
1149
|
-
mintlify: "Mintlify",
|
|
1150
|
-
docusaurus: "Docusaurus",
|
|
1151
|
-
gitbook: "GitBook",
|
|
1152
|
-
nextra: "Nextra",
|
|
1153
|
-
vitepress: "VitePress",
|
|
1154
|
-
starlight: "Starlight (Astro)",
|
|
1155
|
-
thally: "Thally",
|
|
1156
|
-
unknown: "unknown"
|
|
1157
|
-
};
|
|
1158
|
-
function detectNavFromConfig(cloneDir, docsDir, platform) {
|
|
1159
|
-
const detected = platform ?? detectPlatform(cloneDir);
|
|
1160
|
-
const label = PLATFORM_LABELS[detected];
|
|
1161
|
-
switch (detected) {
|
|
1162
|
-
case "thally": {
|
|
1163
|
-
try {
|
|
1164
|
-
const parsed = JSON.parse(
|
|
1165
|
-
readFileSync5(join6(cloneDir, "docs.json"), "utf8")
|
|
1166
|
-
);
|
|
1167
|
-
console.log(` \u{1F4CB} Detected ${label} \u2014 using docs.json navigation as-is`);
|
|
1168
|
-
return parsed;
|
|
1169
|
-
} catch {
|
|
1170
|
-
return null;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
case "mintlify": {
|
|
1174
|
-
for (const file of ["docs.json", "mint.json"]) {
|
|
1175
|
-
const p = join6(cloneDir, file);
|
|
1176
|
-
if (!existsSync5(p)) continue;
|
|
1177
|
-
try {
|
|
1178
|
-
const config = JSON.parse(readFileSync5(p, "utf8"));
|
|
1179
|
-
const nav = parseMintConfig(config, docsDir);
|
|
1180
|
-
if (nav) {
|
|
1181
|
-
console.log(` \u{1F4CB} Detected ${label} (${file}) \u2014 converting navigation`);
|
|
1182
|
-
return nav;
|
|
1183
|
-
}
|
|
1184
|
-
} catch {
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
|
-
case "gitbook": {
|
|
1190
|
-
const nav = parseGitBookSummary(cloneDir, docsDir);
|
|
1191
|
-
if (nav) console.log(` \u{1F4CB} Detected ${label} (SUMMARY.md) \u2014 converting navigation`);
|
|
1192
|
-
return nav;
|
|
1193
|
-
}
|
|
1194
|
-
case "nextra": {
|
|
1195
|
-
const nav = parseNextraMeta(cloneDir, docsDir);
|
|
1196
|
-
if (nav) console.log(` \u{1F4CB} Detected ${label} (_meta.json) \u2014 converting navigation`);
|
|
1197
|
-
return nav;
|
|
1198
|
-
}
|
|
1199
|
-
case "docusaurus":
|
|
1200
|
-
case "vitepress":
|
|
1201
|
-
case "starlight":
|
|
1202
|
-
console.log(` \u{1F4CB} Detected ${label} \u2014 nav config is JavaScript, using directory structure`);
|
|
1203
|
-
return null;
|
|
1204
|
-
default:
|
|
1205
|
-
return null;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// src/lib/migrate/index.ts
|
|
1210
|
-
function mergeDocsJson(existing, incoming) {
|
|
1211
|
-
const existingTabNames = new Set(existing.tabs.map((t) => t.tab));
|
|
1212
|
-
const merged = { tabs: [...existing.tabs.filter((t) => t.tab !== "Changelog")] };
|
|
1213
|
-
if (existing.ai || incoming.ai) {
|
|
1214
|
-
merged.ai = { ...incoming.ai, ...existing.ai };
|
|
1215
|
-
}
|
|
1216
|
-
for (const tab of incoming.tabs) {
|
|
1217
|
-
if (tab.tab === "Changelog") continue;
|
|
1218
|
-
if (existingTabNames.has(tab.tab)) {
|
|
1219
|
-
const existingTab = merged.tabs.find((t) => t.tab === tab.tab);
|
|
1220
|
-
if (existingTab.groups && tab.groups) {
|
|
1221
|
-
const existingGroupNames = new Set(existingTab.groups.map((g) => g.group));
|
|
1222
|
-
for (const group of tab.groups) {
|
|
1223
|
-
if (existingGroupNames.has(group.group)) {
|
|
1224
|
-
const eg = existingTab.groups.find((g) => g.group === group.group);
|
|
1225
|
-
const existingPageSet = new Set(eg.pages.map((p) => typeof p === "string" ? p : p.group));
|
|
1226
|
-
for (const page of group.pages) {
|
|
1227
|
-
const key = typeof page === "string" ? page : page.group;
|
|
1228
|
-
if (!existingPageSet.has(key)) eg.pages.push(page);
|
|
1229
|
-
}
|
|
1230
|
-
} else {
|
|
1231
|
-
existingTab.groups.push(group);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
} else if (tab.groups) {
|
|
1235
|
-
existingTab.groups = tab.groups;
|
|
1236
|
-
}
|
|
1237
|
-
} else {
|
|
1238
|
-
merged.tabs.push(tab);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
merged.tabs.push({ tab: "Changelog", href: "/changelog" });
|
|
1242
|
-
return merged;
|
|
1243
|
-
}
|
|
1244
|
-
function injectApiTab(config, specFilename) {
|
|
1245
|
-
const apiTab = { tab: "API Reference", api: { source: `/${specFilename}` } };
|
|
1246
|
-
const tabs = config.tabs.filter((t) => {
|
|
1247
|
-
if (t.tab.toLowerCase().includes("api")) return false;
|
|
1248
|
-
return true;
|
|
1249
|
-
});
|
|
1250
|
-
const changelogIdx = tabs.findIndex((t) => t.tab === "Changelog");
|
|
1251
|
-
if (changelogIdx >= 0) {
|
|
1252
|
-
tabs.splice(changelogIdx, 0, apiTab);
|
|
1253
|
-
} else {
|
|
1254
|
-
tabs.push(apiTab);
|
|
1255
|
-
}
|
|
1256
|
-
return { ...config, tabs };
|
|
1257
|
-
}
|
|
1258
|
-
function installDeps2(targetDir) {
|
|
1259
|
-
execSync3("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
1260
|
-
}
|
|
1261
|
-
function initGit2(targetDir) {
|
|
1262
|
-
try {
|
|
1263
|
-
execSync3("git init", { cwd: targetDir, stdio: "inherit" });
|
|
1264
|
-
execSync3("git add -A", { cwd: targetDir, stdio: "inherit" });
|
|
1265
|
-
execSync3('git commit -m "Initial commit from create-thally-docs"', { cwd: targetDir, stdio: "inherit" });
|
|
1266
|
-
} catch {
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
async function migrateDocs(opts) {
|
|
1270
|
-
const { sourceUrl, projectDir: rawProjectDir, into, apiKey, projectName } = opts;
|
|
1271
|
-
const projectDir = resolve2(rawProjectDir);
|
|
1272
|
-
const source = parseGitHubUrl(sourceUrl);
|
|
1273
|
-
if (opts.branch) source.branch = opts.branch;
|
|
1274
|
-
if (!into) {
|
|
1275
|
-
console.log(` \u{1F3D7} Scaffolding new project at ${projectDir}...`);
|
|
1276
|
-
await scaffold({
|
|
1277
|
-
projectDir,
|
|
1278
|
-
projectName: projectName ?? "My Docs",
|
|
1279
|
-
description: `Documentation migrated from ${source.owner}/${source.repo}`,
|
|
1280
|
-
brandPreset: "primary",
|
|
1281
|
-
repoUrl: `https://github.com/${source.owner}/${source.repo}`,
|
|
1282
|
-
doInstall: false
|
|
1283
|
-
});
|
|
1284
|
-
} else {
|
|
1285
|
-
if (!existsSync6(projectDir)) {
|
|
1286
|
-
throw new Error(`Project directory "${projectDir}" does not exist.`);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
const tmpBase = mkdtempSync(join7(tmpdir(), "thally-migrate-"));
|
|
1290
|
-
const cloneDir = join7(tmpBase, "repo");
|
|
1291
|
-
console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
|
|
1292
|
-
try {
|
|
1293
|
-
await cloneRepo(source, cloneDir);
|
|
1294
|
-
const docsDir = opts.docsDir ?? (source.docsDir || detectDocsDir(cloneDir));
|
|
1295
|
-
const docFiles = findDocFiles(cloneDir, docsDir);
|
|
1296
|
-
const docsDirLabel = docsDir ? `${docsDir}/` : "repo root";
|
|
1297
|
-
console.log(` \u{1F4C4} Found ${docFiles.length} files in ${docsDirLabel}`);
|
|
1298
|
-
if (docFiles.length === 0) {
|
|
1299
|
-
console.warn(" \u26A0 No doc files found. Check the URL and try again.");
|
|
1300
|
-
return { pagesWritten: 0, projectDir };
|
|
1301
|
-
}
|
|
1302
|
-
const platform = detectPlatform(cloneDir);
|
|
1303
|
-
const detectedNav = detectNavFromConfig(cloneDir, docsDir, platform);
|
|
1304
|
-
const openApiSpec = detectOpenApiSpec(cloneDir);
|
|
1305
|
-
if (openApiSpec) {
|
|
1306
|
-
console.log(` \u{1F50C} Found OpenAPI spec: ${openApiSpec.filename}`);
|
|
1307
|
-
}
|
|
1308
|
-
const limit = pLimit(5);
|
|
1309
|
-
let doneCount = 0;
|
|
1310
|
-
const imported = (await Promise.all(
|
|
1311
|
-
docFiles.map(
|
|
1312
|
-
(file) => limit(async () => {
|
|
1313
|
-
try {
|
|
1314
|
-
const result = await importFile(file, apiKey);
|
|
1315
|
-
doneCount++;
|
|
1316
|
-
if (result) {
|
|
1317
|
-
console.log(` [${doneCount}/${docFiles.length}] ${result.pageId}`);
|
|
1318
|
-
} else {
|
|
1319
|
-
console.log(` [${doneCount}/${docFiles.length}] ${file.pageId} (openapi \u2014 wired via spec)`);
|
|
1320
|
-
}
|
|
1321
|
-
return result;
|
|
1322
|
-
} catch (err) {
|
|
1323
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1324
|
-
if (msg.includes("no API key")) {
|
|
1325
|
-
console.warn(` \u26A0 ${msg}`);
|
|
1326
|
-
} else {
|
|
1327
|
-
console.warn(` \u26A0 Skipping ${file.relPath}: ${msg}`);
|
|
1328
|
-
}
|
|
1329
|
-
doneCount++;
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
})
|
|
1333
|
-
)
|
|
1334
|
-
)).filter(Boolean);
|
|
1335
|
-
const pageIdSeen = /* @__PURE__ */ new Set();
|
|
1336
|
-
const deduped = imported.filter((p) => {
|
|
1337
|
-
if (pageIdSeen.has(p.pageId)) return false;
|
|
1338
|
-
pageIdSeen.add(p.pageId);
|
|
1339
|
-
return true;
|
|
1340
|
-
});
|
|
1341
|
-
const contentDir = join7(projectDir, "src", "content");
|
|
1342
|
-
let pagesWritten = 0;
|
|
1343
|
-
for (const page of deduped) {
|
|
1344
|
-
const filePath = join7(contentDir, `${page.pageId}.mdx`);
|
|
1345
|
-
mkdirSync3(dirname2(filePath), { recursive: true });
|
|
1346
|
-
const mdx = [
|
|
1347
|
-
"---",
|
|
1348
|
-
`title: "${page.frontmatter.title.replace(/"/g, '\\"')}"`,
|
|
1349
|
-
`description: "${page.frontmatter.description.replace(/"/g, '\\"')}"`,
|
|
1350
|
-
page.frontmatter.keywords.length > 0 ? `keywords: [${page.frontmatter.keywords.map((k) => `"${k.replace(/"/g, '\\"')}"`).join(", ")}]` : null,
|
|
1351
|
-
"---",
|
|
1352
|
-
"",
|
|
1353
|
-
page.body
|
|
1354
|
-
].filter((line) => line !== null).join("\n");
|
|
1355
|
-
writeFileSync5(filePath, mdx, "utf8");
|
|
1356
|
-
pagesWritten++;
|
|
1357
|
-
}
|
|
1358
|
-
let finalNav = detectedNav ?? buildNavStructure(deduped);
|
|
1359
|
-
if (openApiSpec) {
|
|
1360
|
-
const publicDir = join7(projectDir, "public");
|
|
1361
|
-
mkdirSync3(publicDir, { recursive: true });
|
|
1362
|
-
copyFileSync(openApiSpec.absPath, join7(publicDir, openApiSpec.filename));
|
|
1363
|
-
console.log(` \u{1F4CB} Copied ${openApiSpec.filename} \u2192 public/${openApiSpec.filename}`);
|
|
1364
|
-
finalNav = injectApiTab(finalNav, openApiSpec.filename);
|
|
1365
|
-
}
|
|
1366
|
-
if (into && existsSync6(join7(projectDir, "docs.json"))) {
|
|
1367
|
-
const existing = readDocsJson(projectDir);
|
|
1368
|
-
const merged = mergeDocsJson(existing, finalNav);
|
|
1369
|
-
writeDocsJson(projectDir, merged);
|
|
1370
|
-
} else {
|
|
1371
|
-
writeDocsJson(projectDir, finalNav);
|
|
1372
|
-
}
|
|
1373
|
-
if (!into) {
|
|
1374
|
-
installDeps2(projectDir);
|
|
1375
|
-
initGit2(projectDir);
|
|
1376
|
-
}
|
|
1377
|
-
return { pagesWritten, projectDir };
|
|
1378
|
-
} finally {
|
|
1379
|
-
rmSync(tmpBase, { recursive: true, force: true });
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// src/tools/migrate-docs.ts
|
|
530
|
+
import { migrateDocs } from "create-thally-docs/migrate";
|
|
1384
531
|
var migrateDocsSchema = z6.object({
|
|
1385
|
-
sourceUrl: z6.string().describe("GitHub URL
|
|
532
|
+
sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
|
|
1386
533
|
projectDir: z6.string().describe("Path for new project or existing project dir"),
|
|
1387
534
|
into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
|
|
1388
535
|
branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
|
|
1389
536
|
docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
|
|
1390
|
-
apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion")
|
|
537
|
+
apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
|
|
538
|
+
maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import")
|
|
1391
539
|
});
|
|
1392
540
|
async function handleMigrateDocs(input) {
|
|
1393
541
|
const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
@@ -1398,6 +546,7 @@ async function handleMigrateDocs(input) {
|
|
|
1398
546
|
apiKey,
|
|
1399
547
|
branch: input.branch,
|
|
1400
548
|
docsDir: input.docsDir,
|
|
549
|
+
maxPages: input.maxPages,
|
|
1401
550
|
yes: true
|
|
1402
551
|
});
|
|
1403
552
|
return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
|
|
@@ -1405,9 +554,9 @@ async function handleMigrateDocs(input) {
|
|
|
1405
554
|
|
|
1406
555
|
// src/tools/search-docs.ts
|
|
1407
556
|
import { z as z7 } from "zod";
|
|
1408
|
-
import { readdirSync as
|
|
1409
|
-
import { join as
|
|
1410
|
-
import
|
|
557
|
+
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
558
|
+
import { join as join5, relative, extname } from "path";
|
|
559
|
+
import matter2 from "gray-matter";
|
|
1411
560
|
var searchDocsSchema = z7.object({
|
|
1412
561
|
projectDir: z7.string().describe("Path to the Thally project root"),
|
|
1413
562
|
query: z7.string().describe("Search query"),
|
|
@@ -1416,17 +565,17 @@ var searchDocsSchema = z7.object({
|
|
|
1416
565
|
function scanMdxFiles(dir, results) {
|
|
1417
566
|
let entries;
|
|
1418
567
|
try {
|
|
1419
|
-
entries =
|
|
568
|
+
entries = readdirSync2(dir);
|
|
1420
569
|
} catch {
|
|
1421
570
|
return;
|
|
1422
571
|
}
|
|
1423
572
|
for (const entry of entries) {
|
|
1424
|
-
const fullPath =
|
|
573
|
+
const fullPath = join5(dir, entry);
|
|
1425
574
|
try {
|
|
1426
|
-
const stat =
|
|
575
|
+
const stat = statSync(fullPath);
|
|
1427
576
|
if (stat.isDirectory()) {
|
|
1428
577
|
scanMdxFiles(fullPath, results);
|
|
1429
|
-
} else if (
|
|
578
|
+
} else if (extname(entry).toLowerCase() === ".mdx") {
|
|
1430
579
|
results.push(fullPath);
|
|
1431
580
|
}
|
|
1432
581
|
} catch {
|
|
@@ -1439,15 +588,15 @@ function scoreFiles(files, contentDir, query) {
|
|
|
1439
588
|
for (const filePath of files) {
|
|
1440
589
|
let raw;
|
|
1441
590
|
try {
|
|
1442
|
-
raw =
|
|
591
|
+
raw = readFileSync4(filePath, "utf8");
|
|
1443
592
|
} catch {
|
|
1444
593
|
continue;
|
|
1445
594
|
}
|
|
1446
|
-
const { data, content } =
|
|
595
|
+
const { data, content } = matter2(raw);
|
|
1447
596
|
const title = data.title ?? "";
|
|
1448
597
|
const description = data.description ?? "";
|
|
1449
598
|
const keywords = data.keywords ?? [];
|
|
1450
|
-
const pageId =
|
|
599
|
+
const pageId = relative(contentDir, filePath).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
1451
600
|
let score = 0;
|
|
1452
601
|
for (const term of terms) {
|
|
1453
602
|
if (title.toLowerCase().includes(term)) score += 3;
|
|
@@ -1464,8 +613,8 @@ function scoreFiles(files, contentDir, query) {
|
|
|
1464
613
|
}
|
|
1465
614
|
async function handleSearchDocs(input) {
|
|
1466
615
|
const { projectDir, query, limit = 5 } = input;
|
|
1467
|
-
const contentDir =
|
|
1468
|
-
if (!
|
|
616
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
617
|
+
if (!existsSync4(contentDir)) {
|
|
1469
618
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
1470
619
|
}
|
|
1471
620
|
const files = [];
|
|
@@ -1560,23 +709,23 @@ async function handleAgentReadiness(input) {
|
|
|
1560
709
|
|
|
1561
710
|
// src/tools/read-page.ts
|
|
1562
711
|
import { z as z10 } from "zod";
|
|
1563
|
-
import { existsSync as
|
|
1564
|
-
import { join as
|
|
1565
|
-
import
|
|
712
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
713
|
+
import { join as join6 } from "path";
|
|
714
|
+
import matter3 from "gray-matter";
|
|
1566
715
|
var readPageSchema = z10.object({
|
|
1567
716
|
projectDir: z10.string().describe("Path to the Thally project root"),
|
|
1568
717
|
pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
|
|
1569
718
|
});
|
|
1570
719
|
async function handleReadPage(input) {
|
|
1571
720
|
const { projectDir, pageId } = input;
|
|
1572
|
-
const contentDir =
|
|
721
|
+
const contentDir = join6(projectDir, "src", "content");
|
|
1573
722
|
const candidates = [
|
|
1574
|
-
|
|
1575
|
-
|
|
723
|
+
join6(contentDir, `${pageId}.mdx`),
|
|
724
|
+
join6(contentDir, `${pageId}/index.mdx`)
|
|
1576
725
|
];
|
|
1577
726
|
let filePath = null;
|
|
1578
727
|
for (const c of candidates) {
|
|
1579
|
-
if (
|
|
728
|
+
if (existsSync5(c)) {
|
|
1580
729
|
filePath = c;
|
|
1581
730
|
break;
|
|
1582
731
|
}
|
|
@@ -1584,8 +733,8 @@ async function handleReadPage(input) {
|
|
|
1584
733
|
if (!filePath) {
|
|
1585
734
|
throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
|
|
1586
735
|
}
|
|
1587
|
-
const raw =
|
|
1588
|
-
const { data, content } =
|
|
736
|
+
const raw = readFileSync5(filePath, "utf8");
|
|
737
|
+
const { data, content } = matter3(raw);
|
|
1589
738
|
const title = data.title ?? pageId;
|
|
1590
739
|
const description = data.description ?? "";
|
|
1591
740
|
const lines = [`# ${title}`, `*${pageId}*`, ""];
|
|
@@ -1599,9 +748,9 @@ async function handleReadPage(input) {
|
|
|
1599
748
|
|
|
1600
749
|
// src/tools/get-context.ts
|
|
1601
750
|
import { z as z11 } from "zod";
|
|
1602
|
-
import { existsSync as
|
|
1603
|
-
import { join as
|
|
1604
|
-
import
|
|
751
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
752
|
+
import { join as join7 } from "path";
|
|
753
|
+
import matter4 from "gray-matter";
|
|
1605
754
|
var getContextSchema = z11.object({
|
|
1606
755
|
projectDir: z11.string().describe("Path to the Thally project root"),
|
|
1607
756
|
topic: z11.string().describe("Topic or question to find relevant docs for"),
|
|
@@ -1609,8 +758,8 @@ var getContextSchema = z11.object({
|
|
|
1609
758
|
});
|
|
1610
759
|
async function handleGetContext(input) {
|
|
1611
760
|
const { projectDir, topic, maxTokens = 4e3 } = input;
|
|
1612
|
-
const contentDir =
|
|
1613
|
-
if (!
|
|
761
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
762
|
+
if (!existsSync6(contentDir)) {
|
|
1614
763
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
1615
764
|
}
|
|
1616
765
|
const files = [];
|
|
@@ -1624,14 +773,14 @@ async function handleGetContext(input) {
|
|
|
1624
773
|
const sections = [];
|
|
1625
774
|
for (const result of scored) {
|
|
1626
775
|
const candidates = [
|
|
1627
|
-
|
|
1628
|
-
|
|
776
|
+
join7(contentDir, `${result.pageId}.mdx`),
|
|
777
|
+
join7(contentDir, `${result.pageId}/index.mdx`)
|
|
1629
778
|
];
|
|
1630
779
|
let content = "";
|
|
1631
780
|
for (const c of candidates) {
|
|
1632
|
-
if (
|
|
1633
|
-
const raw =
|
|
1634
|
-
const { content: body } =
|
|
781
|
+
if (existsSync6(c)) {
|
|
782
|
+
const raw = readFileSync6(c, "utf8");
|
|
783
|
+
const { content: body } = matter4(raw);
|
|
1635
784
|
content = body.trim();
|
|
1636
785
|
break;
|
|
1637
786
|
}
|
|
@@ -1655,9 +804,9 @@ async function handleGetContext(input) {
|
|
|
1655
804
|
|
|
1656
805
|
// src/tools/lint-project.ts
|
|
1657
806
|
import { z as z12 } from "zod";
|
|
1658
|
-
import { existsSync as
|
|
1659
|
-
import { join as
|
|
1660
|
-
import
|
|
807
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
808
|
+
import { join as join8 } from "path";
|
|
809
|
+
import matter5 from "gray-matter";
|
|
1661
810
|
var lintProjectSchema = z12.object({
|
|
1662
811
|
projectDir: z12.string().describe("Path to the Thally project root"),
|
|
1663
812
|
fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
|
|
@@ -1688,9 +837,9 @@ function addOrphanToNav(projectDir, pageId) {
|
|
|
1688
837
|
}
|
|
1689
838
|
async function handleLintProject(input) {
|
|
1690
839
|
const { projectDir, fix = false } = input;
|
|
1691
|
-
const contentDir =
|
|
840
|
+
const contentDir = join8(projectDir, "src", "content");
|
|
1692
841
|
const issues = [];
|
|
1693
|
-
if (!
|
|
842
|
+
if (!existsSync7(join8(projectDir, "docs.json"))) {
|
|
1694
843
|
throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
|
|
1695
844
|
}
|
|
1696
845
|
const config = readDocsJson(projectDir);
|
|
@@ -1709,10 +858,10 @@ async function handleLintProject(input) {
|
|
|
1709
858
|
}
|
|
1710
859
|
for (const pageId of navPageIds) {
|
|
1711
860
|
const candidates = [
|
|
1712
|
-
|
|
1713
|
-
|
|
861
|
+
join8(contentDir, `${pageId}.mdx`),
|
|
862
|
+
join8(contentDir, `${pageId}/index.mdx`)
|
|
1714
863
|
];
|
|
1715
|
-
if (!candidates.some((c) =>
|
|
864
|
+
if (!candidates.some((c) => existsSync7(c))) {
|
|
1716
865
|
issues.push({
|
|
1717
866
|
severity: "error",
|
|
1718
867
|
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
@@ -1721,7 +870,7 @@ async function handleLintProject(input) {
|
|
|
1721
870
|
}
|
|
1722
871
|
}
|
|
1723
872
|
const allFiles = [];
|
|
1724
|
-
if (
|
|
873
|
+
if (existsSync7(contentDir)) {
|
|
1725
874
|
scanMdxFiles(contentDir, allFiles);
|
|
1726
875
|
}
|
|
1727
876
|
const fixedOrphans = [];
|
|
@@ -1739,8 +888,8 @@ async function handleLintProject(input) {
|
|
|
1739
888
|
let data = {};
|
|
1740
889
|
let content = "";
|
|
1741
890
|
try {
|
|
1742
|
-
const raw =
|
|
1743
|
-
const parsed =
|
|
891
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
892
|
+
const parsed = matter5(raw);
|
|
1744
893
|
data = parsed.data;
|
|
1745
894
|
content = parsed.content;
|
|
1746
895
|
} catch {
|
|
@@ -1796,11 +945,11 @@ async function handleLintProject(input) {
|
|
|
1796
945
|
|
|
1797
946
|
// src/tools/translate-docs.ts
|
|
1798
947
|
import { z as z13 } from "zod";
|
|
1799
|
-
import { readFileSync as
|
|
1800
|
-
import { join as
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
1803
|
-
import
|
|
948
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
|
|
949
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
950
|
+
import matter6 from "gray-matter";
|
|
951
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
952
|
+
import pLimit from "p-limit";
|
|
1804
953
|
var translateDocsSchema = z13.object({
|
|
1805
954
|
projectDir: z13.string().describe("Path to the Thally project directory"),
|
|
1806
955
|
locale: z13.string().describe('Target locale code, e.g. "es", "fr"'),
|
|
@@ -1810,8 +959,8 @@ var translateDocsSchema = z13.object({
|
|
|
1810
959
|
model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
|
|
1811
960
|
});
|
|
1812
961
|
function readDocsJson2(projectDir) {
|
|
1813
|
-
const docsPath =
|
|
1814
|
-
const raw =
|
|
962
|
+
const docsPath = join9(projectDir, "docs.json");
|
|
963
|
+
const raw = readFileSync8(docsPath, "utf8");
|
|
1815
964
|
return JSON.parse(raw);
|
|
1816
965
|
}
|
|
1817
966
|
function collectPageIds(pages) {
|
|
@@ -1853,12 +1002,12 @@ function getAllPageIds(config) {
|
|
|
1853
1002
|
return { ids, hrefOnlyPages };
|
|
1854
1003
|
}
|
|
1855
1004
|
function findSourceFile(projectDir, pageId) {
|
|
1856
|
-
const contentRoot =
|
|
1005
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
1857
1006
|
const candidates = [
|
|
1858
|
-
|
|
1859
|
-
|
|
1007
|
+
join9(contentRoot, `${pageId}.mdx`),
|
|
1008
|
+
join9(contentRoot, `${pageId}/index.mdx`)
|
|
1860
1009
|
];
|
|
1861
|
-
return candidates.find((p) =>
|
|
1010
|
+
return candidates.find((p) => existsSync8(p)) ?? null;
|
|
1862
1011
|
}
|
|
1863
1012
|
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
1864
1013
|
|
|
@@ -1910,7 +1059,7 @@ async function handleTranslateDocs(input) {
|
|
|
1910
1059
|
}
|
|
1911
1060
|
const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
|
|
1912
1061
|
const targetPageIds = pages ?? allPageIds;
|
|
1913
|
-
const contentRoot =
|
|
1062
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
1914
1063
|
const toTranslate = [];
|
|
1915
1064
|
const skipped = [];
|
|
1916
1065
|
for (const pageId of targetPageIds) {
|
|
@@ -1920,8 +1069,8 @@ async function handleTranslateDocs(input) {
|
|
|
1920
1069
|
continue;
|
|
1921
1070
|
}
|
|
1922
1071
|
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
1923
|
-
const targetFile =
|
|
1924
|
-
if (
|
|
1072
|
+
const targetFile = join9(contentRoot, locale, relativeFromContent);
|
|
1073
|
+
if (existsSync8(targetFile) && !force) {
|
|
1925
1074
|
skipped.push(`${pageId} (already translated)`);
|
|
1926
1075
|
continue;
|
|
1927
1076
|
}
|
|
@@ -1930,21 +1079,21 @@ async function handleTranslateDocs(input) {
|
|
|
1930
1079
|
if (toTranslate.length === 0) {
|
|
1931
1080
|
return `Nothing to translate. ${skipped.length} page(s) skipped.`;
|
|
1932
1081
|
}
|
|
1933
|
-
const client = new
|
|
1934
|
-
const limit =
|
|
1082
|
+
const client = new Anthropic({ apiKey });
|
|
1083
|
+
const limit = pLimit(3);
|
|
1935
1084
|
const results = [];
|
|
1936
1085
|
await Promise.all(
|
|
1937
1086
|
toTranslate.map(
|
|
1938
1087
|
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
1939
1088
|
try {
|
|
1940
|
-
const sourceContent =
|
|
1941
|
-
const parsed =
|
|
1089
|
+
const sourceContent = readFileSync8(sourceFile, "utf8");
|
|
1090
|
+
const parsed = matter6(sourceContent);
|
|
1942
1091
|
if (!parsed.data.title) {
|
|
1943
1092
|
console.warn(`[translate] ${pageId}: missing title in frontmatter`);
|
|
1944
1093
|
}
|
|
1945
1094
|
const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
|
|
1946
|
-
|
|
1947
|
-
|
|
1095
|
+
mkdirSync3(dirname2(targetFile), { recursive: true });
|
|
1096
|
+
writeFileSync6(targetFile, translated + "\n", "utf8");
|
|
1948
1097
|
results.push({ pageId, success: true });
|
|
1949
1098
|
} catch (err) {
|
|
1950
1099
|
const msg = err instanceof Error ? err.message : String(err);
|