@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/tools.js
CHANGED
|
@@ -2,258 +2,18 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// src/lib/scaffold.ts
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var
|
|
14
|
-
var
|
|
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
|
-
|
|
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('
|
|
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
|
|
308
|
-
import { join as
|
|
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
|
|
312
|
-
import { join
|
|
71
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
72
|
+
import { join } from "path";
|
|
313
73
|
function readDocsJson(projectDir) {
|
|
314
|
-
const docsPath =
|
|
315
|
-
const raw =
|
|
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 =
|
|
320
|
-
|
|
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 =
|
|
373
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
504
|
-
import { join as
|
|
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
|
-
|
|
517
|
-
|
|
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 (
|
|
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 =
|
|
535
|
-
const parsed =
|
|
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 =
|
|
544
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
351
|
+
return migrateDocs({
|
|
570
352
|
sourceUrl: input.sourceUrl,
|
|
571
353
|
projectDir: input.projectDir,
|
|
572
|
-
|
|
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
|
-
|
|
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
|
|
586
|
-
import { join as
|
|
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 =
|
|
386
|
+
entries = readdirSync(dir);
|
|
597
387
|
} catch {
|
|
598
388
|
return;
|
|
599
389
|
}
|
|
600
390
|
for (const entry of entries) {
|
|
601
|
-
const fullPath =
|
|
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 =
|
|
409
|
+
raw = readFileSync3(filePath, "utf8");
|
|
620
410
|
} catch {
|
|
621
411
|
continue;
|
|
622
412
|
}
|
|
623
|
-
const { data, content } =
|
|
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 =
|
|
645
|
-
if (!
|
|
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
|
|
741
|
-
import { join as
|
|
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 =
|
|
538
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
750
539
|
const candidates = [
|
|
751
|
-
|
|
752
|
-
|
|
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 (
|
|
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 =
|
|
765
|
-
const { data, content } =
|
|
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
|
|
777
|
-
import { join as
|
|
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 =
|
|
787
|
-
if (!
|
|
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
|
-
|
|
802
|
-
|
|
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 (
|
|
807
|
-
const raw =
|
|
808
|
-
const { content: body } =
|
|
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
|
|
833
|
-
import { join as
|
|
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 =
|
|
652
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
866
653
|
const issues = [];
|
|
867
|
-
if (!
|
|
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
|
-
|
|
887
|
-
|
|
673
|
+
join7(contentDir, `${pageId}.mdx`),
|
|
674
|
+
join7(contentDir, `${pageId}/index.mdx`)
|
|
888
675
|
];
|
|
889
|
-
if (!candidates.some((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 (
|
|
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 =
|
|
917
|
-
const parsed =
|
|
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
|
|
974
|
-
import { join as
|
|
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 =
|
|
988
|
-
const raw =
|
|
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 =
|
|
816
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1031
817
|
const candidates = [
|
|
1032
|
-
|
|
1033
|
-
|
|
818
|
+
join8(contentRoot, `${pageId}.mdx`),
|
|
819
|
+
join8(contentRoot, `${pageId}/index.mdx`)
|
|
1034
820
|
];
|
|
1035
|
-
return candidates.find((p) =>
|
|
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 =
|
|
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 =
|
|
1098
|
-
if (
|
|
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 =
|
|
1115
|
-
const parsed =
|
|
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
|
-
|
|
1121
|
-
|
|
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: "
|
|
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",
|