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