@thallylabs/mcp 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-project.d.ts +49 -0
- package/dist/create-project.js +67 -0
- package/dist/index.js +76 -316
- package/dist/tools.js +76 -316
- package/package.json +6 -4
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,8 +269,8 @@ async function handleListPages(input) {
|
|
|
509
269
|
|
|
510
270
|
// src/tools/update-page.ts
|
|
511
271
|
import { z as z5 } from "zod";
|
|
512
|
-
import { existsSync as
|
|
513
|
-
import { join as
|
|
272
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
273
|
+
import { join as join3 } from "path";
|
|
514
274
|
|
|
515
275
|
// src/lib/frontmatter.ts
|
|
516
276
|
import matter from "gray-matter";
|
|
@@ -538,11 +298,11 @@ var updatePageSchema = z5.object({
|
|
|
538
298
|
});
|
|
539
299
|
function findPageFile(projectDir, pageId) {
|
|
540
300
|
const candidates = [
|
|
541
|
-
|
|
542
|
-
|
|
301
|
+
join3(projectDir, "src", "content", `${pageId}.mdx`),
|
|
302
|
+
join3(projectDir, "src", "content", `${pageId}/index.mdx`)
|
|
543
303
|
];
|
|
544
304
|
for (const candidate of candidates) {
|
|
545
|
-
if (
|
|
305
|
+
if (existsSync2(candidate)) return candidate;
|
|
546
306
|
}
|
|
547
307
|
return null;
|
|
548
308
|
}
|
|
@@ -556,7 +316,7 @@ async function handleUpdatePage(input) {
|
|
|
556
316
|
src/content/${pageId}/index.mdx`
|
|
557
317
|
);
|
|
558
318
|
}
|
|
559
|
-
const raw =
|
|
319
|
+
const raw = readFileSync2(filePath, "utf8");
|
|
560
320
|
const parsed = parseFrontmatter(raw);
|
|
561
321
|
const newFm = { ...parsed.data };
|
|
562
322
|
if (input.title !== void 0) newFm["title"] = input.title;
|
|
@@ -566,7 +326,7 @@ async function handleUpdatePage(input) {
|
|
|
566
326
|
}
|
|
567
327
|
const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
|
|
568
328
|
const newContent = stringifyFrontmatter(newBody.trim(), newFm);
|
|
569
|
-
|
|
329
|
+
writeFileSync3(filePath, newContent, "utf8");
|
|
570
330
|
return [
|
|
571
331
|
`\u2705 Page updated: ${filePath}`,
|
|
572
332
|
` pageId: ${pageId}`,
|
|
@@ -622,8 +382,8 @@ async function handleImportDocs(input) {
|
|
|
622
382
|
|
|
623
383
|
// src/tools/search-docs.ts
|
|
624
384
|
import { z as z7 } from "zod";
|
|
625
|
-
import { readdirSync
|
|
626
|
-
import { join as
|
|
385
|
+
import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
386
|
+
import { join as join4, relative, extname } from "path";
|
|
627
387
|
var searchDocsSchema = z7.object({
|
|
628
388
|
projectDir: z7.string().describe("Path to the Thally project root"),
|
|
629
389
|
query: z7.string().describe("Search query"),
|
|
@@ -632,12 +392,12 @@ var searchDocsSchema = z7.object({
|
|
|
632
392
|
function scanMdxFiles(dir, results) {
|
|
633
393
|
let entries;
|
|
634
394
|
try {
|
|
635
|
-
entries =
|
|
395
|
+
entries = readdirSync(dir);
|
|
636
396
|
} catch {
|
|
637
397
|
return;
|
|
638
398
|
}
|
|
639
399
|
for (const entry of entries) {
|
|
640
|
-
const fullPath =
|
|
400
|
+
const fullPath = join4(dir, entry);
|
|
641
401
|
try {
|
|
642
402
|
const stat = statSync(fullPath);
|
|
643
403
|
if (stat.isDirectory()) {
|
|
@@ -655,7 +415,7 @@ function scoreFiles(files, contentDir, query) {
|
|
|
655
415
|
for (const filePath of files) {
|
|
656
416
|
let raw;
|
|
657
417
|
try {
|
|
658
|
-
raw =
|
|
418
|
+
raw = readFileSync3(filePath, "utf8");
|
|
659
419
|
} catch {
|
|
660
420
|
continue;
|
|
661
421
|
}
|
|
@@ -680,8 +440,8 @@ function scoreFiles(files, contentDir, query) {
|
|
|
680
440
|
}
|
|
681
441
|
async function handleSearchDocs(input) {
|
|
682
442
|
const { projectDir, query, limit = 5 } = input;
|
|
683
|
-
const contentDir =
|
|
684
|
-
if (!
|
|
443
|
+
const contentDir = join4(projectDir, "src", "content");
|
|
444
|
+
if (!existsSync3(contentDir)) {
|
|
685
445
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
686
446
|
}
|
|
687
447
|
const files = [];
|
|
@@ -776,22 +536,22 @@ async function handleAgentReadiness(input) {
|
|
|
776
536
|
|
|
777
537
|
// src/tools/read-page.ts
|
|
778
538
|
import { z as z10 } from "zod";
|
|
779
|
-
import { existsSync as
|
|
780
|
-
import { join as
|
|
539
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
540
|
+
import { join as join5 } from "path";
|
|
781
541
|
var readPageSchema = z10.object({
|
|
782
542
|
projectDir: z10.string().describe("Path to the Thally project root"),
|
|
783
543
|
pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
|
|
784
544
|
});
|
|
785
545
|
async function handleReadPage(input) {
|
|
786
546
|
const { projectDir, pageId } = input;
|
|
787
|
-
const contentDir =
|
|
547
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
788
548
|
const candidates = [
|
|
789
|
-
|
|
790
|
-
|
|
549
|
+
join5(contentDir, `${pageId}.mdx`),
|
|
550
|
+
join5(contentDir, `${pageId}/index.mdx`)
|
|
791
551
|
];
|
|
792
552
|
let filePath = null;
|
|
793
553
|
for (const c of candidates) {
|
|
794
|
-
if (
|
|
554
|
+
if (existsSync4(c)) {
|
|
795
555
|
filePath = c;
|
|
796
556
|
break;
|
|
797
557
|
}
|
|
@@ -799,7 +559,7 @@ async function handleReadPage(input) {
|
|
|
799
559
|
if (!filePath) {
|
|
800
560
|
throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
|
|
801
561
|
}
|
|
802
|
-
const raw =
|
|
562
|
+
const raw = readFileSync4(filePath, "utf8");
|
|
803
563
|
const { data, content } = parseFrontmatter(raw);
|
|
804
564
|
const title = data.title ?? pageId;
|
|
805
565
|
const description = data.description ?? "";
|
|
@@ -811,8 +571,8 @@ async function handleReadPage(input) {
|
|
|
811
571
|
|
|
812
572
|
// src/tools/get-context.ts
|
|
813
573
|
import { z as z11 } from "zod";
|
|
814
|
-
import { existsSync as
|
|
815
|
-
import { join as
|
|
574
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
575
|
+
import { join as join6 } from "path";
|
|
816
576
|
var getContextSchema = z11.object({
|
|
817
577
|
projectDir: z11.string().describe("Path to the Thally project root"),
|
|
818
578
|
topic: z11.string().describe("Topic or question to find relevant docs for"),
|
|
@@ -820,8 +580,8 @@ var getContextSchema = z11.object({
|
|
|
820
580
|
});
|
|
821
581
|
async function handleGetContext(input) {
|
|
822
582
|
const { projectDir, topic, maxTokens = 4e3 } = input;
|
|
823
|
-
const contentDir =
|
|
824
|
-
if (!
|
|
583
|
+
const contentDir = join6(projectDir, "src", "content");
|
|
584
|
+
if (!existsSync5(contentDir)) {
|
|
825
585
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
826
586
|
}
|
|
827
587
|
const files = [];
|
|
@@ -835,13 +595,13 @@ async function handleGetContext(input) {
|
|
|
835
595
|
const sections = [];
|
|
836
596
|
for (const result of scored) {
|
|
837
597
|
const candidates = [
|
|
838
|
-
|
|
839
|
-
|
|
598
|
+
join6(contentDir, `${result.pageId}.mdx`),
|
|
599
|
+
join6(contentDir, `${result.pageId}/index.mdx`)
|
|
840
600
|
];
|
|
841
601
|
let content = "";
|
|
842
602
|
for (const c of candidates) {
|
|
843
|
-
if (
|
|
844
|
-
const raw =
|
|
603
|
+
if (existsSync5(c)) {
|
|
604
|
+
const raw = readFileSync5(c, "utf8");
|
|
845
605
|
const { content: body } = parseFrontmatter(raw);
|
|
846
606
|
content = body.trim();
|
|
847
607
|
break;
|
|
@@ -866,8 +626,8 @@ async function handleGetContext(input) {
|
|
|
866
626
|
|
|
867
627
|
// src/tools/lint-project.ts
|
|
868
628
|
import { z as z12 } from "zod";
|
|
869
|
-
import { existsSync as
|
|
870
|
-
import { join as
|
|
629
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
630
|
+
import { join as join7 } from "path";
|
|
871
631
|
var lintProjectSchema = z12.object({
|
|
872
632
|
projectDir: z12.string().describe("Path to the Thally project root"),
|
|
873
633
|
fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
|
|
@@ -898,9 +658,9 @@ function addOrphanToNav(projectDir, pageId) {
|
|
|
898
658
|
}
|
|
899
659
|
async function handleLintProject(input) {
|
|
900
660
|
const { projectDir, fix = false } = input;
|
|
901
|
-
const contentDir =
|
|
661
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
902
662
|
const issues = [];
|
|
903
|
-
if (!
|
|
663
|
+
if (!existsSync6(join7(projectDir, "docs.json"))) {
|
|
904
664
|
throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
|
|
905
665
|
}
|
|
906
666
|
const config = readDocsJson(projectDir);
|
|
@@ -919,10 +679,10 @@ async function handleLintProject(input) {
|
|
|
919
679
|
}
|
|
920
680
|
for (const pageId of navPageIds) {
|
|
921
681
|
const candidates = [
|
|
922
|
-
|
|
923
|
-
|
|
682
|
+
join7(contentDir, `${pageId}.mdx`),
|
|
683
|
+
join7(contentDir, `${pageId}/index.mdx`)
|
|
924
684
|
];
|
|
925
|
-
if (!candidates.some((c) =>
|
|
685
|
+
if (!candidates.some((c) => existsSync6(c))) {
|
|
926
686
|
issues.push({
|
|
927
687
|
severity: "error",
|
|
928
688
|
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
@@ -931,7 +691,7 @@ async function handleLintProject(input) {
|
|
|
931
691
|
}
|
|
932
692
|
}
|
|
933
693
|
const allFiles = [];
|
|
934
|
-
if (
|
|
694
|
+
if (existsSync6(contentDir)) {
|
|
935
695
|
scanMdxFiles(contentDir, allFiles);
|
|
936
696
|
}
|
|
937
697
|
const fixedOrphans = [];
|
|
@@ -949,7 +709,7 @@ async function handleLintProject(input) {
|
|
|
949
709
|
let data = {};
|
|
950
710
|
let content = "";
|
|
951
711
|
try {
|
|
952
|
-
const raw =
|
|
712
|
+
const raw = readFileSync6(filePath, "utf8");
|
|
953
713
|
const parsed = parseFrontmatter(raw);
|
|
954
714
|
data = parsed.data;
|
|
955
715
|
content = parsed.content;
|
|
@@ -1006,8 +766,8 @@ async function handleLintProject(input) {
|
|
|
1006
766
|
|
|
1007
767
|
// src/tools/translate-docs.ts
|
|
1008
768
|
import { z as z13 } from "zod";
|
|
1009
|
-
import { readFileSync as
|
|
1010
|
-
import { join as
|
|
769
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
770
|
+
import { join as join8, dirname as dirname2 } from "path";
|
|
1011
771
|
import Anthropic from "@anthropic-ai/sdk";
|
|
1012
772
|
import pLimit from "p-limit";
|
|
1013
773
|
var translateDocsSchema = z13.object({
|
|
@@ -1019,8 +779,8 @@ var translateDocsSchema = z13.object({
|
|
|
1019
779
|
model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
|
|
1020
780
|
});
|
|
1021
781
|
function readDocsJson2(projectDir) {
|
|
1022
|
-
const docsPath =
|
|
1023
|
-
const raw =
|
|
782
|
+
const docsPath = join8(projectDir, "docs.json");
|
|
783
|
+
const raw = readFileSync7(docsPath, "utf8");
|
|
1024
784
|
return JSON.parse(raw);
|
|
1025
785
|
}
|
|
1026
786
|
function collectPageIds(pages) {
|
|
@@ -1062,12 +822,12 @@ function getAllPageIds(config) {
|
|
|
1062
822
|
return { ids, hrefOnlyPages };
|
|
1063
823
|
}
|
|
1064
824
|
function findSourceFile(projectDir, pageId) {
|
|
1065
|
-
const contentRoot =
|
|
825
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1066
826
|
const candidates = [
|
|
1067
|
-
|
|
1068
|
-
|
|
827
|
+
join8(contentRoot, `${pageId}.mdx`),
|
|
828
|
+
join8(contentRoot, `${pageId}/index.mdx`)
|
|
1069
829
|
];
|
|
1070
|
-
return candidates.find((p) =>
|
|
830
|
+
return candidates.find((p) => existsSync7(p)) ?? null;
|
|
1071
831
|
}
|
|
1072
832
|
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
1073
833
|
|
|
@@ -1119,7 +879,7 @@ async function handleTranslateDocs(input) {
|
|
|
1119
879
|
}
|
|
1120
880
|
const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
|
|
1121
881
|
const targetPageIds = pages ?? allPageIds;
|
|
1122
|
-
const contentRoot =
|
|
882
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1123
883
|
const toTranslate = [];
|
|
1124
884
|
const skipped = [];
|
|
1125
885
|
for (const pageId of targetPageIds) {
|
|
@@ -1129,8 +889,8 @@ async function handleTranslateDocs(input) {
|
|
|
1129
889
|
continue;
|
|
1130
890
|
}
|
|
1131
891
|
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
1132
|
-
const targetFile =
|
|
1133
|
-
if (
|
|
892
|
+
const targetFile = join8(contentRoot, locale, relativeFromContent);
|
|
893
|
+
if (existsSync7(targetFile) && !force) {
|
|
1134
894
|
skipped.push(`${pageId} (already translated)`);
|
|
1135
895
|
continue;
|
|
1136
896
|
}
|
|
@@ -1146,14 +906,14 @@ async function handleTranslateDocs(input) {
|
|
|
1146
906
|
toTranslate.map(
|
|
1147
907
|
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
1148
908
|
try {
|
|
1149
|
-
const sourceContent =
|
|
909
|
+
const sourceContent = readFileSync7(sourceFile, "utf8");
|
|
1150
910
|
const parsed = parseFrontmatter(sourceContent);
|
|
1151
911
|
if (!parsed.data.title) {
|
|
1152
912
|
console.warn(`[translate] ${pageId}: missing title in frontmatter`);
|
|
1153
913
|
}
|
|
1154
914
|
const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
|
|
1155
|
-
|
|
1156
|
-
|
|
915
|
+
mkdirSync2(dirname2(targetFile), { recursive: true });
|
|
916
|
+
writeFileSync5(targetFile, translated + "\n", "utf8");
|
|
1157
917
|
results.push({ pageId, success: true });
|
|
1158
918
|
} catch (err) {
|
|
1159
919
|
const msg = err instanceof Error ? err.message : String(err);
|