@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/README.md +12 -5
- package/dist/create-project.d.ts +49 -0
- package/dist/create-project.js +67 -0
- package/dist/index.js +129 -336
- package/dist/tools.js +129 -336
- package/package.json +7 -5
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 {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var
|
|
23
|
-
var
|
|
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
|
-
|
|
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('
|
|
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
|
|
317
|
-
import { join as
|
|
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
|
|
321
|
-
import { join
|
|
80
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
81
|
+
import { join } from "path";
|
|
322
82
|
function readDocsJson(projectDir) {
|
|
323
|
-
const docsPath =
|
|
324
|
-
const raw =
|
|
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 =
|
|
329
|
-
|
|
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 =
|
|
382
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
513
|
-
import { join as
|
|
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
|
-
|
|
526
|
-
|
|
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 (
|
|
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 =
|
|
544
|
-
const parsed =
|
|
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 =
|
|
553
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
360
|
+
return migrateDocs({
|
|
579
361
|
sourceUrl: input.sourceUrl,
|
|
580
362
|
projectDir: input.projectDir,
|
|
581
|
-
|
|
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
|
-
|
|
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
|
|
595
|
-
import { join as
|
|
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 =
|
|
395
|
+
entries = readdirSync(dir);
|
|
606
396
|
} catch {
|
|
607
397
|
return;
|
|
608
398
|
}
|
|
609
399
|
for (const entry of entries) {
|
|
610
|
-
const fullPath =
|
|
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 =
|
|
418
|
+
raw = readFileSync3(filePath, "utf8");
|
|
629
419
|
} catch {
|
|
630
420
|
continue;
|
|
631
421
|
}
|
|
632
|
-
const { data, content } =
|
|
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 =
|
|
654
|
-
if (!
|
|
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
|
|
750
|
-
import { join as
|
|
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 =
|
|
547
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
759
548
|
const candidates = [
|
|
760
|
-
|
|
761
|
-
|
|
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 (
|
|
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 =
|
|
774
|
-
const { data, content } =
|
|
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
|
|
786
|
-
import { join as
|
|
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 =
|
|
796
|
-
if (!
|
|
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
|
-
|
|
811
|
-
|
|
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 (
|
|
816
|
-
const raw =
|
|
817
|
-
const { content: body } =
|
|
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
|
|
842
|
-
import { join as
|
|
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 =
|
|
661
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
875
662
|
const issues = [];
|
|
876
|
-
if (!
|
|
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
|
-
|
|
896
|
-
|
|
682
|
+
join7(contentDir, `${pageId}.mdx`),
|
|
683
|
+
join7(contentDir, `${pageId}/index.mdx`)
|
|
897
684
|
];
|
|
898
|
-
if (!candidates.some((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 (
|
|
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 =
|
|
926
|
-
const parsed =
|
|
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
|
|
983
|
-
import { join as
|
|
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 =
|
|
997
|
-
const raw =
|
|
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 =
|
|
825
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1040
826
|
const candidates = [
|
|
1041
|
-
|
|
1042
|
-
|
|
827
|
+
join8(contentRoot, `${pageId}.mdx`),
|
|
828
|
+
join8(contentRoot, `${pageId}/index.mdx`)
|
|
1043
829
|
];
|
|
1044
|
-
return candidates.find((p) =>
|
|
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 =
|
|
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 =
|
|
1107
|
-
if (
|
|
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 =
|
|
1124
|
-
const parsed =
|
|
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
|
-
|
|
1130
|
-
|
|
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: "
|
|
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",
|