@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/tools.js CHANGED
@@ -2,258 +2,18 @@
2
2
  import { z } from "zod";
3
3
 
4
4
  // src/lib/scaffold.ts
5
- import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, cpSync } from "fs";
6
- import { resolve, join } from "path";
7
- import { execSync } from "child_process";
8
- import { Readable, pipeline } from "stream";
9
- import { promisify } from "util";
10
- import tar from "tar";
11
- var pipelineAsync = promisify(pipeline);
12
- var MCP_TEMPLATE_REPOSITORY = "thallylabs/docs";
13
- var TARBALL_URL = `https://codeload.github.com/${MCP_TEMPLATE_REPOSITORY}/tar.gz/main`;
14
- var MCP_EXCLUDE_PATHS = [
15
- "/cli/",
16
- "/packages/",
17
- "/node_modules/",
18
- "/.git/",
19
- "/thally-track.yml",
20
- "/CODEOWNERS",
21
- "/CLAUDE.md",
22
- "/notes/"
23
- ];
24
- function shouldIncludeMcpTemplatePath(path) {
25
- return !MCP_EXCLUDE_PATHS.some((excluded) => path.includes(excluded));
26
- }
27
- var STARTER_PAGES = {
28
- "introduction.mdx": `---
29
- title: Introduction
30
- description: Welcome to {NAME} documentation.
31
- ---
32
-
33
- ## Welcome
34
-
35
- This is the home page of your **{NAME}** documentation site, powered by [Thally](https://github.com/thallylabs/thally).
36
-
37
- Get started by editing this file at \`src/content/introduction.mdx\`.
38
- `,
39
- "quickstart.mdx": `---
40
- title: Quickstart
41
- description: Get up and running with {NAME} in under 5 minutes.
42
- ---
43
-
44
- ## Installation
45
-
46
- \`\`\`bash
47
- npm install {SLUG}
48
- \`\`\`
49
-
50
- ## Basic usage
51
-
52
- \`\`\`ts
53
- import { create } from '{SLUG}'
54
-
55
- const client = create({ apiKey: 'your-api-key' })
56
- \`\`\`
57
-
58
- That's it \u2014 you're ready to go!
59
- `
60
- };
61
- function buildStarterDocsJson({
62
- enableAiChat,
63
- repoUrl,
64
- i18nLocales
65
- }) {
66
- const config = {};
67
- if (enableAiChat) {
68
- config.ai = { chat: true };
69
- }
70
- if (repoUrl) {
71
- config.navbar = {
72
- links: [{ label: "GitHub", href: repoUrl, type: "github" }],
73
- primary: { label: "Get started", href: "/quickstart" }
74
- };
75
- }
76
- if (i18nLocales && i18nLocales.length > 0) {
77
- config.i18n = {
78
- defaultLocale: "en",
79
- locales: [{ code: "en", label: "English" }, ...i18nLocales]
80
- };
81
- }
82
- config.tabs = [
83
- {
84
- tab: "Overview",
85
- groups: [{ group: "Getting Started", pages: ["introduction", "quickstart"] }]
86
- },
87
- { tab: "API Reference", api: { source: "openapi.yaml" } },
88
- { tab: "Changelog", href: "/changelog" }
89
- ];
90
- return JSON.stringify(config, null, 2) + "\n";
91
- }
92
- function slugify(name) {
93
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
94
- }
95
- function run(cmd, cwd) {
96
- execSync(cmd, { cwd, stdio: "inherit" });
97
- }
98
- async function downloadTemplate(targetDir) {
99
- const response = await fetch(TARBALL_URL);
100
- if (!response.ok) {
101
- throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
102
- }
103
- if (!response.body) {
104
- throw new Error("Response body is empty");
105
- }
106
- const nodeStream = Readable.fromWeb(response.body);
107
- await pipelineAsync(
108
- nodeStream,
109
- tar.extract({
110
- cwd: targetDir,
111
- strip: 1,
112
- filter: shouldIncludeMcpTemplatePath
113
- })
114
- );
115
- }
116
- function writeStarterContent(targetDir, projectName, slug, enableAiChat = true, repoUrl = "", i18nLocales) {
117
- const contentDir = join(targetDir, "src", "content");
118
- if (existsSync(contentDir)) {
119
- const entries = readdirSync(contentDir);
120
- for (const entry of entries) {
121
- execSync(`rm -rf "${join(contentDir, entry)}"`);
122
- }
123
- } else {
124
- mkdirSync(contentDir, { recursive: true });
125
- }
126
- for (const [filename, template] of Object.entries(STARTER_PAGES)) {
127
- const content = template.replace(/\{NAME\}/g, projectName).replace(/\{SLUG\}/g, slug);
128
- writeFileSync(join(contentDir, filename), content, "utf8");
129
- }
130
- writeFileSync(
131
- join(targetDir, "docs.json"),
132
- buildStarterDocsJson({ enableAiChat, repoUrl: repoUrl || void 0, i18nLocales }),
133
- "utf8"
134
- );
135
- }
136
- function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
137
- const siteFile = join(targetDir, "src", "data", "site.ts");
138
- if (!existsSync(siteFile)) return;
139
- let source = readFileSync(siteFile, "utf8");
140
- source = source.replace(/name:\s*'[^']*'/, `name: '${projectName.replace(/'/g, "\\'")}'`);
141
- source = source.replace(
142
- /description:\s*\n\s*'[^']*'/,
143
- `description:
144
- '${description.replace(/'/g, "\\'")}'`
145
- );
146
- source = source.replace(
147
- /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
148
- `const brandPreset: BrandPresetKey = '${brandPreset}'`
149
- );
150
- if (repoUrl) {
151
- source = source.replace(/repoUrl:\s*'[^']*'/, `repoUrl: '${repoUrl}'`);
152
- source = source.replace(
153
- /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
154
- `{ label: 'GitHub', href: '${repoUrl}' }`
155
- );
156
- source = source.replace(
157
- /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
158
- `{ label: 'Support', href: '${repoUrl}/issues/new' }`
159
- );
160
- }
161
- writeFileSync(siteFile, source, "utf8");
162
- }
163
- function patchTopBarNavigation(targetDir) {
164
- const filePath = join(targetDir, "src", "components", "layout", "top-bar.tsx");
165
- if (!existsSync(filePath)) return;
166
- const source = readFileSync(filePath, "utf8");
167
- if (!source.includes("target={isExternal ? '_blank' : undefined}")) return;
168
- const patched = source.replace(
169
- /if \(collection\.href\) \{\n const isExternal[^\n]+\n return \(\n <a[\s\S]*?<\/a>\n \)\n \}/,
170
- `if (collection.href) {
171
- const isExternal = /^https?:\\/\\//.test(collection.href)
172
- if (isExternal) {
173
- return (
174
- <a
175
- key={collection.id}
176
- href={collection.href}
177
- target="_blank"
178
- rel="noreferrer"
179
- className={baseClasses}
180
- >
181
- {collection.label}
182
- </a>
183
- )
184
- }
185
- return (
186
- <Link
187
- key={collection.id}
188
- href={collection.href}
189
- className={baseClasses}
190
- >
191
- {collection.label}
192
- </Link>
193
- )
194
- }`
195
- );
196
- writeFileSync(filePath, patched, "utf8");
197
- }
198
- function patchApiReferenceGuard(targetDir) {
199
- const filePath = join(targetDir, "src", "data", "api-reference.ts");
200
- if (!existsSync(filePath)) return;
201
- let source = readFileSync(filePath, "utf8");
202
- source = source.replace(
203
- /export async function buildApiNavigation\([^)]*\)[^{]*\{\n/,
204
- (match) => `${match} if (apiReferenceConfig.specs.length === 0) return []
205
- `
206
- );
207
- writeFileSync(filePath, source, "utf8");
208
- }
209
- function patchOpenApiFetch(targetDir) {
210
- const filePath = join(targetDir, "src", "lib", "openapi", "fetch.ts");
211
- if (!existsSync(filePath)) return;
212
- let source = readFileSync(filePath, "utf8");
213
- source = source.replace(
214
- /const absolutePath = path\.isAbsolute\(filePath\) \? filePath : path\.resolve\(process\.cwd\(\), filePath\)/,
215
- `const absolutePath = filePath.startsWith('/')
216
- ? path.resolve(process.cwd(), 'public', filePath.slice(1))
217
- : path.resolve(process.cwd(), filePath)`
218
- );
219
- writeFileSync(filePath, source, "utf8");
220
- }
221
- function updateEnvExample(targetDir) {
222
- const envFile = join(targetDir, ".env.example");
223
- if (existsSync(envFile)) {
224
- const envLocal = join(targetDir, ".env.local");
225
- if (!existsSync(envLocal)) cpSync(envFile, envLocal);
226
- }
227
- }
228
- function installDeps(targetDir) {
229
- run("npm install", targetDir);
230
- }
231
- function initGit(targetDir) {
232
- try {
233
- run("git init", targetDir);
234
- run("git add -A", targetDir);
235
- run('git commit -m "Initial commit from create-thally-docs"', targetDir);
236
- } catch {
237
- }
238
- }
5
+ import {
6
+ EXCLUDE_PATHS,
7
+ STABLE_SCAFFOLD_RELEASE,
8
+ TEMPLATE_REPOSITORY,
9
+ scaffold as scaffoldProject,
10
+ shouldInclude
11
+ } from "create-thally-docs/scaffold";
12
+ import { buildStarterDocsJson } from "create-thally-docs/starter";
13
+ var MCP_TEMPLATE_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
14
+ var MCP_SCAFFOLD_RELEASE_ID = STABLE_SCAFFOLD_RELEASE.id;
239
15
  async function scaffold(options) {
240
- const { projectDir, projectName, description, brandPreset, repoUrl, doInstall, enableAiChat = true, i18nLocales } = options;
241
- const targetDir = resolve(projectDir);
242
- if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
243
- throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
244
- }
245
- mkdirSync(targetDir, { recursive: true });
246
- const slug = slugify(projectName);
247
- await downloadTemplate(targetDir);
248
- writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
249
- updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
250
- patchApiReferenceGuard(targetDir);
251
- patchTopBarNavigation(targetDir);
252
- patchOpenApiFetch(targetDir);
253
- updateEnvExample(targetDir);
254
- if (doInstall) installDeps(targetDir);
255
- initGit(targetDir);
256
- return { projectDir: targetDir };
16
+ return scaffoldProject(options);
257
17
  }
258
18
 
259
19
  // src/tools/create-project.ts
@@ -265,7 +25,7 @@ var createProjectSchema = z.object({
265
25
  repoUrl: z.string().optional().describe("GitHub repository URL (optional)"),
266
26
  install: z.boolean().optional().default(true).describe("Whether to run npm install after scaffolding"),
267
27
  enableAiChat: z.boolean().optional().default(true).describe("Enable AI chat in docs.json (default true)"),
268
- i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Secondary locales to enable (e.g. [{code:"es",label:"Espa\xF1ol"}])')
28
+ 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"}])')
269
29
  });
270
30
  async function handleCreateProject(input) {
271
31
  const { projectDir, brandPreset = "primary", install = true } = input;
@@ -304,20 +64,20 @@ async function handleCreateProject(input) {
304
64
 
305
65
  // src/tools/add-page.ts
306
66
  import { z as z2 } from "zod";
307
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
308
- import { join as join3, dirname } from "path";
67
+ import { existsSync, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
68
+ import { join as join2, dirname } from "path";
309
69
 
310
70
  // src/lib/docs-json.ts
311
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
312
- import { join as join2 } from "path";
71
+ import { readFileSync, writeFileSync } from "fs";
72
+ import { join } from "path";
313
73
  function readDocsJson(projectDir) {
314
- const docsPath = join2(projectDir, "docs.json");
315
- const raw = readFileSync2(docsPath, "utf8");
74
+ const docsPath = join(projectDir, "docs.json");
75
+ const raw = readFileSync(docsPath, "utf8");
316
76
  return JSON.parse(raw);
317
77
  }
318
78
  function writeDocsJson(projectDir, config) {
319
- const docsPath = join2(projectDir, "docs.json");
320
- writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
79
+ const docsPath = join(projectDir, "docs.json");
80
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
321
81
  }
322
82
 
323
83
  // src/lib/page-echo.ts
@@ -369,11 +129,11 @@ async function handleAddPage(input) {
369
129
  `Invalid pageId "${pageId}". Use only alphanumeric characters, hyphens, and slashes. Do not include .mdx extension.`
370
130
  );
371
131
  }
372
- const mdxPath = join3(projectDir, "src", "content", `${pageId}.mdx`);
373
- if (existsSync2(mdxPath)) {
132
+ const mdxPath = join2(projectDir, "src", "content", `${pageId}.mdx`);
133
+ if (existsSync(mdxPath)) {
374
134
  throw new Error(`Page already exists at: ${mdxPath}`);
375
135
  }
376
- mkdirSync2(dirname(mdxPath), { recursive: true });
136
+ mkdirSync(dirname(mdxPath), { recursive: true });
377
137
  const frontmatterLines = [`title: ${title}`];
378
138
  if (description) {
379
139
  frontmatterLines.push(`description: ${description}`);
@@ -387,7 +147,7 @@ ${frontmatterLines.join("\n")}
387
147
 
388
148
  ${bodyContent}
389
149
  `;
390
- writeFileSync3(mdxPath, mdxContent, "utf8");
150
+ writeFileSync2(mdxPath, mdxContent, "utf8");
391
151
  const config = readDocsJson(projectDir);
392
152
  let targetTab = config.tabs[0];
393
153
  if (input.tab) {
@@ -500,9 +260,25 @@ async function handleListPages(input) {
500
260
 
501
261
  // src/tools/update-page.ts
502
262
  import { z as z5 } from "zod";
503
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
504
- import { join as join4 } from "path";
263
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
264
+ import { join as join3 } from "path";
265
+
266
+ // src/lib/frontmatter.ts
505
267
  import matter from "gray-matter";
268
+ var FRONTMATTER_OPTIONS = {
269
+ engines: {
270
+ javascript: () => ({}),
271
+ js: () => ({})
272
+ }
273
+ };
274
+ function parseFrontmatter(raw) {
275
+ return matter(raw, FRONTMATTER_OPTIONS);
276
+ }
277
+ function stringifyFrontmatter(body, data) {
278
+ return matter.stringify(body, data);
279
+ }
280
+
281
+ // src/tools/update-page.ts
506
282
  var updatePageSchema = z5.object({
507
283
  projectDir: z5.string().describe("Path to the Thally project root"),
508
284
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -513,11 +289,11 @@ var updatePageSchema = z5.object({
513
289
  });
514
290
  function findPageFile(projectDir, pageId) {
515
291
  const candidates = [
516
- join4(projectDir, "src", "content", `${pageId}.mdx`),
517
- join4(projectDir, "src", "content", `${pageId}/index.mdx`)
292
+ join3(projectDir, "src", "content", `${pageId}.mdx`),
293
+ join3(projectDir, "src", "content", `${pageId}/index.mdx`)
518
294
  ];
519
295
  for (const candidate of candidates) {
520
- if (existsSync3(candidate)) return candidate;
296
+ if (existsSync2(candidate)) return candidate;
521
297
  }
522
298
  return null;
523
299
  }
@@ -531,8 +307,8 @@ async function handleUpdatePage(input) {
531
307
  src/content/${pageId}/index.mdx`
532
308
  );
533
309
  }
534
- const raw = readFileSync3(filePath, "utf8");
535
- const parsed = matter(raw);
310
+ const raw = readFileSync2(filePath, "utf8");
311
+ const parsed = parseFrontmatter(raw);
536
312
  const newFm = { ...parsed.data };
537
313
  if (input.title !== void 0) newFm["title"] = input.title;
538
314
  if (input.description !== void 0) newFm["description"] = input.description;
@@ -540,8 +316,8 @@ async function handleUpdatePage(input) {
540
316
  Object.assign(newFm, input.mergeFrontmatter);
541
317
  }
542
318
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
543
- const newContent = matter.stringify(newBody.trim(), newFm);
544
- writeFileSync4(filePath, newContent, "utf8");
319
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
320
+ writeFileSync3(filePath, newContent, "utf8");
545
321
  return [
546
322
  `\u2705 Page updated: ${filePath}`,
547
323
  ` pageId: ${pageId}`,
@@ -554,22 +330,30 @@ async function handleUpdatePage(input) {
554
330
  // src/tools/migrate-docs.ts
555
331
  import { z as z6 } from "zod";
556
332
  import { migrateDocs } from "create-thally-docs/migrate";
557
- var migrateDocsSchema = z6.object({
333
+ var migrationSourceShape = {
558
334
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
559
- projectDir: z6.string().describe("Path for new project or existing project dir"),
560
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
561
335
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
562
336
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
563
337
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
564
338
  maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
565
339
  platform: z6.enum(["mintlify", "docusaurus"]).optional().describe("Source platform (default: auto-detect)")
340
+ };
341
+ var migrateDocsSchema = z6.object({
342
+ ...migrationSourceShape,
343
+ projectDir: z6.string().describe("Path for the new canonical Thally project; the directory must be absent or empty")
566
344
  });
567
- async function handleMigrateDocs(input) {
345
+ var importDocsSchema = z6.object({
346
+ ...migrationSourceShape,
347
+ projectDir: z6.string().describe("Path to an existing Thally project whose runtime should be preserved")
348
+ });
349
+ async function runMigration(input, isInPlaceImport) {
568
350
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
569
- const result = await migrateDocs({
351
+ return migrateDocs({
570
352
  sourceUrl: input.sourceUrl,
571
353
  projectDir: input.projectDir,
572
- into: input.into ?? false,
354
+ // Keep this decision inside the adapter so stale or adversarial callers
355
+ // cannot turn a template-first migration into an in-place mutation.
356
+ into: isInPlaceImport,
573
357
  apiKey,
574
358
  branch: input.branch,
575
359
  docsDir: input.docsDir,
@@ -577,14 +361,20 @@ async function handleMigrateDocs(input) {
577
361
  platform: input.platform,
578
362
  yes: true
579
363
  });
580
- return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
364
+ }
365
+ async function handleMigrateDocs(input) {
366
+ const result = await runMigration(input, false);
367
+ return `Migration complete! Created a fresh Thally template at ${result.projectDir} and imported ${result.pagesWritten} pages.`;
368
+ }
369
+ async function handleImportDocs(input) {
370
+ const result = await runMigration(input, true);
371
+ return `Import complete! Imported ${result.pagesWritten} pages into the existing Thally project at ${result.projectDir}.`;
581
372
  }
582
373
 
583
374
  // src/tools/search-docs.ts
584
375
  import { z as z7 } from "zod";
585
- import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
586
- import { join as join5, relative, extname } from "path";
587
- import matter2 from "gray-matter";
376
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
377
+ import { join as join4, relative, extname } from "path";
588
378
  var searchDocsSchema = z7.object({
589
379
  projectDir: z7.string().describe("Path to the Thally project root"),
590
380
  query: z7.string().describe("Search query"),
@@ -593,12 +383,12 @@ var searchDocsSchema = z7.object({
593
383
  function scanMdxFiles(dir, results) {
594
384
  let entries;
595
385
  try {
596
- entries = readdirSync2(dir);
386
+ entries = readdirSync(dir);
597
387
  } catch {
598
388
  return;
599
389
  }
600
390
  for (const entry of entries) {
601
- const fullPath = join5(dir, entry);
391
+ const fullPath = join4(dir, entry);
602
392
  try {
603
393
  const stat = statSync(fullPath);
604
394
  if (stat.isDirectory()) {
@@ -616,11 +406,11 @@ function scoreFiles(files, contentDir, query) {
616
406
  for (const filePath of files) {
617
407
  let raw;
618
408
  try {
619
- raw = readFileSync4(filePath, "utf8");
409
+ raw = readFileSync3(filePath, "utf8");
620
410
  } catch {
621
411
  continue;
622
412
  }
623
- const { data, content } = matter2(raw);
413
+ const { data, content } = parseFrontmatter(raw);
624
414
  const title = data.title ?? "";
625
415
  const description = data.description ?? "";
626
416
  const keywords = data.keywords ?? [];
@@ -641,8 +431,8 @@ function scoreFiles(files, contentDir, query) {
641
431
  }
642
432
  async function handleSearchDocs(input) {
643
433
  const { projectDir, query, limit = 5 } = input;
644
- const contentDir = join5(projectDir, "src", "content");
645
- if (!existsSync4(contentDir)) {
434
+ const contentDir = join4(projectDir, "src", "content");
435
+ if (!existsSync3(contentDir)) {
646
436
  throw new Error(`Content directory not found: ${contentDir}`);
647
437
  }
648
438
  const files = [];
@@ -737,23 +527,22 @@ async function handleAgentReadiness(input) {
737
527
 
738
528
  // src/tools/read-page.ts
739
529
  import { z as z10 } from "zod";
740
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
741
- import { join as join6 } from "path";
742
- import matter3 from "gray-matter";
530
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
531
+ import { join as join5 } from "path";
743
532
  var readPageSchema = z10.object({
744
533
  projectDir: z10.string().describe("Path to the Thally project root"),
745
534
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
746
535
  });
747
536
  async function handleReadPage(input) {
748
537
  const { projectDir, pageId } = input;
749
- const contentDir = join6(projectDir, "src", "content");
538
+ const contentDir = join5(projectDir, "src", "content");
750
539
  const candidates = [
751
- join6(contentDir, `${pageId}.mdx`),
752
- join6(contentDir, `${pageId}/index.mdx`)
540
+ join5(contentDir, `${pageId}.mdx`),
541
+ join5(contentDir, `${pageId}/index.mdx`)
753
542
  ];
754
543
  let filePath = null;
755
544
  for (const c of candidates) {
756
- if (existsSync5(c)) {
545
+ if (existsSync4(c)) {
757
546
  filePath = c;
758
547
  break;
759
548
  }
@@ -761,8 +550,8 @@ async function handleReadPage(input) {
761
550
  if (!filePath) {
762
551
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
763
552
  }
764
- const raw = readFileSync5(filePath, "utf8");
765
- const { data, content } = matter3(raw);
553
+ const raw = readFileSync4(filePath, "utf8");
554
+ const { data, content } = parseFrontmatter(raw);
766
555
  const title = data.title ?? pageId;
767
556
  const description = data.description ?? "";
768
557
  const lines = [`id: ${pageId}`, `title: ${title}`];
@@ -773,9 +562,8 @@ async function handleReadPage(input) {
773
562
 
774
563
  // src/tools/get-context.ts
775
564
  import { z as z11 } from "zod";
776
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
777
- import { join as join7 } from "path";
778
- import matter4 from "gray-matter";
565
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
566
+ import { join as join6 } from "path";
779
567
  var getContextSchema = z11.object({
780
568
  projectDir: z11.string().describe("Path to the Thally project root"),
781
569
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -783,8 +571,8 @@ var getContextSchema = z11.object({
783
571
  });
784
572
  async function handleGetContext(input) {
785
573
  const { projectDir, topic, maxTokens = 4e3 } = input;
786
- const contentDir = join7(projectDir, "src", "content");
787
- if (!existsSync6(contentDir)) {
574
+ const contentDir = join6(projectDir, "src", "content");
575
+ if (!existsSync5(contentDir)) {
788
576
  throw new Error(`Content directory not found: ${contentDir}`);
789
577
  }
790
578
  const files = [];
@@ -798,14 +586,14 @@ async function handleGetContext(input) {
798
586
  const sections = [];
799
587
  for (const result of scored) {
800
588
  const candidates = [
801
- join7(contentDir, `${result.pageId}.mdx`),
802
- join7(contentDir, `${result.pageId}/index.mdx`)
589
+ join6(contentDir, `${result.pageId}.mdx`),
590
+ join6(contentDir, `${result.pageId}/index.mdx`)
803
591
  ];
804
592
  let content = "";
805
593
  for (const c of candidates) {
806
- if (existsSync6(c)) {
807
- const raw = readFileSync6(c, "utf8");
808
- const { content: body } = matter4(raw);
594
+ if (existsSync5(c)) {
595
+ const raw = readFileSync5(c, "utf8");
596
+ const { content: body } = parseFrontmatter(raw);
809
597
  content = body.trim();
810
598
  break;
811
599
  }
@@ -829,9 +617,8 @@ async function handleGetContext(input) {
829
617
 
830
618
  // src/tools/lint-project.ts
831
619
  import { z as z12 } from "zod";
832
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
833
- import { join as join8 } from "path";
834
- import matter5 from "gray-matter";
620
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
621
+ import { join as join7 } from "path";
835
622
  var lintProjectSchema = z12.object({
836
623
  projectDir: z12.string().describe("Path to the Thally project root"),
837
624
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -862,9 +649,9 @@ function addOrphanToNav(projectDir, pageId) {
862
649
  }
863
650
  async function handleLintProject(input) {
864
651
  const { projectDir, fix = false } = input;
865
- const contentDir = join8(projectDir, "src", "content");
652
+ const contentDir = join7(projectDir, "src", "content");
866
653
  const issues = [];
867
- if (!existsSync7(join8(projectDir, "docs.json"))) {
654
+ if (!existsSync6(join7(projectDir, "docs.json"))) {
868
655
  throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
869
656
  }
870
657
  const config = readDocsJson(projectDir);
@@ -883,10 +670,10 @@ async function handleLintProject(input) {
883
670
  }
884
671
  for (const pageId of navPageIds) {
885
672
  const candidates = [
886
- join8(contentDir, `${pageId}.mdx`),
887
- join8(contentDir, `${pageId}/index.mdx`)
673
+ join7(contentDir, `${pageId}.mdx`),
674
+ join7(contentDir, `${pageId}/index.mdx`)
888
675
  ];
889
- if (!candidates.some((c) => existsSync7(c))) {
676
+ if (!candidates.some((c) => existsSync6(c))) {
890
677
  issues.push({
891
678
  severity: "error",
892
679
  message: `"${pageId}" is in docs.json but has no MDX file`,
@@ -895,7 +682,7 @@ async function handleLintProject(input) {
895
682
  }
896
683
  }
897
684
  const allFiles = [];
898
- if (existsSync7(contentDir)) {
685
+ if (existsSync6(contentDir)) {
899
686
  scanMdxFiles(contentDir, allFiles);
900
687
  }
901
688
  const fixedOrphans = [];
@@ -913,8 +700,8 @@ async function handleLintProject(input) {
913
700
  let data = {};
914
701
  let content = "";
915
702
  try {
916
- const raw = readFileSync7(filePath, "utf8");
917
- const parsed = matter5(raw);
703
+ const raw = readFileSync6(filePath, "utf8");
704
+ const parsed = parseFrontmatter(raw);
918
705
  data = parsed.data;
919
706
  content = parsed.content;
920
707
  } catch {
@@ -970,9 +757,8 @@ async function handleLintProject(input) {
970
757
 
971
758
  // src/tools/translate-docs.ts
972
759
  import { z as z13 } from "zod";
973
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
974
- import { join as join9, dirname as dirname2 } from "path";
975
- import matter6 from "gray-matter";
760
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
761
+ import { join as join8, dirname as dirname2 } from "path";
976
762
  import Anthropic from "@anthropic-ai/sdk";
977
763
  import pLimit from "p-limit";
978
764
  var translateDocsSchema = z13.object({
@@ -984,8 +770,8 @@ var translateDocsSchema = z13.object({
984
770
  model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
985
771
  });
986
772
  function readDocsJson2(projectDir) {
987
- const docsPath = join9(projectDir, "docs.json");
988
- const raw = readFileSync8(docsPath, "utf8");
773
+ const docsPath = join8(projectDir, "docs.json");
774
+ const raw = readFileSync7(docsPath, "utf8");
989
775
  return JSON.parse(raw);
990
776
  }
991
777
  function collectPageIds(pages) {
@@ -1027,12 +813,12 @@ function getAllPageIds(config) {
1027
813
  return { ids, hrefOnlyPages };
1028
814
  }
1029
815
  function findSourceFile(projectDir, pageId) {
1030
- const contentRoot = join9(projectDir, "src", "content");
816
+ const contentRoot = join8(projectDir, "src", "content");
1031
817
  const candidates = [
1032
- join9(contentRoot, `${pageId}.mdx`),
1033
- join9(contentRoot, `${pageId}/index.mdx`)
818
+ join8(contentRoot, `${pageId}.mdx`),
819
+ join8(contentRoot, `${pageId}/index.mdx`)
1034
820
  ];
1035
- return candidates.find((p) => existsSync8(p)) ?? null;
821
+ return candidates.find((p) => existsSync7(p)) ?? null;
1036
822
  }
1037
823
  var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
1038
824
 
@@ -1084,7 +870,7 @@ async function handleTranslateDocs(input) {
1084
870
  }
1085
871
  const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
1086
872
  const targetPageIds = pages ?? allPageIds;
1087
- const contentRoot = join9(projectDir, "src", "content");
873
+ const contentRoot = join8(projectDir, "src", "content");
1088
874
  const toTranslate = [];
1089
875
  const skipped = [];
1090
876
  for (const pageId of targetPageIds) {
@@ -1094,8 +880,8 @@ async function handleTranslateDocs(input) {
1094
880
  continue;
1095
881
  }
1096
882
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
1097
- const targetFile = join9(contentRoot, locale, relativeFromContent);
1098
- if (existsSync8(targetFile) && !force) {
883
+ const targetFile = join8(contentRoot, locale, relativeFromContent);
884
+ if (existsSync7(targetFile) && !force) {
1099
885
  skipped.push(`${pageId} (already translated)`);
1100
886
  continue;
1101
887
  }
@@ -1111,14 +897,14 @@ async function handleTranslateDocs(input) {
1111
897
  toTranslate.map(
1112
898
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1113
899
  try {
1114
- const sourceContent = readFileSync8(sourceFile, "utf8");
1115
- const parsed = matter6(sourceContent);
900
+ const sourceContent = readFileSync7(sourceFile, "utf8");
901
+ const parsed = parseFrontmatter(sourceContent);
1116
902
  if (!parsed.data.title) {
1117
903
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1118
904
  }
1119
905
  const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
1120
- mkdirSync3(dirname2(targetFile), { recursive: true });
1121
- writeFileSync6(targetFile, translated + "\n", "utf8");
906
+ mkdirSync2(dirname2(targetFile), { recursive: true });
907
+ writeFileSync5(targetFile, translated + "\n", "utf8");
1122
908
  results.push({ pageId, success: true });
1123
909
  } catch (err) {
1124
910
  const msg = err instanceof Error ? err.message : String(err);
@@ -1467,11 +1253,18 @@ var tools = [
1467
1253
  }),
1468
1254
  defineTool({
1469
1255
  name: "migrate_docs",
1470
- description: "Crawl a docs site and migrate it into a Thally project",
1256
+ 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",
1471
1257
  scope: "project",
1472
1258
  schema: migrateDocsSchema,
1473
1259
  handler: handleMigrateDocs
1474
1260
  }),
1261
+ defineTool({
1262
+ name: "import_docs",
1263
+ description: "Import content into an existing Thally project without scaffolding; use only when an in-place import is explicitly requested",
1264
+ scope: "project",
1265
+ schema: importDocsSchema,
1266
+ handler: handleImportDocs
1267
+ }),
1475
1268
  defineTool({
1476
1269
  name: "search_docs",
1477
1270
  description: "Search documentation pages by keyword \u2014 returns ranked list of matching pages",