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