@thallylabs/mcp 0.8.0 → 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,9 +269,25 @@ 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";
274
+
275
+ // src/lib/frontmatter.ts
514
276
  import matter from "gray-matter";
277
+ var FRONTMATTER_OPTIONS = {
278
+ engines: {
279
+ javascript: () => ({}),
280
+ js: () => ({})
281
+ }
282
+ };
283
+ function parseFrontmatter(raw) {
284
+ return matter(raw, FRONTMATTER_OPTIONS);
285
+ }
286
+ function stringifyFrontmatter(body, data) {
287
+ return matter.stringify(body, data);
288
+ }
289
+
290
+ // src/tools/update-page.ts
515
291
  var updatePageSchema = z5.object({
516
292
  projectDir: z5.string().describe("Path to the Thally project root"),
517
293
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -522,11 +298,11 @@ var updatePageSchema = z5.object({
522
298
  });
523
299
  function findPageFile(projectDir, pageId) {
524
300
  const candidates = [
525
- join4(projectDir, "src", "content", `${pageId}.mdx`),
526
- join4(projectDir, "src", "content", `${pageId}/index.mdx`)
301
+ join3(projectDir, "src", "content", `${pageId}.mdx`),
302
+ join3(projectDir, "src", "content", `${pageId}/index.mdx`)
527
303
  ];
528
304
  for (const candidate of candidates) {
529
- if (existsSync3(candidate)) return candidate;
305
+ if (existsSync2(candidate)) return candidate;
530
306
  }
531
307
  return null;
532
308
  }
@@ -540,8 +316,8 @@ async function handleUpdatePage(input) {
540
316
  src/content/${pageId}/index.mdx`
541
317
  );
542
318
  }
543
- const raw = readFileSync3(filePath, "utf8");
544
- const parsed = matter(raw);
319
+ const raw = readFileSync2(filePath, "utf8");
320
+ const parsed = parseFrontmatter(raw);
545
321
  const newFm = { ...parsed.data };
546
322
  if (input.title !== void 0) newFm["title"] = input.title;
547
323
  if (input.description !== void 0) newFm["description"] = input.description;
@@ -549,8 +325,8 @@ async function handleUpdatePage(input) {
549
325
  Object.assign(newFm, input.mergeFrontmatter);
550
326
  }
551
327
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
552
- const newContent = matter.stringify(newBody.trim(), newFm);
553
- writeFileSync4(filePath, newContent, "utf8");
328
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
329
+ writeFileSync3(filePath, newContent, "utf8");
554
330
  return [
555
331
  `\u2705 Page updated: ${filePath}`,
556
332
  ` pageId: ${pageId}`,
@@ -563,22 +339,30 @@ async function handleUpdatePage(input) {
563
339
  // src/tools/migrate-docs.ts
564
340
  import { z as z6 } from "zod";
565
341
  import { migrateDocs } from "create-thally-docs/migrate";
566
- var migrateDocsSchema = z6.object({
342
+ var migrationSourceShape = {
567
343
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
568
- projectDir: z6.string().describe("Path for new project or existing project dir"),
569
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
570
344
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
571
345
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
572
346
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
573
347
  maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
574
348
  platform: z6.enum(["mintlify", "docusaurus"]).optional().describe("Source platform (default: auto-detect)")
349
+ };
350
+ var migrateDocsSchema = z6.object({
351
+ ...migrationSourceShape,
352
+ projectDir: z6.string().describe("Path for the new canonical Thally project; the directory must be absent or empty")
575
353
  });
576
- async function handleMigrateDocs(input) {
354
+ var importDocsSchema = z6.object({
355
+ ...migrationSourceShape,
356
+ projectDir: z6.string().describe("Path to an existing Thally project whose runtime should be preserved")
357
+ });
358
+ async function runMigration(input, isInPlaceImport) {
577
359
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
578
- const result = await migrateDocs({
360
+ return migrateDocs({
579
361
  sourceUrl: input.sourceUrl,
580
362
  projectDir: input.projectDir,
581
- into: input.into ?? false,
363
+ // Keep this decision inside the adapter so stale or adversarial callers
364
+ // cannot turn a template-first migration into an in-place mutation.
365
+ into: isInPlaceImport,
582
366
  apiKey,
583
367
  branch: input.branch,
584
368
  docsDir: input.docsDir,
@@ -586,14 +370,20 @@ async function handleMigrateDocs(input) {
586
370
  platform: input.platform,
587
371
  yes: true
588
372
  });
589
- return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
373
+ }
374
+ async function handleMigrateDocs(input) {
375
+ const result = await runMigration(input, false);
376
+ return `Migration complete! Created a fresh Thally template at ${result.projectDir} and imported ${result.pagesWritten} pages.`;
377
+ }
378
+ async function handleImportDocs(input) {
379
+ const result = await runMigration(input, true);
380
+ return `Import complete! Imported ${result.pagesWritten} pages into the existing Thally project at ${result.projectDir}.`;
590
381
  }
591
382
 
592
383
  // src/tools/search-docs.ts
593
384
  import { z as z7 } from "zod";
594
- import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
595
- import { join as join5, relative, extname } from "path";
596
- import matter2 from "gray-matter";
385
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
386
+ import { join as join4, relative, extname } from "path";
597
387
  var searchDocsSchema = z7.object({
598
388
  projectDir: z7.string().describe("Path to the Thally project root"),
599
389
  query: z7.string().describe("Search query"),
@@ -602,12 +392,12 @@ var searchDocsSchema = z7.object({
602
392
  function scanMdxFiles(dir, results) {
603
393
  let entries;
604
394
  try {
605
- entries = readdirSync2(dir);
395
+ entries = readdirSync(dir);
606
396
  } catch {
607
397
  return;
608
398
  }
609
399
  for (const entry of entries) {
610
- const fullPath = join5(dir, entry);
400
+ const fullPath = join4(dir, entry);
611
401
  try {
612
402
  const stat = statSync(fullPath);
613
403
  if (stat.isDirectory()) {
@@ -625,11 +415,11 @@ function scoreFiles(files, contentDir, query) {
625
415
  for (const filePath of files) {
626
416
  let raw;
627
417
  try {
628
- raw = readFileSync4(filePath, "utf8");
418
+ raw = readFileSync3(filePath, "utf8");
629
419
  } catch {
630
420
  continue;
631
421
  }
632
- const { data, content } = matter2(raw);
422
+ const { data, content } = parseFrontmatter(raw);
633
423
  const title = data.title ?? "";
634
424
  const description = data.description ?? "";
635
425
  const keywords = data.keywords ?? [];
@@ -650,8 +440,8 @@ function scoreFiles(files, contentDir, query) {
650
440
  }
651
441
  async function handleSearchDocs(input) {
652
442
  const { projectDir, query, limit = 5 } = input;
653
- const contentDir = join5(projectDir, "src", "content");
654
- if (!existsSync4(contentDir)) {
443
+ const contentDir = join4(projectDir, "src", "content");
444
+ if (!existsSync3(contentDir)) {
655
445
  throw new Error(`Content directory not found: ${contentDir}`);
656
446
  }
657
447
  const files = [];
@@ -746,23 +536,22 @@ async function handleAgentReadiness(input) {
746
536
 
747
537
  // src/tools/read-page.ts
748
538
  import { z as z10 } from "zod";
749
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
750
- import { join as join6 } from "path";
751
- import matter3 from "gray-matter";
539
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
540
+ import { join as join5 } from "path";
752
541
  var readPageSchema = z10.object({
753
542
  projectDir: z10.string().describe("Path to the Thally project root"),
754
543
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
755
544
  });
756
545
  async function handleReadPage(input) {
757
546
  const { projectDir, pageId } = input;
758
- const contentDir = join6(projectDir, "src", "content");
547
+ const contentDir = join5(projectDir, "src", "content");
759
548
  const candidates = [
760
- join6(contentDir, `${pageId}.mdx`),
761
- join6(contentDir, `${pageId}/index.mdx`)
549
+ join5(contentDir, `${pageId}.mdx`),
550
+ join5(contentDir, `${pageId}/index.mdx`)
762
551
  ];
763
552
  let filePath = null;
764
553
  for (const c of candidates) {
765
- if (existsSync5(c)) {
554
+ if (existsSync4(c)) {
766
555
  filePath = c;
767
556
  break;
768
557
  }
@@ -770,8 +559,8 @@ async function handleReadPage(input) {
770
559
  if (!filePath) {
771
560
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
772
561
  }
773
- const raw = readFileSync5(filePath, "utf8");
774
- const { data, content } = matter3(raw);
562
+ const raw = readFileSync4(filePath, "utf8");
563
+ const { data, content } = parseFrontmatter(raw);
775
564
  const title = data.title ?? pageId;
776
565
  const description = data.description ?? "";
777
566
  const lines = [`id: ${pageId}`, `title: ${title}`];
@@ -782,9 +571,8 @@ async function handleReadPage(input) {
782
571
 
783
572
  // src/tools/get-context.ts
784
573
  import { z as z11 } from "zod";
785
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
786
- import { join as join7 } from "path";
787
- import matter4 from "gray-matter";
574
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
575
+ import { join as join6 } from "path";
788
576
  var getContextSchema = z11.object({
789
577
  projectDir: z11.string().describe("Path to the Thally project root"),
790
578
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -792,8 +580,8 @@ var getContextSchema = z11.object({
792
580
  });
793
581
  async function handleGetContext(input) {
794
582
  const { projectDir, topic, maxTokens = 4e3 } = input;
795
- const contentDir = join7(projectDir, "src", "content");
796
- if (!existsSync6(contentDir)) {
583
+ const contentDir = join6(projectDir, "src", "content");
584
+ if (!existsSync5(contentDir)) {
797
585
  throw new Error(`Content directory not found: ${contentDir}`);
798
586
  }
799
587
  const files = [];
@@ -807,14 +595,14 @@ async function handleGetContext(input) {
807
595
  const sections = [];
808
596
  for (const result of scored) {
809
597
  const candidates = [
810
- join7(contentDir, `${result.pageId}.mdx`),
811
- join7(contentDir, `${result.pageId}/index.mdx`)
598
+ join6(contentDir, `${result.pageId}.mdx`),
599
+ join6(contentDir, `${result.pageId}/index.mdx`)
812
600
  ];
813
601
  let content = "";
814
602
  for (const c of candidates) {
815
- if (existsSync6(c)) {
816
- const raw = readFileSync6(c, "utf8");
817
- const { content: body } = matter4(raw);
603
+ if (existsSync5(c)) {
604
+ const raw = readFileSync5(c, "utf8");
605
+ const { content: body } = parseFrontmatter(raw);
818
606
  content = body.trim();
819
607
  break;
820
608
  }
@@ -838,9 +626,8 @@ async function handleGetContext(input) {
838
626
 
839
627
  // src/tools/lint-project.ts
840
628
  import { z as z12 } from "zod";
841
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
842
- import { join as join8 } from "path";
843
- import matter5 from "gray-matter";
629
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
630
+ import { join as join7 } from "path";
844
631
  var lintProjectSchema = z12.object({
845
632
  projectDir: z12.string().describe("Path to the Thally project root"),
846
633
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -871,9 +658,9 @@ function addOrphanToNav(projectDir, pageId) {
871
658
  }
872
659
  async function handleLintProject(input) {
873
660
  const { projectDir, fix = false } = input;
874
- const contentDir = join8(projectDir, "src", "content");
661
+ const contentDir = join7(projectDir, "src", "content");
875
662
  const issues = [];
876
- if (!existsSync7(join8(projectDir, "docs.json"))) {
663
+ if (!existsSync6(join7(projectDir, "docs.json"))) {
877
664
  throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
878
665
  }
879
666
  const config = readDocsJson(projectDir);
@@ -892,10 +679,10 @@ async function handleLintProject(input) {
892
679
  }
893
680
  for (const pageId of navPageIds) {
894
681
  const candidates = [
895
- join8(contentDir, `${pageId}.mdx`),
896
- join8(contentDir, `${pageId}/index.mdx`)
682
+ join7(contentDir, `${pageId}.mdx`),
683
+ join7(contentDir, `${pageId}/index.mdx`)
897
684
  ];
898
- if (!candidates.some((c) => existsSync7(c))) {
685
+ if (!candidates.some((c) => existsSync6(c))) {
899
686
  issues.push({
900
687
  severity: "error",
901
688
  message: `"${pageId}" is in docs.json but has no MDX file`,
@@ -904,7 +691,7 @@ async function handleLintProject(input) {
904
691
  }
905
692
  }
906
693
  const allFiles = [];
907
- if (existsSync7(contentDir)) {
694
+ if (existsSync6(contentDir)) {
908
695
  scanMdxFiles(contentDir, allFiles);
909
696
  }
910
697
  const fixedOrphans = [];
@@ -922,8 +709,8 @@ async function handleLintProject(input) {
922
709
  let data = {};
923
710
  let content = "";
924
711
  try {
925
- const raw = readFileSync7(filePath, "utf8");
926
- const parsed = matter5(raw);
712
+ const raw = readFileSync6(filePath, "utf8");
713
+ const parsed = parseFrontmatter(raw);
927
714
  data = parsed.data;
928
715
  content = parsed.content;
929
716
  } catch {
@@ -979,9 +766,8 @@ async function handleLintProject(input) {
979
766
 
980
767
  // src/tools/translate-docs.ts
981
768
  import { z as z13 } from "zod";
982
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
983
- import { join as join9, dirname as dirname2 } from "path";
984
- import matter6 from "gray-matter";
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";
985
771
  import Anthropic from "@anthropic-ai/sdk";
986
772
  import pLimit from "p-limit";
987
773
  var translateDocsSchema = z13.object({
@@ -993,8 +779,8 @@ var translateDocsSchema = z13.object({
993
779
  model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
994
780
  });
995
781
  function readDocsJson2(projectDir) {
996
- const docsPath = join9(projectDir, "docs.json");
997
- const raw = readFileSync8(docsPath, "utf8");
782
+ const docsPath = join8(projectDir, "docs.json");
783
+ const raw = readFileSync7(docsPath, "utf8");
998
784
  return JSON.parse(raw);
999
785
  }
1000
786
  function collectPageIds(pages) {
@@ -1036,12 +822,12 @@ function getAllPageIds(config) {
1036
822
  return { ids, hrefOnlyPages };
1037
823
  }
1038
824
  function findSourceFile(projectDir, pageId) {
1039
- const contentRoot = join9(projectDir, "src", "content");
825
+ const contentRoot = join8(projectDir, "src", "content");
1040
826
  const candidates = [
1041
- join9(contentRoot, `${pageId}.mdx`),
1042
- join9(contentRoot, `${pageId}/index.mdx`)
827
+ join8(contentRoot, `${pageId}.mdx`),
828
+ join8(contentRoot, `${pageId}/index.mdx`)
1043
829
  ];
1044
- return candidates.find((p) => existsSync8(p)) ?? null;
830
+ return candidates.find((p) => existsSync7(p)) ?? null;
1045
831
  }
1046
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.
1047
833
 
@@ -1093,7 +879,7 @@ async function handleTranslateDocs(input) {
1093
879
  }
1094
880
  const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
1095
881
  const targetPageIds = pages ?? allPageIds;
1096
- const contentRoot = join9(projectDir, "src", "content");
882
+ const contentRoot = join8(projectDir, "src", "content");
1097
883
  const toTranslate = [];
1098
884
  const skipped = [];
1099
885
  for (const pageId of targetPageIds) {
@@ -1103,8 +889,8 @@ async function handleTranslateDocs(input) {
1103
889
  continue;
1104
890
  }
1105
891
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
1106
- const targetFile = join9(contentRoot, locale, relativeFromContent);
1107
- if (existsSync8(targetFile) && !force) {
892
+ const targetFile = join8(contentRoot, locale, relativeFromContent);
893
+ if (existsSync7(targetFile) && !force) {
1108
894
  skipped.push(`${pageId} (already translated)`);
1109
895
  continue;
1110
896
  }
@@ -1120,14 +906,14 @@ async function handleTranslateDocs(input) {
1120
906
  toTranslate.map(
1121
907
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1122
908
  try {
1123
- const sourceContent = readFileSync8(sourceFile, "utf8");
1124
- const parsed = matter6(sourceContent);
909
+ const sourceContent = readFileSync7(sourceFile, "utf8");
910
+ const parsed = parseFrontmatter(sourceContent);
1125
911
  if (!parsed.data.title) {
1126
912
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1127
913
  }
1128
914
  const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
1129
- mkdirSync3(dirname2(targetFile), { recursive: true });
1130
- writeFileSync6(targetFile, translated + "\n", "utf8");
915
+ mkdirSync2(dirname2(targetFile), { recursive: true });
916
+ writeFileSync5(targetFile, translated + "\n", "utf8");
1131
917
  results.push({ pageId, success: true });
1132
918
  } catch (err) {
1133
919
  const msg = err instanceof Error ? err.message : String(err);
@@ -1476,11 +1262,18 @@ var tools = [
1476
1262
  }),
1477
1263
  defineTool({
1478
1264
  name: "migrate_docs",
1479
- description: "Crawl a docs site and migrate it into a Thally project",
1265
+ description: "Create a fresh canonical Thally template, then migrate a GitHub repository or public docs site into it; the target must be new or empty",
1480
1266
  scope: "project",
1481
1267
  schema: migrateDocsSchema,
1482
1268
  handler: handleMigrateDocs
1483
1269
  }),
1270
+ defineTool({
1271
+ name: "import_docs",
1272
+ description: "Import content into an existing Thally project without scaffolding; use only when an in-place import is explicitly requested",
1273
+ scope: "project",
1274
+ schema: importDocsSchema,
1275
+ handler: handleImportDocs
1276
+ }),
1484
1277
  defineTool({
1485
1278
  name: "search_docs",
1486
1279
  description: "Search documentation pages by keyword \u2014 returns ranked list of matching pages",