@thallylabs/mcp 0.8.1 → 0.9.0

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 CHANGED
@@ -11,258 +11,18 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { z } from "zod";
12
12
 
13
13
  // src/lib/scaffold.ts
14
- import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, cpSync } from "fs";
15
- import { resolve, join } from "path";
16
- import { execSync } from "child_process";
17
- import { Readable, pipeline } from "stream";
18
- import { promisify } from "util";
19
- import tar from "tar";
20
- var pipelineAsync = promisify(pipeline);
21
- var MCP_TEMPLATE_REPOSITORY = "thallylabs/docs";
22
- var TARBALL_URL = `https://codeload.github.com/${MCP_TEMPLATE_REPOSITORY}/tar.gz/main`;
23
- var MCP_EXCLUDE_PATHS = [
24
- "/cli/",
25
- "/packages/",
26
- "/node_modules/",
27
- "/.git/",
28
- "/thally-track.yml",
29
- "/CODEOWNERS",
30
- "/CLAUDE.md",
31
- "/notes/"
32
- ];
33
- function shouldIncludeMcpTemplatePath(path) {
34
- return !MCP_EXCLUDE_PATHS.some((excluded) => path.includes(excluded));
35
- }
36
- var STARTER_PAGES = {
37
- "introduction.mdx": `---
38
- title: Introduction
39
- description: Welcome to {NAME} documentation.
40
- ---
41
-
42
- ## Welcome
43
-
44
- This is the home page of your **{NAME}** documentation site, powered by [Thally](https://github.com/thallylabs/thally).
45
-
46
- Get started by editing this file at \`src/content/introduction.mdx\`.
47
- `,
48
- "quickstart.mdx": `---
49
- title: Quickstart
50
- description: Get up and running with {NAME} in under 5 minutes.
51
- ---
52
-
53
- ## Installation
54
-
55
- \`\`\`bash
56
- npm install {SLUG}
57
- \`\`\`
58
-
59
- ## Basic usage
60
-
61
- \`\`\`ts
62
- import { create } from '{SLUG}'
63
-
64
- const client = create({ apiKey: 'your-api-key' })
65
- \`\`\`
66
-
67
- That's it \u2014 you're ready to go!
68
- `
69
- };
70
- function buildStarterDocsJson({
71
- enableAiChat,
72
- repoUrl,
73
- i18nLocales
74
- }) {
75
- const config = {};
76
- if (enableAiChat) {
77
- config.ai = { chat: true };
78
- }
79
- if (repoUrl) {
80
- config.navbar = {
81
- links: [{ label: "GitHub", href: repoUrl, type: "github" }],
82
- primary: { label: "Get started", href: "/quickstart" }
83
- };
84
- }
85
- if (i18nLocales && i18nLocales.length > 0) {
86
- config.i18n = {
87
- defaultLocale: "en",
88
- locales: [{ code: "en", label: "English" }, ...i18nLocales]
89
- };
90
- }
91
- config.tabs = [
92
- {
93
- tab: "Overview",
94
- groups: [{ group: "Getting Started", pages: ["introduction", "quickstart"] }]
95
- },
96
- { tab: "API Reference", api: { source: "openapi.yaml" } },
97
- { tab: "Changelog", href: "/changelog" }
98
- ];
99
- return JSON.stringify(config, null, 2) + "\n";
100
- }
101
- function slugify(name) {
102
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
103
- }
104
- function run(cmd, cwd) {
105
- execSync(cmd, { cwd, stdio: "inherit" });
106
- }
107
- async function downloadTemplate(targetDir) {
108
- const response = await fetch(TARBALL_URL);
109
- if (!response.ok) {
110
- throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
111
- }
112
- if (!response.body) {
113
- throw new Error("Response body is empty");
114
- }
115
- const nodeStream = Readable.fromWeb(response.body);
116
- await pipelineAsync(
117
- nodeStream,
118
- tar.extract({
119
- cwd: targetDir,
120
- strip: 1,
121
- filter: shouldIncludeMcpTemplatePath
122
- })
123
- );
124
- }
125
- function writeStarterContent(targetDir, projectName, slug, enableAiChat = true, repoUrl = "", i18nLocales) {
126
- const contentDir = join(targetDir, "src", "content");
127
- if (existsSync(contentDir)) {
128
- const entries = readdirSync(contentDir);
129
- for (const entry of entries) {
130
- execSync(`rm -rf "${join(contentDir, entry)}"`);
131
- }
132
- } else {
133
- mkdirSync(contentDir, { recursive: true });
134
- }
135
- for (const [filename, template] of Object.entries(STARTER_PAGES)) {
136
- const content = template.replace(/\{NAME\}/g, projectName).replace(/\{SLUG\}/g, slug);
137
- writeFileSync(join(contentDir, filename), content, "utf8");
138
- }
139
- writeFileSync(
140
- join(targetDir, "docs.json"),
141
- buildStarterDocsJson({ enableAiChat, repoUrl: repoUrl || void 0, i18nLocales }),
142
- "utf8"
143
- );
144
- }
145
- function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
146
- const siteFile = join(targetDir, "src", "data", "site.ts");
147
- if (!existsSync(siteFile)) return;
148
- let source = readFileSync(siteFile, "utf8");
149
- source = source.replace(/name:\s*'[^']*'/, `name: '${projectName.replace(/'/g, "\\'")}'`);
150
- source = source.replace(
151
- /description:\s*\n\s*'[^']*'/,
152
- `description:
153
- '${description.replace(/'/g, "\\'")}'`
154
- );
155
- source = source.replace(
156
- /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
157
- `const brandPreset: BrandPresetKey = '${brandPreset}'`
158
- );
159
- if (repoUrl) {
160
- source = source.replace(/repoUrl:\s*'[^']*'/, `repoUrl: '${repoUrl}'`);
161
- source = source.replace(
162
- /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
163
- `{ label: 'GitHub', href: '${repoUrl}' }`
164
- );
165
- source = source.replace(
166
- /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
167
- `{ label: 'Support', href: '${repoUrl}/issues/new' }`
168
- );
169
- }
170
- writeFileSync(siteFile, source, "utf8");
171
- }
172
- function patchTopBarNavigation(targetDir) {
173
- const filePath = join(targetDir, "src", "components", "layout", "top-bar.tsx");
174
- if (!existsSync(filePath)) return;
175
- const source = readFileSync(filePath, "utf8");
176
- if (!source.includes("target={isExternal ? '_blank' : undefined}")) return;
177
- const patched = source.replace(
178
- /if \(collection\.href\) \{\n const isExternal[^\n]+\n return \(\n <a[\s\S]*?<\/a>\n \)\n \}/,
179
- `if (collection.href) {
180
- const isExternal = /^https?:\\/\\//.test(collection.href)
181
- if (isExternal) {
182
- return (
183
- <a
184
- key={collection.id}
185
- href={collection.href}
186
- target="_blank"
187
- rel="noreferrer"
188
- className={baseClasses}
189
- >
190
- {collection.label}
191
- </a>
192
- )
193
- }
194
- return (
195
- <Link
196
- key={collection.id}
197
- href={collection.href}
198
- className={baseClasses}
199
- >
200
- {collection.label}
201
- </Link>
202
- )
203
- }`
204
- );
205
- writeFileSync(filePath, patched, "utf8");
206
- }
207
- function patchApiReferenceGuard(targetDir) {
208
- const filePath = join(targetDir, "src", "data", "api-reference.ts");
209
- if (!existsSync(filePath)) return;
210
- let source = readFileSync(filePath, "utf8");
211
- source = source.replace(
212
- /export async function buildApiNavigation\([^)]*\)[^{]*\{\n/,
213
- (match) => `${match} if (apiReferenceConfig.specs.length === 0) return []
214
- `
215
- );
216
- writeFileSync(filePath, source, "utf8");
217
- }
218
- function patchOpenApiFetch(targetDir) {
219
- const filePath = join(targetDir, "src", "lib", "openapi", "fetch.ts");
220
- if (!existsSync(filePath)) return;
221
- let source = readFileSync(filePath, "utf8");
222
- source = source.replace(
223
- /const absolutePath = path\.isAbsolute\(filePath\) \? filePath : path\.resolve\(process\.cwd\(\), filePath\)/,
224
- `const absolutePath = filePath.startsWith('/')
225
- ? path.resolve(process.cwd(), 'public', filePath.slice(1))
226
- : path.resolve(process.cwd(), filePath)`
227
- );
228
- writeFileSync(filePath, source, "utf8");
229
- }
230
- function updateEnvExample(targetDir) {
231
- const envFile = join(targetDir, ".env.example");
232
- if (existsSync(envFile)) {
233
- const envLocal = join(targetDir, ".env.local");
234
- if (!existsSync(envLocal)) cpSync(envFile, envLocal);
235
- }
236
- }
237
- function installDeps(targetDir) {
238
- run("npm install", targetDir);
239
- }
240
- function initGit(targetDir) {
241
- try {
242
- run("git init", targetDir);
243
- run("git add -A", targetDir);
244
- run('git commit -m "Initial commit from create-thally-docs"', targetDir);
245
- } catch {
246
- }
247
- }
14
+ import {
15
+ EXCLUDE_PATHS,
16
+ STABLE_SCAFFOLD_RELEASE,
17
+ TEMPLATE_REPOSITORY,
18
+ scaffold as scaffoldProject,
19
+ shouldInclude
20
+ } from "create-thally-docs/scaffold";
21
+ import { buildStarterDocsJson } from "create-thally-docs/starter";
22
+ var MCP_TEMPLATE_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
23
+ var MCP_SCAFFOLD_RELEASE_ID = STABLE_SCAFFOLD_RELEASE.id;
248
24
  async function scaffold(options) {
249
- const { projectDir, projectName, description, brandPreset, repoUrl, doInstall, enableAiChat = true, i18nLocales } = options;
250
- const targetDir = resolve(projectDir);
251
- if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
252
- throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
253
- }
254
- mkdirSync(targetDir, { recursive: true });
255
- const slug = slugify(projectName);
256
- await downloadTemplate(targetDir);
257
- writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
258
- updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
259
- patchApiReferenceGuard(targetDir);
260
- patchTopBarNavigation(targetDir);
261
- patchOpenApiFetch(targetDir);
262
- updateEnvExample(targetDir);
263
- if (doInstall) installDeps(targetDir);
264
- initGit(targetDir);
265
- return { projectDir: targetDir };
25
+ return scaffoldProject(options);
266
26
  }
267
27
 
268
28
  // src/tools/create-project.ts
@@ -274,7 +34,7 @@ var createProjectSchema = z.object({
274
34
  repoUrl: z.string().optional().describe("GitHub repository URL (optional)"),
275
35
  install: z.boolean().optional().default(true).describe("Whether to run npm install after scaffolding"),
276
36
  enableAiChat: z.boolean().optional().default(true).describe("Enable AI chat in docs.json (default true)"),
277
- i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Secondary locales to enable (e.g. [{code:"es",label:"Espa\xF1ol"}])')
37
+ i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Additional locales beyond the included English and Spanish defaults (e.g. [{code:"fr",label:"Fran\xE7ais"}])')
278
38
  });
279
39
  async function handleCreateProject(input) {
280
40
  const { projectDir, brandPreset = "primary", install = true } = input;
@@ -313,20 +73,20 @@ async function handleCreateProject(input) {
313
73
 
314
74
  // src/tools/add-page.ts
315
75
  import { z as z2 } from "zod";
316
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
317
- import { join as join3, dirname } from "path";
76
+ import { existsSync, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
77
+ import { join as join2, dirname } from "path";
318
78
 
319
79
  // src/lib/docs-json.ts
320
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
321
- import { join as join2 } from "path";
80
+ import { readFileSync, writeFileSync } from "fs";
81
+ import { join } from "path";
322
82
  function readDocsJson(projectDir) {
323
- const docsPath = join2(projectDir, "docs.json");
324
- const raw = readFileSync2(docsPath, "utf8");
83
+ const docsPath = join(projectDir, "docs.json");
84
+ const raw = readFileSync(docsPath, "utf8");
325
85
  return JSON.parse(raw);
326
86
  }
327
87
  function writeDocsJson(projectDir, config) {
328
- const docsPath = join2(projectDir, "docs.json");
329
- writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
88
+ const docsPath = join(projectDir, "docs.json");
89
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
330
90
  }
331
91
 
332
92
  // src/lib/page-echo.ts
@@ -378,11 +138,11 @@ async function handleAddPage(input) {
378
138
  `Invalid pageId "${pageId}". Use only alphanumeric characters, hyphens, and slashes. Do not include .mdx extension.`
379
139
  );
380
140
  }
381
- const mdxPath = join3(projectDir, "src", "content", `${pageId}.mdx`);
382
- if (existsSync2(mdxPath)) {
141
+ const mdxPath = join2(projectDir, "src", "content", `${pageId}.mdx`);
142
+ if (existsSync(mdxPath)) {
383
143
  throw new Error(`Page already exists at: ${mdxPath}`);
384
144
  }
385
- mkdirSync2(dirname(mdxPath), { recursive: true });
145
+ mkdirSync(dirname(mdxPath), { recursive: true });
386
146
  const frontmatterLines = [`title: ${title}`];
387
147
  if (description) {
388
148
  frontmatterLines.push(`description: ${description}`);
@@ -396,7 +156,7 @@ ${frontmatterLines.join("\n")}
396
156
 
397
157
  ${bodyContent}
398
158
  `;
399
- writeFileSync3(mdxPath, mdxContent, "utf8");
159
+ writeFileSync2(mdxPath, mdxContent, "utf8");
400
160
  const config = readDocsJson(projectDir);
401
161
  let targetTab = config.tabs[0];
402
162
  if (input.tab) {
@@ -509,8 +269,8 @@ async function handleListPages(input) {
509
269
 
510
270
  // src/tools/update-page.ts
511
271
  import { z as z5 } from "zod";
512
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
513
- import { join as join4 } from "path";
272
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
273
+ import { join as join3 } from "path";
514
274
 
515
275
  // src/lib/frontmatter.ts
516
276
  import matter from "gray-matter";
@@ -538,11 +298,11 @@ var updatePageSchema = z5.object({
538
298
  });
539
299
  function findPageFile(projectDir, pageId) {
540
300
  const candidates = [
541
- join4(projectDir, "src", "content", `${pageId}.mdx`),
542
- join4(projectDir, "src", "content", `${pageId}/index.mdx`)
301
+ join3(projectDir, "src", "content", `${pageId}.mdx`),
302
+ join3(projectDir, "src", "content", `${pageId}/index.mdx`)
543
303
  ];
544
304
  for (const candidate of candidates) {
545
- if (existsSync3(candidate)) return candidate;
305
+ if (existsSync2(candidate)) return candidate;
546
306
  }
547
307
  return null;
548
308
  }
@@ -556,7 +316,7 @@ async function handleUpdatePage(input) {
556
316
  src/content/${pageId}/index.mdx`
557
317
  );
558
318
  }
559
- const raw = readFileSync3(filePath, "utf8");
319
+ const raw = readFileSync2(filePath, "utf8");
560
320
  const parsed = parseFrontmatter(raw);
561
321
  const newFm = { ...parsed.data };
562
322
  if (input.title !== void 0) newFm["title"] = input.title;
@@ -566,7 +326,7 @@ async function handleUpdatePage(input) {
566
326
  }
567
327
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
568
328
  const newContent = stringifyFrontmatter(newBody.trim(), newFm);
569
- writeFileSync4(filePath, newContent, "utf8");
329
+ writeFileSync3(filePath, newContent, "utf8");
570
330
  return [
571
331
  `\u2705 Page updated: ${filePath}`,
572
332
  ` pageId: ${pageId}`,
@@ -622,8 +382,8 @@ async function handleImportDocs(input) {
622
382
 
623
383
  // src/tools/search-docs.ts
624
384
  import { z as z7 } from "zod";
625
- import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
626
- import { join as join5, relative, extname } from "path";
385
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
386
+ import { join as join4, relative, extname } from "path";
627
387
  var searchDocsSchema = z7.object({
628
388
  projectDir: z7.string().describe("Path to the Thally project root"),
629
389
  query: z7.string().describe("Search query"),
@@ -632,12 +392,12 @@ var searchDocsSchema = z7.object({
632
392
  function scanMdxFiles(dir, results) {
633
393
  let entries;
634
394
  try {
635
- entries = readdirSync2(dir);
395
+ entries = readdirSync(dir);
636
396
  } catch {
637
397
  return;
638
398
  }
639
399
  for (const entry of entries) {
640
- const fullPath = join5(dir, entry);
400
+ const fullPath = join4(dir, entry);
641
401
  try {
642
402
  const stat = statSync(fullPath);
643
403
  if (stat.isDirectory()) {
@@ -655,7 +415,7 @@ function scoreFiles(files, contentDir, query) {
655
415
  for (const filePath of files) {
656
416
  let raw;
657
417
  try {
658
- raw = readFileSync4(filePath, "utf8");
418
+ raw = readFileSync3(filePath, "utf8");
659
419
  } catch {
660
420
  continue;
661
421
  }
@@ -680,8 +440,8 @@ function scoreFiles(files, contentDir, query) {
680
440
  }
681
441
  async function handleSearchDocs(input) {
682
442
  const { projectDir, query, limit = 5 } = input;
683
- const contentDir = join5(projectDir, "src", "content");
684
- if (!existsSync4(contentDir)) {
443
+ const contentDir = join4(projectDir, "src", "content");
444
+ if (!existsSync3(contentDir)) {
685
445
  throw new Error(`Content directory not found: ${contentDir}`);
686
446
  }
687
447
  const files = [];
@@ -776,22 +536,22 @@ async function handleAgentReadiness(input) {
776
536
 
777
537
  // src/tools/read-page.ts
778
538
  import { z as z10 } from "zod";
779
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
780
- import { join as join6 } from "path";
539
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
540
+ import { join as join5 } from "path";
781
541
  var readPageSchema = z10.object({
782
542
  projectDir: z10.string().describe("Path to the Thally project root"),
783
543
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
784
544
  });
785
545
  async function handleReadPage(input) {
786
546
  const { projectDir, pageId } = input;
787
- const contentDir = join6(projectDir, "src", "content");
547
+ const contentDir = join5(projectDir, "src", "content");
788
548
  const candidates = [
789
- join6(contentDir, `${pageId}.mdx`),
790
- join6(contentDir, `${pageId}/index.mdx`)
549
+ join5(contentDir, `${pageId}.mdx`),
550
+ join5(contentDir, `${pageId}/index.mdx`)
791
551
  ];
792
552
  let filePath = null;
793
553
  for (const c of candidates) {
794
- if (existsSync5(c)) {
554
+ if (existsSync4(c)) {
795
555
  filePath = c;
796
556
  break;
797
557
  }
@@ -799,7 +559,7 @@ async function handleReadPage(input) {
799
559
  if (!filePath) {
800
560
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
801
561
  }
802
- const raw = readFileSync5(filePath, "utf8");
562
+ const raw = readFileSync4(filePath, "utf8");
803
563
  const { data, content } = parseFrontmatter(raw);
804
564
  const title = data.title ?? pageId;
805
565
  const description = data.description ?? "";
@@ -811,8 +571,8 @@ async function handleReadPage(input) {
811
571
 
812
572
  // src/tools/get-context.ts
813
573
  import { z as z11 } from "zod";
814
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
815
- import { join as join7 } from "path";
574
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
575
+ import { join as join6 } from "path";
816
576
  var getContextSchema = z11.object({
817
577
  projectDir: z11.string().describe("Path to the Thally project root"),
818
578
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -820,8 +580,8 @@ var getContextSchema = z11.object({
820
580
  });
821
581
  async function handleGetContext(input) {
822
582
  const { projectDir, topic, maxTokens = 4e3 } = input;
823
- const contentDir = join7(projectDir, "src", "content");
824
- if (!existsSync6(contentDir)) {
583
+ const contentDir = join6(projectDir, "src", "content");
584
+ if (!existsSync5(contentDir)) {
825
585
  throw new Error(`Content directory not found: ${contentDir}`);
826
586
  }
827
587
  const files = [];
@@ -835,13 +595,13 @@ async function handleGetContext(input) {
835
595
  const sections = [];
836
596
  for (const result of scored) {
837
597
  const candidates = [
838
- join7(contentDir, `${result.pageId}.mdx`),
839
- join7(contentDir, `${result.pageId}/index.mdx`)
598
+ join6(contentDir, `${result.pageId}.mdx`),
599
+ join6(contentDir, `${result.pageId}/index.mdx`)
840
600
  ];
841
601
  let content = "";
842
602
  for (const c of candidates) {
843
- if (existsSync6(c)) {
844
- const raw = readFileSync6(c, "utf8");
603
+ if (existsSync5(c)) {
604
+ const raw = readFileSync5(c, "utf8");
845
605
  const { content: body } = parseFrontmatter(raw);
846
606
  content = body.trim();
847
607
  break;
@@ -866,8 +626,8 @@ async function handleGetContext(input) {
866
626
 
867
627
  // src/tools/lint-project.ts
868
628
  import { z as z12 } from "zod";
869
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
870
- import { join as join8 } from "path";
629
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
630
+ import { join as join7 } from "path";
871
631
  var lintProjectSchema = z12.object({
872
632
  projectDir: z12.string().describe("Path to the Thally project root"),
873
633
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -898,9 +658,9 @@ function addOrphanToNav(projectDir, pageId) {
898
658
  }
899
659
  async function handleLintProject(input) {
900
660
  const { projectDir, fix = false } = input;
901
- const contentDir = join8(projectDir, "src", "content");
661
+ const contentDir = join7(projectDir, "src", "content");
902
662
  const issues = [];
903
- if (!existsSync7(join8(projectDir, "docs.json"))) {
663
+ if (!existsSync6(join7(projectDir, "docs.json"))) {
904
664
  throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
905
665
  }
906
666
  const config = readDocsJson(projectDir);
@@ -919,10 +679,10 @@ async function handleLintProject(input) {
919
679
  }
920
680
  for (const pageId of navPageIds) {
921
681
  const candidates = [
922
- join8(contentDir, `${pageId}.mdx`),
923
- join8(contentDir, `${pageId}/index.mdx`)
682
+ join7(contentDir, `${pageId}.mdx`),
683
+ join7(contentDir, `${pageId}/index.mdx`)
924
684
  ];
925
- if (!candidates.some((c) => existsSync7(c))) {
685
+ if (!candidates.some((c) => existsSync6(c))) {
926
686
  issues.push({
927
687
  severity: "error",
928
688
  message: `"${pageId}" is in docs.json but has no MDX file`,
@@ -931,7 +691,7 @@ async function handleLintProject(input) {
931
691
  }
932
692
  }
933
693
  const allFiles = [];
934
- if (existsSync7(contentDir)) {
694
+ if (existsSync6(contentDir)) {
935
695
  scanMdxFiles(contentDir, allFiles);
936
696
  }
937
697
  const fixedOrphans = [];
@@ -949,7 +709,7 @@ async function handleLintProject(input) {
949
709
  let data = {};
950
710
  let content = "";
951
711
  try {
952
- const raw = readFileSync7(filePath, "utf8");
712
+ const raw = readFileSync6(filePath, "utf8");
953
713
  const parsed = parseFrontmatter(raw);
954
714
  data = parsed.data;
955
715
  content = parsed.content;
@@ -1006,8 +766,8 @@ async function handleLintProject(input) {
1006
766
 
1007
767
  // src/tools/translate-docs.ts
1008
768
  import { z as z13 } from "zod";
1009
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
1010
- import { join as join9, dirname as dirname2 } from "path";
769
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
770
+ import { join as join8, dirname as dirname2 } from "path";
1011
771
  import Anthropic from "@anthropic-ai/sdk";
1012
772
  import pLimit from "p-limit";
1013
773
  var translateDocsSchema = z13.object({
@@ -1019,8 +779,8 @@ var translateDocsSchema = z13.object({
1019
779
  model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
1020
780
  });
1021
781
  function readDocsJson2(projectDir) {
1022
- const docsPath = join9(projectDir, "docs.json");
1023
- const raw = readFileSync8(docsPath, "utf8");
782
+ const docsPath = join8(projectDir, "docs.json");
783
+ const raw = readFileSync7(docsPath, "utf8");
1024
784
  return JSON.parse(raw);
1025
785
  }
1026
786
  function collectPageIds(pages) {
@@ -1062,12 +822,12 @@ function getAllPageIds(config) {
1062
822
  return { ids, hrefOnlyPages };
1063
823
  }
1064
824
  function findSourceFile(projectDir, pageId) {
1065
- const contentRoot = join9(projectDir, "src", "content");
825
+ const contentRoot = join8(projectDir, "src", "content");
1066
826
  const candidates = [
1067
- join9(contentRoot, `${pageId}.mdx`),
1068
- join9(contentRoot, `${pageId}/index.mdx`)
827
+ join8(contentRoot, `${pageId}.mdx`),
828
+ join8(contentRoot, `${pageId}/index.mdx`)
1069
829
  ];
1070
- return candidates.find((p) => existsSync8(p)) ?? null;
830
+ return candidates.find((p) => existsSync7(p)) ?? null;
1071
831
  }
1072
832
  var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
1073
833
 
@@ -1119,7 +879,7 @@ async function handleTranslateDocs(input) {
1119
879
  }
1120
880
  const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
1121
881
  const targetPageIds = pages ?? allPageIds;
1122
- const contentRoot = join9(projectDir, "src", "content");
882
+ const contentRoot = join8(projectDir, "src", "content");
1123
883
  const toTranslate = [];
1124
884
  const skipped = [];
1125
885
  for (const pageId of targetPageIds) {
@@ -1129,8 +889,8 @@ async function handleTranslateDocs(input) {
1129
889
  continue;
1130
890
  }
1131
891
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
1132
- const targetFile = join9(contentRoot, locale, relativeFromContent);
1133
- if (existsSync8(targetFile) && !force) {
892
+ const targetFile = join8(contentRoot, locale, relativeFromContent);
893
+ if (existsSync7(targetFile) && !force) {
1134
894
  skipped.push(`${pageId} (already translated)`);
1135
895
  continue;
1136
896
  }
@@ -1146,14 +906,14 @@ async function handleTranslateDocs(input) {
1146
906
  toTranslate.map(
1147
907
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1148
908
  try {
1149
- const sourceContent = readFileSync8(sourceFile, "utf8");
909
+ const sourceContent = readFileSync7(sourceFile, "utf8");
1150
910
  const parsed = parseFrontmatter(sourceContent);
1151
911
  if (!parsed.data.title) {
1152
912
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1153
913
  }
1154
914
  const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
1155
- mkdirSync3(dirname2(targetFile), { recursive: true });
1156
- writeFileSync6(targetFile, translated + "\n", "utf8");
915
+ mkdirSync2(dirname2(targetFile), { recursive: true });
916
+ writeFileSync5(targetFile, translated + "\n", "utf8");
1157
917
  results.push({ pageId, success: true });
1158
918
  } catch (err) {
1159
919
  const msg = err instanceof Error ? err.message : String(err);