@thallylabs/mcp 0.10.21 → 0.10.23
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/index.js +438 -142
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +438 -142
- package/dist/track.d.ts +14 -8
- package/dist/track.js +81 -21
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -333,24 +333,106 @@ async function handleUpdatePage(input) {
|
|
|
333
333
|
].join("\n");
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// src/tools/
|
|
336
|
+
// src/tools/replace-page-text.ts
|
|
337
|
+
import { createHash } from "crypto";
|
|
338
|
+
import {
|
|
339
|
+
existsSync as existsSync3,
|
|
340
|
+
lstatSync,
|
|
341
|
+
readFileSync as readFileSync3,
|
|
342
|
+
realpathSync,
|
|
343
|
+
writeFileSync as writeFileSync4
|
|
344
|
+
} from "fs";
|
|
345
|
+
import { isAbsolute, join as join4, relative, sep } from "path";
|
|
337
346
|
import { z as z6 } from "zod";
|
|
347
|
+
var MAX_REPLACEMENT_BYTES = 64 * 1024;
|
|
348
|
+
var replacePageTextSchema = z6.object({
|
|
349
|
+
projectDir: z6.string().describe("Path to the Thally project root"),
|
|
350
|
+
pageId: z6.string().describe(
|
|
351
|
+
'Page identifier (for example "guides/auth"). No .mdx extension.'
|
|
352
|
+
),
|
|
353
|
+
oldText: z6.string().min(1).max(MAX_REPLACEMENT_BYTES).describe("Exact existing text to replace; it must occur exactly once"),
|
|
354
|
+
newText: z6.string().min(1).max(MAX_REPLACEMENT_BYTES).describe(
|
|
355
|
+
"Complete replacement prose for oldText; never include a Track evidence marker"
|
|
356
|
+
),
|
|
357
|
+
evidenceReferenceId: z6.string().min(1).max(128).optional().describe(
|
|
358
|
+
"Exact evidence reference ID supplied by Track. When present, the tool appends its deterministic citation marker."
|
|
359
|
+
)
|
|
360
|
+
});
|
|
361
|
+
function isInside(root, candidate) {
|
|
362
|
+
const fromRoot = relative(root, candidate);
|
|
363
|
+
return Boolean(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
364
|
+
}
|
|
365
|
+
function pageFile(projectDir, pageId) {
|
|
366
|
+
if (!/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/u.test(pageId) || pageId.split("/").some((part) => part.toLowerCase() === ".git")) {
|
|
367
|
+
throw new Error("Invalid pageId.");
|
|
368
|
+
}
|
|
369
|
+
const contentRoot = realpathSync(join4(projectDir, "src", "content"));
|
|
370
|
+
const candidates = [
|
|
371
|
+
join4(projectDir, "src", "content", `${pageId}.mdx`),
|
|
372
|
+
join4(projectDir, "src", "content", pageId, "index.mdx")
|
|
373
|
+
];
|
|
374
|
+
const existing = candidates.filter((candidate2) => existsSync3(candidate2));
|
|
375
|
+
if (existing.length !== 1) throw new Error("Page not found or ambiguous.");
|
|
376
|
+
const candidate = existing[0];
|
|
377
|
+
const metadata = lstatSync(candidate);
|
|
378
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || !isInside(contentRoot, realpathSync(candidate))) {
|
|
379
|
+
throw new Error("Page must be a regular file inside src/content.");
|
|
380
|
+
}
|
|
381
|
+
return candidate;
|
|
382
|
+
}
|
|
383
|
+
function consumedTrailingLineSeparator(value) {
|
|
384
|
+
return value.match(/(\r?\n)[^\S\r\n]*$/u)?.[1] ?? "";
|
|
385
|
+
}
|
|
386
|
+
async function handleReplacePageText(input) {
|
|
387
|
+
if (input.oldText.includes("\0") || input.newText.includes("\0") || input.oldText === input.newText || input.newText.trim().length === 0) {
|
|
388
|
+
throw new Error("Replacement text is invalid.");
|
|
389
|
+
}
|
|
390
|
+
const filePath = pageFile(input.projectDir, input.pageId);
|
|
391
|
+
const source = readFileSync3(filePath, "utf8");
|
|
392
|
+
const first = source.indexOf(input.oldText);
|
|
393
|
+
if (first < 0 || source.indexOf(input.oldText, first + input.oldText.length) >= 0) {
|
|
394
|
+
throw new Error("oldText must match exactly one span.");
|
|
395
|
+
}
|
|
396
|
+
const marker = input.evidenceReferenceId ? `<!-- thally-cite:v1:${createHash("sha256").update(`evidence\0${input.evidenceReferenceId}`, "utf8").digest("hex")} -->` : null;
|
|
397
|
+
if (marker && input.newText.includes(marker)) {
|
|
398
|
+
throw new Error("Replacement text must not include its citation marker.");
|
|
399
|
+
}
|
|
400
|
+
const suffixSeparator = marker ? consumedTrailingLineSeparator(input.oldText) : "";
|
|
401
|
+
const citedReplacement = marker ? `${input.newText.replace(/\s*$/u, "")}
|
|
402
|
+
${marker}` : input.newText;
|
|
403
|
+
const replacement = `${citedReplacement}${suffixSeparator}`;
|
|
404
|
+
writeFileSync4(
|
|
405
|
+
filePath,
|
|
406
|
+
`${source.slice(0, first)}${replacement}${source.slice(first + input.oldText.length)}`,
|
|
407
|
+
"utf8"
|
|
408
|
+
);
|
|
409
|
+
const startLine = source.slice(0, first).split("\n").length;
|
|
410
|
+
const endLine = startLine + citedReplacement.split("\n").length - 1;
|
|
411
|
+
return [
|
|
412
|
+
`\u2705 Page text replaced: ${input.pageId}`,
|
|
413
|
+
`Final replacement lines: ${startLine}-${endLine}.`,
|
|
414
|
+
"Use this exact final line range for the corresponding factual claim."
|
|
415
|
+
].join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/tools/migrate-docs.ts
|
|
419
|
+
import { z as z7 } from "zod";
|
|
338
420
|
import { migrateDocs } from "create-thally-docs/migrate";
|
|
339
421
|
var migrationSourceShape = {
|
|
340
|
-
sourceUrl:
|
|
341
|
-
branch:
|
|
342
|
-
docsDir:
|
|
343
|
-
apiKey:
|
|
344
|
-
maxPages:
|
|
345
|
-
platform:
|
|
422
|
+
sourceUrl: z7.string().describe("GitHub repository URL or public documentation URL to migrate"),
|
|
423
|
+
branch: z7.string().optional().describe("Git branch (default: auto-detect)"),
|
|
424
|
+
docsDir: z7.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
|
|
425
|
+
apiKey: z7.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
|
|
426
|
+
maxPages: z7.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
|
|
427
|
+
platform: z7.enum(["mintlify", "docusaurus"]).optional().describe("Source platform (default: auto-detect)")
|
|
346
428
|
};
|
|
347
|
-
var migrateDocsSchema =
|
|
429
|
+
var migrateDocsSchema = z7.object({
|
|
348
430
|
...migrationSourceShape,
|
|
349
|
-
projectDir:
|
|
431
|
+
projectDir: z7.string().describe("Path for the new canonical Thally project; the directory must be absent or empty")
|
|
350
432
|
});
|
|
351
|
-
var importDocsSchema =
|
|
433
|
+
var importDocsSchema = z7.object({
|
|
352
434
|
...migrationSourceShape,
|
|
353
|
-
projectDir:
|
|
435
|
+
projectDir: z7.string().describe("Path to an existing Thally project whose runtime should be preserved")
|
|
354
436
|
});
|
|
355
437
|
async function runMigration(input, isInPlaceImport) {
|
|
356
438
|
const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
@@ -378,13 +460,13 @@ async function handleImportDocs(input) {
|
|
|
378
460
|
}
|
|
379
461
|
|
|
380
462
|
// src/tools/search-docs.ts
|
|
381
|
-
import { z as
|
|
382
|
-
import { readdirSync, statSync, readFileSync as
|
|
383
|
-
import { join as
|
|
384
|
-
var searchDocsSchema =
|
|
385
|
-
projectDir:
|
|
386
|
-
query:
|
|
387
|
-
limit:
|
|
463
|
+
import { z as z8 } from "zod";
|
|
464
|
+
import { readdirSync, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
465
|
+
import { join as join5, relative as relative2, extname } from "path";
|
|
466
|
+
var searchDocsSchema = z8.object({
|
|
467
|
+
projectDir: z8.string().describe("Path to the Thally project root"),
|
|
468
|
+
query: z8.string().describe("Search query"),
|
|
469
|
+
limit: z8.number().optional().default(5).describe("Max results to return (default 5)")
|
|
388
470
|
});
|
|
389
471
|
function scanMdxFiles(dir, results) {
|
|
390
472
|
let entries;
|
|
@@ -394,7 +476,7 @@ function scanMdxFiles(dir, results) {
|
|
|
394
476
|
return;
|
|
395
477
|
}
|
|
396
478
|
for (const entry of entries) {
|
|
397
|
-
const fullPath =
|
|
479
|
+
const fullPath = join5(dir, entry);
|
|
398
480
|
try {
|
|
399
481
|
const stat = statSync(fullPath);
|
|
400
482
|
if (stat.isDirectory()) {
|
|
@@ -412,7 +494,7 @@ function scoreFiles(files, contentDir, query) {
|
|
|
412
494
|
for (const filePath of files) {
|
|
413
495
|
let raw;
|
|
414
496
|
try {
|
|
415
|
-
raw =
|
|
497
|
+
raw = readFileSync4(filePath, "utf8");
|
|
416
498
|
} catch {
|
|
417
499
|
continue;
|
|
418
500
|
}
|
|
@@ -420,7 +502,7 @@ function scoreFiles(files, contentDir, query) {
|
|
|
420
502
|
const title = data.title ?? "";
|
|
421
503
|
const description = data.description ?? "";
|
|
422
504
|
const keywords = data.keywords ?? [];
|
|
423
|
-
const pageId =
|
|
505
|
+
const pageId = relative2(contentDir, filePath).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
424
506
|
let score = 0;
|
|
425
507
|
for (const term of terms) {
|
|
426
508
|
if (title.toLowerCase().includes(term)) score += 3;
|
|
@@ -437,8 +519,8 @@ function scoreFiles(files, contentDir, query) {
|
|
|
437
519
|
}
|
|
438
520
|
async function handleSearchDocs(input) {
|
|
439
521
|
const { projectDir, query, limit = 5 } = input;
|
|
440
|
-
const contentDir =
|
|
441
|
-
if (!
|
|
522
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
523
|
+
if (!existsSync4(contentDir)) {
|
|
442
524
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
443
525
|
}
|
|
444
526
|
const files = [];
|
|
@@ -458,12 +540,12 @@ async function handleSearchDocs(input) {
|
|
|
458
540
|
}
|
|
459
541
|
|
|
460
542
|
// src/tools/semantic-search.ts
|
|
461
|
-
import { z as
|
|
462
|
-
var semanticSearchSchema =
|
|
463
|
-
siteUrl:
|
|
464
|
-
query:
|
|
465
|
-
limit:
|
|
466
|
-
mode:
|
|
543
|
+
import { z as z9 } from "zod";
|
|
544
|
+
var semanticSearchSchema = z9.object({
|
|
545
|
+
siteUrl: z9.string().describe("Base URL of the deployed Thally site (e.g. https://docs.example.com)"),
|
|
546
|
+
query: z9.string().describe("Natural-language search query"),
|
|
547
|
+
limit: z9.number().optional().default(8).describe("Max results to return (default 8)"),
|
|
548
|
+
mode: z9.enum(["hybrid", "fulltext"]).optional().default("hybrid").describe("Search mode: hybrid (full-text + vector) or fulltext")
|
|
467
549
|
});
|
|
468
550
|
async function handleSemanticSearch(input) {
|
|
469
551
|
const { siteUrl, query, limit = 8, mode = "hybrid" } = input;
|
|
@@ -494,10 +576,10 @@ async function handleSemanticSearch(input) {
|
|
|
494
576
|
}
|
|
495
577
|
|
|
496
578
|
// src/tools/agent-readiness.ts
|
|
497
|
-
import { z as
|
|
498
|
-
var agentReadinessSchema =
|
|
499
|
-
siteUrl:
|
|
500
|
-
minScore:
|
|
579
|
+
import { z as z10 } from "zod";
|
|
580
|
+
var agentReadinessSchema = z10.object({
|
|
581
|
+
siteUrl: z10.string().describe("Base URL of the deployed Thally site (e.g. https://docs.example.com)"),
|
|
582
|
+
minScore: z10.number().optional().describe("Optional threshold (0-100). If set, the summary flags whether the site passes.")
|
|
501
583
|
});
|
|
502
584
|
async function handleAgentReadiness(input) {
|
|
503
585
|
const { siteUrl, minScore } = input;
|
|
@@ -532,53 +614,211 @@ async function handleAgentReadiness(input) {
|
|
|
532
614
|
}
|
|
533
615
|
|
|
534
616
|
// src/tools/read-page.ts
|
|
535
|
-
import { z as
|
|
536
|
-
import { existsSync as
|
|
537
|
-
import { join as
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
617
|
+
import { z as z11 } from "zod";
|
|
618
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
|
|
619
|
+
import { isAbsolute as isAbsolute2, join as join6, relative as relative3, sep as sep2 } from "path";
|
|
620
|
+
|
|
621
|
+
// src/lib/text-window.ts
|
|
622
|
+
import { createHash as createHash2 } from "crypto";
|
|
623
|
+
import {
|
|
624
|
+
closeSync,
|
|
625
|
+
constants,
|
|
626
|
+
fstatSync,
|
|
627
|
+
openSync,
|
|
628
|
+
readFileSync as readFileSync5
|
|
629
|
+
} from "fs";
|
|
630
|
+
var MODEL_READ_WINDOW_DEFAULT_BYTES = 48 * 1024;
|
|
631
|
+
var MODEL_READ_WINDOW_MAX_BYTES = 180 * 1024;
|
|
632
|
+
var MODEL_READ_SOURCE_MAX_BYTES = 8 * 1024 * 1024;
|
|
633
|
+
function readModelTextFile(path) {
|
|
634
|
+
let descriptor;
|
|
635
|
+
try {
|
|
636
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
637
|
+
const metadata = fstatSync(descriptor);
|
|
638
|
+
if (!metadata.isFile() || metadata.size > MODEL_READ_SOURCE_MAX_BYTES) {
|
|
639
|
+
throw new Error("text_source_invalid");
|
|
640
|
+
}
|
|
641
|
+
const bytes = readFileSync5(descriptor);
|
|
642
|
+
if (bytes.byteLength !== metadata.size)
|
|
643
|
+
throw new Error("text_source_invalid");
|
|
644
|
+
return bytes;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error instanceof Error && error.message === "text_source_invalid")
|
|
647
|
+
throw error;
|
|
648
|
+
throw new Error("text_source_invalid");
|
|
649
|
+
} finally {
|
|
650
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function textWindowMetadata(window) {
|
|
654
|
+
const metadata = [
|
|
655
|
+
`content-sha256: ${window.sha256}`,
|
|
656
|
+
window.contentBytes === 0 ? `window-bytes: empty at ${window.startByte} of ${window.totalBytes}` : `window-bytes: ${window.startByte}-${window.startByte + window.contentBytes - 1} of ${window.totalBytes}`,
|
|
657
|
+
`window-lines: ${window.startLine}-${window.endLine} of ${window.totalLines}`,
|
|
658
|
+
`complete: ${window.isComplete}`
|
|
659
|
+
];
|
|
660
|
+
if (!window.isComplete) {
|
|
661
|
+
metadata.push(
|
|
662
|
+
`next-start-byte: ${window.nextStartByte}`,
|
|
663
|
+
`next-start-line: ${window.nextStartLine}`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return metadata;
|
|
667
|
+
}
|
|
668
|
+
function countLinesBefore(bytes, end) {
|
|
669
|
+
let lines = 1;
|
|
670
|
+
for (let index = 0; index < end; index += 1) {
|
|
671
|
+
if (bytes[index] === 10) lines += 1;
|
|
672
|
+
}
|
|
673
|
+
return lines;
|
|
674
|
+
}
|
|
675
|
+
function byteOffsetForLine(bytes, requestedLine) {
|
|
676
|
+
if (requestedLine === 1) return 0;
|
|
677
|
+
let line = 1;
|
|
678
|
+
for (let index = 0; index < bytes.byteLength; index += 1) {
|
|
679
|
+
if (bytes[index] !== 10) continue;
|
|
680
|
+
line += 1;
|
|
681
|
+
if (line === requestedLine) return index + 1;
|
|
682
|
+
}
|
|
683
|
+
throw new Error("text_window_start_out_of_range");
|
|
684
|
+
}
|
|
685
|
+
function decodeUtf8(bytes) {
|
|
686
|
+
try {
|
|
687
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error("text_window_invalid_utf8");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function isUtf8Boundary(bytes, offset) {
|
|
693
|
+
return offset === 0 || offset === bytes.byteLength || (bytes[offset] & 192) !== 128;
|
|
694
|
+
}
|
|
695
|
+
function createTextWindow(bytes, request = {}) {
|
|
696
|
+
decodeUtf8(bytes);
|
|
697
|
+
const maxBytes = request.maxBytes ?? MODEL_READ_WINDOW_DEFAULT_BYTES;
|
|
698
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MODEL_READ_WINDOW_MAX_BYTES) {
|
|
699
|
+
throw new Error("text_window_size_invalid");
|
|
700
|
+
}
|
|
701
|
+
if (request.startByte !== void 0 && request.startLine !== void 0) {
|
|
702
|
+
throw new Error("text_window_start_ambiguous");
|
|
703
|
+
}
|
|
704
|
+
let startByte;
|
|
705
|
+
if (request.startLine !== void 0) {
|
|
706
|
+
if (!Number.isSafeInteger(request.startLine) || request.startLine < 1) {
|
|
707
|
+
throw new Error("text_window_start_invalid");
|
|
708
|
+
}
|
|
709
|
+
startByte = byteOffsetForLine(bytes, request.startLine);
|
|
710
|
+
} else {
|
|
711
|
+
startByte = request.startByte ?? 0;
|
|
712
|
+
if (!Number.isSafeInteger(startByte) || startByte < 0 || startByte > bytes.byteLength || !isUtf8Boundary(bytes, startByte)) {
|
|
713
|
+
throw new Error("text_window_start_invalid");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
let endByte = Math.min(bytes.byteLength, startByte + maxBytes);
|
|
717
|
+
while (endByte > startByte && !isUtf8Boundary(bytes, endByte)) endByte -= 1;
|
|
718
|
+
if (endByte === startByte && startByte < bytes.byteLength) {
|
|
719
|
+
throw new Error("text_window_size_invalid");
|
|
720
|
+
}
|
|
721
|
+
const contentBytes = endByte - startByte;
|
|
722
|
+
const content = decodeUtf8(bytes.subarray(startByte, endByte));
|
|
723
|
+
const startLine = countLinesBefore(bytes, startByte);
|
|
724
|
+
const endLine = startLine + (content.match(/\n/g)?.length ?? 0);
|
|
725
|
+
const isComplete = endByte === bytes.byteLength;
|
|
726
|
+
const totalLines = countLinesBefore(bytes, bytes.byteLength);
|
|
727
|
+
return {
|
|
728
|
+
content,
|
|
729
|
+
contentBytes,
|
|
730
|
+
endLine,
|
|
731
|
+
isComplete,
|
|
732
|
+
...isComplete ? {} : { nextStartByte: endByte, nextStartLine: endLine },
|
|
733
|
+
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
734
|
+
startByte,
|
|
735
|
+
startLine,
|
|
736
|
+
totalBytes: bytes.byteLength,
|
|
737
|
+
totalLines
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function renderTextWindow(window) {
|
|
741
|
+
return [...textWindowMetadata(window), "", window.content].join("\n");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/tools/read-page.ts
|
|
745
|
+
var readPageSchema = z11.object({
|
|
746
|
+
projectDir: z11.string().describe("Path to the Thally project root"),
|
|
747
|
+
pageId: z11.string().describe('Page ID, e.g. "guides/authentication"'),
|
|
748
|
+
startByte: z11.number().int().min(0).optional().describe("UTF-8 byte continuation from a previous partial result"),
|
|
749
|
+
startLine: z11.number().int().min(1).optional().describe("1-based body line to start at; do not combine with startByte"),
|
|
750
|
+
maxBytes: z11.number().int().min(1).max(MODEL_READ_WINDOW_MAX_BYTES).optional().describe(
|
|
751
|
+
`Maximum body bytes to return (default 49152, maximum ${MODEL_READ_WINDOW_MAX_BYTES})`
|
|
752
|
+
)
|
|
541
753
|
});
|
|
754
|
+
function isInside2(root, candidate) {
|
|
755
|
+
const fromRoot = relative3(root, candidate);
|
|
756
|
+
return Boolean(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep2}`) && !isAbsolute2(fromRoot);
|
|
757
|
+
}
|
|
542
758
|
async function handleReadPage(input) {
|
|
543
759
|
const { projectDir, pageId } = input;
|
|
544
|
-
const contentDir =
|
|
760
|
+
const contentDir = join6(projectDir, "src", "content");
|
|
761
|
+
if (!/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/u.test(pageId) || pageId.split("/").some((part) => part.toLowerCase() === ".git")) {
|
|
762
|
+
throw new Error("Page ID must be a safe content-relative identifier");
|
|
763
|
+
}
|
|
764
|
+
const contentRoot = realpathSync2(contentDir);
|
|
545
765
|
const candidates = [
|
|
546
|
-
|
|
547
|
-
|
|
766
|
+
join6(contentDir, `${pageId}.mdx`),
|
|
767
|
+
join6(contentDir, `${pageId}/index.mdx`)
|
|
548
768
|
];
|
|
549
769
|
let filePath = null;
|
|
550
770
|
for (const c of candidates) {
|
|
551
|
-
if (
|
|
771
|
+
if (existsSync5(c)) {
|
|
772
|
+
const metadata = lstatSync2(c);
|
|
773
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || !isInside2(contentRoot, realpathSync2(c))) {
|
|
774
|
+
throw new Error("Page must be a regular file inside src/content");
|
|
775
|
+
}
|
|
552
776
|
filePath = c;
|
|
553
777
|
break;
|
|
554
778
|
}
|
|
555
779
|
}
|
|
556
780
|
if (!filePath) {
|
|
557
|
-
throw new Error(
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const rawBytes = readModelTextFile(filePath);
|
|
786
|
+
let raw;
|
|
787
|
+
try {
|
|
788
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(rawBytes);
|
|
789
|
+
} catch {
|
|
790
|
+
throw new Error("Page is not valid UTF-8");
|
|
558
791
|
}
|
|
559
|
-
const raw = readFileSync4(filePath, "utf8");
|
|
560
792
|
const { data, content } = parseFrontmatter(raw);
|
|
561
793
|
const title = data.title ?? pageId;
|
|
562
794
|
const description = data.description ?? "";
|
|
563
795
|
const lines = [`id: ${pageId}`, `title: ${title}`];
|
|
564
796
|
if (description) lines.push(`description: ${description}`);
|
|
565
|
-
|
|
797
|
+
const window = createTextWindow(Buffer.from(content, "utf8"), input);
|
|
798
|
+
lines.push(
|
|
799
|
+
"",
|
|
800
|
+
...textWindowMetadata(window),
|
|
801
|
+
"",
|
|
802
|
+
readPageBodyDelimiter(),
|
|
803
|
+
"",
|
|
804
|
+
window.content
|
|
805
|
+
);
|
|
566
806
|
return lines.join("\n");
|
|
567
807
|
}
|
|
568
808
|
|
|
569
809
|
// src/tools/get-context.ts
|
|
570
|
-
import { z as
|
|
571
|
-
import { existsSync as
|
|
572
|
-
import { join as
|
|
573
|
-
var getContextSchema =
|
|
574
|
-
projectDir:
|
|
575
|
-
topic:
|
|
576
|
-
maxTokens:
|
|
810
|
+
import { z as z12 } from "zod";
|
|
811
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
812
|
+
import { join as join7 } from "path";
|
|
813
|
+
var getContextSchema = z12.object({
|
|
814
|
+
projectDir: z12.string().describe("Path to the Thally project root"),
|
|
815
|
+
topic: z12.string().describe("Topic or question to find relevant docs for"),
|
|
816
|
+
maxTokens: z12.number().optional().default(4e3).describe("Approximate token budget for returned context (default 4000)")
|
|
577
817
|
});
|
|
578
818
|
async function handleGetContext(input) {
|
|
579
819
|
const { projectDir, topic, maxTokens = 4e3 } = input;
|
|
580
|
-
const contentDir =
|
|
581
|
-
if (!
|
|
820
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
821
|
+
if (!existsSync6(contentDir)) {
|
|
582
822
|
throw new Error(`Content directory not found: ${contentDir}`);
|
|
583
823
|
}
|
|
584
824
|
const files = [];
|
|
@@ -592,13 +832,13 @@ async function handleGetContext(input) {
|
|
|
592
832
|
const sections = [];
|
|
593
833
|
for (const result of scored) {
|
|
594
834
|
const candidates = [
|
|
595
|
-
|
|
596
|
-
|
|
835
|
+
join7(contentDir, `${result.pageId}.mdx`),
|
|
836
|
+
join7(contentDir, `${result.pageId}/index.mdx`)
|
|
597
837
|
];
|
|
598
838
|
let content = "";
|
|
599
839
|
for (const c of candidates) {
|
|
600
|
-
if (
|
|
601
|
-
const raw =
|
|
840
|
+
if (existsSync6(c)) {
|
|
841
|
+
const raw = readFileSync6(c, "utf8");
|
|
602
842
|
const { content: body } = parseFrontmatter(raw);
|
|
603
843
|
content = body.trim();
|
|
604
844
|
break;
|
|
@@ -622,12 +862,12 @@ async function handleGetContext(input) {
|
|
|
622
862
|
}
|
|
623
863
|
|
|
624
864
|
// src/tools/lint-project.ts
|
|
625
|
-
import { z as
|
|
626
|
-
import { existsSync as
|
|
627
|
-
import { join as
|
|
628
|
-
var lintProjectSchema =
|
|
629
|
-
projectDir:
|
|
630
|
-
fix:
|
|
865
|
+
import { z as z13 } from "zod";
|
|
866
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
867
|
+
import { join as join8 } from "path";
|
|
868
|
+
var lintProjectSchema = z13.object({
|
|
869
|
+
projectDir: z13.string().describe("Path to the Thally project root"),
|
|
870
|
+
fix: z13.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
|
|
631
871
|
});
|
|
632
872
|
function collectNavPageIds(groups, seen, duplicates) {
|
|
633
873
|
for (const page of groups) {
|
|
@@ -655,9 +895,9 @@ function addOrphanToNav(projectDir, pageId) {
|
|
|
655
895
|
}
|
|
656
896
|
async function handleLintProject(input) {
|
|
657
897
|
const { projectDir, fix = false } = input;
|
|
658
|
-
const contentDir =
|
|
898
|
+
const contentDir = join8(projectDir, "src", "content");
|
|
659
899
|
const issues = [];
|
|
660
|
-
if (!
|
|
900
|
+
if (!existsSync7(join8(projectDir, "docs.json"))) {
|
|
661
901
|
throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
|
|
662
902
|
}
|
|
663
903
|
const config = readDocsJson(projectDir);
|
|
@@ -676,10 +916,10 @@ async function handleLintProject(input) {
|
|
|
676
916
|
}
|
|
677
917
|
for (const pageId of navPageIds) {
|
|
678
918
|
const candidates = [
|
|
679
|
-
|
|
680
|
-
|
|
919
|
+
join8(contentDir, `${pageId}.mdx`),
|
|
920
|
+
join8(contentDir, `${pageId}/index.mdx`)
|
|
681
921
|
];
|
|
682
|
-
if (!candidates.some((c) =>
|
|
922
|
+
if (!candidates.some((c) => existsSync7(c))) {
|
|
683
923
|
issues.push({
|
|
684
924
|
severity: "error",
|
|
685
925
|
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
@@ -688,7 +928,7 @@ async function handleLintProject(input) {
|
|
|
688
928
|
}
|
|
689
929
|
}
|
|
690
930
|
const allFiles = [];
|
|
691
|
-
if (
|
|
931
|
+
if (existsSync7(contentDir)) {
|
|
692
932
|
scanMdxFiles(contentDir, allFiles);
|
|
693
933
|
}
|
|
694
934
|
const fixedOrphans = [];
|
|
@@ -706,7 +946,7 @@ async function handleLintProject(input) {
|
|
|
706
946
|
let data = {};
|
|
707
947
|
let content = "";
|
|
708
948
|
try {
|
|
709
|
-
const raw =
|
|
949
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
710
950
|
const parsed = parseFrontmatter(raw);
|
|
711
951
|
data = parsed.data;
|
|
712
952
|
content = parsed.content;
|
|
@@ -762,22 +1002,22 @@ async function handleLintProject(input) {
|
|
|
762
1002
|
}
|
|
763
1003
|
|
|
764
1004
|
// src/tools/translate-docs.ts
|
|
765
|
-
import { z as
|
|
766
|
-
import { readFileSync as
|
|
767
|
-
import { join as
|
|
1005
|
+
import { z as z14 } from "zod";
|
|
1006
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync2 } from "fs";
|
|
1007
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
768
1008
|
import Anthropic from "@anthropic-ai/sdk";
|
|
769
1009
|
import pLimit from "p-limit";
|
|
770
|
-
var translateDocsSchema =
|
|
771
|
-
projectDir:
|
|
772
|
-
locale:
|
|
773
|
-
pages:
|
|
774
|
-
force:
|
|
775
|
-
apiKey:
|
|
776
|
-
model:
|
|
1010
|
+
var translateDocsSchema = z14.object({
|
|
1011
|
+
projectDir: z14.string().describe("Path to the Thally project directory"),
|
|
1012
|
+
locale: z14.string().describe('Target locale code, e.g. "es", "fr"'),
|
|
1013
|
+
pages: z14.array(z14.string()).optional().describe("Page IDs to translate (omit for all pages)"),
|
|
1014
|
+
force: z14.boolean().optional().default(false).describe("Overwrite existing translation files"),
|
|
1015
|
+
apiKey: z14.string().optional().describe("Anthropic API key (falls back to ANTHROPIC_API_KEY env var)"),
|
|
1016
|
+
model: z14.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
|
|
777
1017
|
});
|
|
778
1018
|
function readDocsJson2(projectDir) {
|
|
779
|
-
const docsPath =
|
|
780
|
-
const raw =
|
|
1019
|
+
const docsPath = join9(projectDir, "docs.json");
|
|
1020
|
+
const raw = readFileSync8(docsPath, "utf8");
|
|
781
1021
|
return JSON.parse(raw);
|
|
782
1022
|
}
|
|
783
1023
|
function collectPageIds(pages) {
|
|
@@ -819,12 +1059,12 @@ function getAllPageIds(config) {
|
|
|
819
1059
|
return { ids, hrefOnlyPages };
|
|
820
1060
|
}
|
|
821
1061
|
function findSourceFile(projectDir, pageId) {
|
|
822
|
-
const contentRoot =
|
|
1062
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
823
1063
|
const candidates = [
|
|
824
|
-
|
|
825
|
-
|
|
1064
|
+
join9(contentRoot, `${pageId}.mdx`),
|
|
1065
|
+
join9(contentRoot, `${pageId}/index.mdx`)
|
|
826
1066
|
];
|
|
827
|
-
return candidates.find((p) =>
|
|
1067
|
+
return candidates.find((p) => existsSync8(p)) ?? null;
|
|
828
1068
|
}
|
|
829
1069
|
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
830
1070
|
|
|
@@ -876,7 +1116,7 @@ async function handleTranslateDocs(input) {
|
|
|
876
1116
|
}
|
|
877
1117
|
const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
|
|
878
1118
|
const targetPageIds = pages ?? allPageIds;
|
|
879
|
-
const contentRoot =
|
|
1119
|
+
const contentRoot = join9(projectDir, "src", "content");
|
|
880
1120
|
const toTranslate = [];
|
|
881
1121
|
const skipped = [];
|
|
882
1122
|
for (const pageId of targetPageIds) {
|
|
@@ -886,8 +1126,8 @@ async function handleTranslateDocs(input) {
|
|
|
886
1126
|
continue;
|
|
887
1127
|
}
|
|
888
1128
|
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
889
|
-
const targetFile =
|
|
890
|
-
if (
|
|
1129
|
+
const targetFile = join9(contentRoot, locale, relativeFromContent);
|
|
1130
|
+
if (existsSync8(targetFile) && !force) {
|
|
891
1131
|
skipped.push(`${pageId} (already translated)`);
|
|
892
1132
|
continue;
|
|
893
1133
|
}
|
|
@@ -903,14 +1143,14 @@ async function handleTranslateDocs(input) {
|
|
|
903
1143
|
toTranslate.map(
|
|
904
1144
|
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
905
1145
|
try {
|
|
906
|
-
const sourceContent =
|
|
1146
|
+
const sourceContent = readFileSync8(sourceFile, "utf8");
|
|
907
1147
|
const parsed = parseFrontmatter(sourceContent);
|
|
908
1148
|
if (!parsed.data.title) {
|
|
909
1149
|
console.warn(`[translate] ${pageId}: missing title in frontmatter`);
|
|
910
1150
|
}
|
|
911
1151
|
const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
|
|
912
1152
|
mkdirSync2(dirname2(targetFile), { recursive: true });
|
|
913
|
-
|
|
1153
|
+
writeFileSync6(targetFile, translated + "\n", "utf8");
|
|
914
1154
|
results.push({ pageId, success: true });
|
|
915
1155
|
} catch (err) {
|
|
916
1156
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -950,19 +1190,30 @@ async function handleTranslateDocs(input) {
|
|
|
950
1190
|
}
|
|
951
1191
|
|
|
952
1192
|
// src/tools/sync-from-repo.ts
|
|
953
|
-
import { z as
|
|
1193
|
+
import { z as z15 } from "zod";
|
|
954
1194
|
|
|
955
1195
|
// src/lib/track.ts
|
|
956
|
-
import { createSign, createHash } from "crypto";
|
|
1196
|
+
import { createSign, createHash as createHash3 } from "crypto";
|
|
957
1197
|
function parseOwnerRepo(spec) {
|
|
958
1198
|
const trimmed = spec.trim();
|
|
959
1199
|
const url = trimmed.match(
|
|
960
1200
|
/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:\/pull\/(\d+))?(?:[/#?].*)?$/i
|
|
961
1201
|
);
|
|
962
|
-
if (url)
|
|
963
|
-
|
|
1202
|
+
if (url)
|
|
1203
|
+
return {
|
|
1204
|
+
owner: url[1],
|
|
1205
|
+
repo: url[2],
|
|
1206
|
+
...url[3] ? { pr: Number(url[3]) } : {}
|
|
1207
|
+
};
|
|
1208
|
+
const plain = trimmed.match(
|
|
1209
|
+
/^([A-Za-z0-9-_.]+)\/([A-Za-z0-9-_.]+?)(?:#(\d+))?$/
|
|
1210
|
+
);
|
|
964
1211
|
if (!plain) return null;
|
|
965
|
-
return {
|
|
1212
|
+
return {
|
|
1213
|
+
owner: plain[1],
|
|
1214
|
+
repo: plain[2],
|
|
1215
|
+
...plain[3] ? { pr: Number(plain[3]) } : {}
|
|
1216
|
+
};
|
|
966
1217
|
}
|
|
967
1218
|
function base64url(input) {
|
|
968
1219
|
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -971,22 +1222,34 @@ var installationTokenCache = /* @__PURE__ */ new Map();
|
|
|
971
1222
|
function createAppJwt(appId, privateKey) {
|
|
972
1223
|
const now = Math.floor(Date.now() / 1e3);
|
|
973
1224
|
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
974
|
-
const payload = base64url(
|
|
975
|
-
|
|
1225
|
+
const payload = base64url(
|
|
1226
|
+
JSON.stringify({ iat: now - 60, exp: now + 9 * 60, iss: String(appId) })
|
|
1227
|
+
);
|
|
1228
|
+
const signature = base64url(
|
|
1229
|
+
createSign("RSA-SHA256").update(`${header}.${payload}`).sign(privateKey)
|
|
1230
|
+
);
|
|
976
1231
|
return `${header}.${payload}.${signature}`;
|
|
977
1232
|
}
|
|
978
1233
|
async function mintInstallationToken(creds, fetchImpl = fetch) {
|
|
979
|
-
const keyFp =
|
|
1234
|
+
const keyFp = createHash3("sha256").update(creds.privateKey).digest("hex").slice(0, 12);
|
|
980
1235
|
const cacheKey = `${creds.appId}:${creds.installationId}:${keyFp}`;
|
|
981
1236
|
const cached = installationTokenCache.get(cacheKey);
|
|
982
1237
|
if (cached && cached.expiresAtMs - 6e4 > Date.now()) return cached.token;
|
|
983
1238
|
const jwt = createAppJwt(creds.appId, creds.privateKey);
|
|
984
|
-
const res = await fetchImpl(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1239
|
+
const res = await fetchImpl(
|
|
1240
|
+
`https://api.github.com/app/installations/${creds.installationId}/access_tokens`,
|
|
1241
|
+
{
|
|
1242
|
+
method: "POST",
|
|
1243
|
+
headers: {
|
|
1244
|
+
Accept: "application/vnd.github+json",
|
|
1245
|
+
Authorization: `Bearer ${jwt}`
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
);
|
|
988
1249
|
if (!res.ok) {
|
|
989
|
-
throw new Error(
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
`GitHub App token exchange failed (${res.status}) \u2014 check the app id, installation id, and private key.`
|
|
1252
|
+
);
|
|
990
1253
|
}
|
|
991
1254
|
const body = await res.json();
|
|
992
1255
|
const parsed = Date.parse(body.expires_at);
|
|
@@ -998,7 +1261,8 @@ function envAppCreds() {
|
|
|
998
1261
|
const appId = (process.env.THALLY_GITHUB_APP_ID ?? process.env.DOX_GITHUB_APP_ID)?.trim();
|
|
999
1262
|
const installationId = (process.env.THALLY_GITHUB_APP_INSTALLATION_ID ?? process.env.DOX_GITHUB_APP_INSTALLATION_ID)?.trim();
|
|
1000
1263
|
const privateKey = process.env.THALLY_GITHUB_APP_PRIVATE_KEY ?? process.env.DOX_GITHUB_APP_PRIVATE_KEY;
|
|
1001
|
-
if (appId && installationId && privateKey)
|
|
1264
|
+
if (appId && installationId && privateKey)
|
|
1265
|
+
return { appId, installationId, privateKey };
|
|
1002
1266
|
return void 0;
|
|
1003
1267
|
}
|
|
1004
1268
|
async function resolveGithubToken(options) {
|
|
@@ -1009,7 +1273,9 @@ async function resolveGithubToken(options) {
|
|
|
1009
1273
|
try {
|
|
1010
1274
|
return await mintInstallationToken(appCreds, options?.fetchImpl ?? fetch);
|
|
1011
1275
|
} catch (err) {
|
|
1012
|
-
console.warn(
|
|
1276
|
+
console.warn(
|
|
1277
|
+
`[thally-track] GitHub App token mint failed, falling back to PAT: ${err instanceof Error ? err.message : String(err)}`
|
|
1278
|
+
);
|
|
1013
1279
|
return pat;
|
|
1014
1280
|
}
|
|
1015
1281
|
}
|
|
@@ -1018,9 +1284,13 @@ async function resolveGithubToken(options) {
|
|
|
1018
1284
|
async function githubJson(path, options) {
|
|
1019
1285
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
1020
1286
|
const token = await resolveGithubToken(options);
|
|
1021
|
-
const headers = {
|
|
1287
|
+
const headers = {
|
|
1288
|
+
Accept: "application/vnd.github+json"
|
|
1289
|
+
};
|
|
1022
1290
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1023
|
-
const response = await fetchImpl(`https://api.github.com${path}`, {
|
|
1291
|
+
const response = await fetchImpl(`https://api.github.com${path}`, {
|
|
1292
|
+
headers
|
|
1293
|
+
});
|
|
1024
1294
|
if (!response.ok) {
|
|
1025
1295
|
const hint = response.status === 404 || response.status === 403 ? " (private repo or rate limit? set THALLY_GITHUB_TOKEN or connect a GitHub App)" : "";
|
|
1026
1296
|
throw new Error(`GitHub API ${response.status} for ${path}${hint}`);
|
|
@@ -1039,7 +1309,10 @@ function toPullRequestInfo(raw) {
|
|
|
1039
1309
|
};
|
|
1040
1310
|
}
|
|
1041
1311
|
async function fetchPullRequest(owner, repo, number, options) {
|
|
1042
|
-
const raw = await githubJson(
|
|
1312
|
+
const raw = await githubJson(
|
|
1313
|
+
`/repos/${owner}/${repo}/pulls/${number}`,
|
|
1314
|
+
options
|
|
1315
|
+
);
|
|
1043
1316
|
return toPullRequestInfo(raw);
|
|
1044
1317
|
}
|
|
1045
1318
|
async function fetchPullRequestFiles(owner, repo, number, options) {
|
|
@@ -1047,7 +1320,10 @@ async function fetchPullRequestFiles(owner, repo, number, options) {
|
|
|
1047
1320
|
const maxPages = 30;
|
|
1048
1321
|
const raw = [];
|
|
1049
1322
|
for (let page = 1; page <= maxPages; page++) {
|
|
1050
|
-
const chunk = await githubJson(
|
|
1323
|
+
const chunk = await githubJson(
|
|
1324
|
+
`/repos/${owner}/${repo}/pulls/${number}/files?per_page=${perPage}&page=${page}`,
|
|
1325
|
+
options
|
|
1326
|
+
);
|
|
1051
1327
|
raw.push(...chunk);
|
|
1052
1328
|
if (chunk.length < perPage) break;
|
|
1053
1329
|
}
|
|
@@ -1064,7 +1340,9 @@ async function fetchLatestMergedPr(owner, repo, base, options) {
|
|
|
1064
1340
|
`/repos/${owner}/${repo}/pulls?state=closed&base=${encodeURIComponent(base)}&sort=updated&direction=desc&per_page=30`,
|
|
1065
1341
|
options
|
|
1066
1342
|
);
|
|
1067
|
-
const merged = raw.filter((pr) => Boolean(pr.merged_at)).sort(
|
|
1343
|
+
const merged = raw.filter((pr) => Boolean(pr.merged_at)).sort(
|
|
1344
|
+
(a, b) => Date.parse(b.merged_at ?? "") - Date.parse(a.merged_at ?? "")
|
|
1345
|
+
)[0];
|
|
1068
1346
|
return merged ? toPullRequestInfo(merged) : null;
|
|
1069
1347
|
}
|
|
1070
1348
|
function compileGlob(pattern) {
|
|
@@ -1137,16 +1415,19 @@ _(diff truncated \u2014 ${files.length} file(s) total)_
|
|
|
1137
1415
|
return context.slice(0, TRACK_CONTEXT_CHAR_CAP);
|
|
1138
1416
|
}
|
|
1139
1417
|
function buildTrackTask(repo, pr, files) {
|
|
1140
|
-
return {
|
|
1418
|
+
return {
|
|
1419
|
+
instruction: buildTrackInstruction(repo, pr),
|
|
1420
|
+
context: buildTrackContext(repo, pr, files)
|
|
1421
|
+
};
|
|
1141
1422
|
}
|
|
1142
1423
|
|
|
1143
1424
|
// src/tools/sync-from-repo.ts
|
|
1144
|
-
var syncFromRepoSchema =
|
|
1145
|
-
projectDir:
|
|
1146
|
-
repo:
|
|
1147
|
-
pr:
|
|
1148
|
-
dryRun:
|
|
1149
|
-
docsRepo:
|
|
1425
|
+
var syncFromRepoSchema = z15.object({
|
|
1426
|
+
projectDir: z15.string().describe("Path to the Thally project (reads the tracking config from docs.json)"),
|
|
1427
|
+
repo: z15.string().optional().describe("Tracked repo to sync as owner/repo (defaults to the single tracked repo when only one is configured)"),
|
|
1428
|
+
pr: z15.number().optional().describe("Pull request number to analyze (defaults to the latest PR merged into the tracked base branch)"),
|
|
1429
|
+
dryRun: z15.boolean().optional().default(true).describe("When true (default), preview the distilled docs task without dispatching anything"),
|
|
1430
|
+
docsRepo: z15.string().optional().describe("owner/repo of the docs repository to dispatch the task to (required when dryRun is false)")
|
|
1150
1431
|
});
|
|
1151
1432
|
async function handleSyncFromRepo(input) {
|
|
1152
1433
|
const config = readDocsJson(input.projectDir);
|
|
@@ -1218,12 +1499,11 @@ async function handleSyncFromRepo(input) {
|
|
|
1218
1499
|
}
|
|
1219
1500
|
|
|
1220
1501
|
// src/tools/read-api-spec.ts
|
|
1221
|
-
import {
|
|
1222
|
-
import { z as z15 } from "zod";
|
|
1502
|
+
import { z as z16 } from "zod";
|
|
1223
1503
|
|
|
1224
1504
|
// src/lib/api-spec.ts
|
|
1225
|
-
import { existsSync as
|
|
1226
|
-
import { isAbsolute, join as
|
|
1505
|
+
import { existsSync as existsSync9, lstatSync as lstatSync3, realpathSync as realpathSync3 } from "fs";
|
|
1506
|
+
import { isAbsolute as isAbsolute3, join as join10, posix, relative as relative4, resolve, sep as sep3 } from "path";
|
|
1227
1507
|
var CONVENTIONAL_API_SOURCES = ["openapi.yaml", "openapi.yml", "openapi.json"];
|
|
1228
1508
|
function normalizeApiSource(source) {
|
|
1229
1509
|
const normalized = posix.normalize(source.trim().replace(/^\/+/, ""));
|
|
@@ -1235,7 +1515,7 @@ function normalizeApiSource(source) {
|
|
|
1235
1515
|
function configuredApiSources(projectDir) {
|
|
1236
1516
|
const configured = readDocsJson(projectDir).tabs.map((tab) => tab.api?.source).filter((source) => typeof source === "string").map(normalizeApiSource).filter((source) => source !== null);
|
|
1237
1517
|
const conventional = CONVENTIONAL_API_SOURCES.filter(
|
|
1238
|
-
(source) =>
|
|
1518
|
+
(source) => existsSync9(join10(projectDir, source))
|
|
1239
1519
|
);
|
|
1240
1520
|
return [.../* @__PURE__ */ new Set([...configured, ...conventional])];
|
|
1241
1521
|
}
|
|
@@ -1259,12 +1539,12 @@ function resolveApiSource(projectDir, requested) {
|
|
|
1259
1539
|
}
|
|
1260
1540
|
function resolveApiSourcePath(projectDir, requested) {
|
|
1261
1541
|
const source = resolveApiSource(projectDir, requested);
|
|
1262
|
-
const projectRoot =
|
|
1542
|
+
const projectRoot = realpathSync3(projectDir);
|
|
1263
1543
|
const candidate = resolve(projectRoot, source);
|
|
1264
1544
|
try {
|
|
1265
|
-
const resolved =
|
|
1266
|
-
const fromRoot =
|
|
1267
|
-
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${
|
|
1545
|
+
const resolved = realpathSync3(candidate);
|
|
1546
|
+
const fromRoot = relative4(projectRoot, resolved);
|
|
1547
|
+
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${sep3}`) || isAbsolute3(fromRoot) || !lstatSync3(candidate).isFile() || lstatSync3(candidate).isSymbolicLink()) {
|
|
1268
1548
|
throw new Error("unsafe");
|
|
1269
1549
|
}
|
|
1270
1550
|
return { source, path: resolved };
|
|
@@ -1274,24 +1554,33 @@ function resolveApiSourcePath(projectDir, requested) {
|
|
|
1274
1554
|
}
|
|
1275
1555
|
|
|
1276
1556
|
// src/tools/read-api-spec.ts
|
|
1277
|
-
var readApiSpecSchema =
|
|
1278
|
-
projectDir:
|
|
1279
|
-
source:
|
|
1557
|
+
var readApiSpecSchema = z16.object({
|
|
1558
|
+
projectDir: z16.string().describe("Path to the Thally project root"),
|
|
1559
|
+
source: z16.string().optional().describe("Configured OpenAPI source; omit when the project has one"),
|
|
1560
|
+
startByte: z16.number().int().min(0).optional().describe("UTF-8 byte continuation from a previous partial result"),
|
|
1561
|
+
startLine: z16.number().int().min(1).optional().describe("1-based source line to start at; do not combine with startByte"),
|
|
1562
|
+
maxBytes: z16.number().int().min(1).max(MODEL_READ_WINDOW_MAX_BYTES).optional().describe(
|
|
1563
|
+
`Maximum source bytes to return (default 49152, maximum ${MODEL_READ_WINDOW_MAX_BYTES})`
|
|
1564
|
+
)
|
|
1280
1565
|
});
|
|
1281
1566
|
async function handleReadApiSpec(input) {
|
|
1282
1567
|
const source = resolveApiSourcePath(input.projectDir, input.source);
|
|
1283
|
-
|
|
1568
|
+
const window = createTextWindow(readModelTextFile(source.path), input);
|
|
1569
|
+
return [
|
|
1570
|
+
`API source: ${source.source}`,
|
|
1571
|
+
...renderTextWindow(window).split("\n")
|
|
1572
|
+
].join("\n");
|
|
1284
1573
|
}
|
|
1285
1574
|
|
|
1286
1575
|
// src/tools/update-api-spec.ts
|
|
1287
|
-
import { writeFileSync as
|
|
1576
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
1288
1577
|
import { extname as extname2 } from "path";
|
|
1289
|
-
import { z as
|
|
1578
|
+
import { z as z17 } from "zod";
|
|
1290
1579
|
import { parse as parseYaml } from "yaml";
|
|
1291
|
-
var updateApiSpecSchema =
|
|
1292
|
-
projectDir:
|
|
1293
|
-
source:
|
|
1294
|
-
content:
|
|
1580
|
+
var updateApiSpecSchema = z17.object({
|
|
1581
|
+
projectDir: z17.string().describe("Path to the Thally project root"),
|
|
1582
|
+
source: z17.string().optional().describe("Configured OpenAPI source; omit when the project has one"),
|
|
1583
|
+
content: z17.string().min(2).max(512e3).describe("Complete replacement OpenAPI JSON or YAML document")
|
|
1295
1584
|
});
|
|
1296
1585
|
function parseDocument(source, content) {
|
|
1297
1586
|
try {
|
|
@@ -1310,7 +1599,7 @@ async function handleUpdateApiSpec(input) {
|
|
|
1310
1599
|
}
|
|
1311
1600
|
const content = input.content.endsWith("\n") ? input.content : `${input.content}
|
|
1312
1601
|
`;
|
|
1313
|
-
|
|
1602
|
+
writeFileSync7(source.path, content, "utf8");
|
|
1314
1603
|
return `\u2705 API source updated: ${source.source}`;
|
|
1315
1604
|
}
|
|
1316
1605
|
|
|
@@ -1354,9 +1643,16 @@ var tools = [
|
|
|
1354
1643
|
schema: updatePageSchema,
|
|
1355
1644
|
handler: handleUpdatePage
|
|
1356
1645
|
}),
|
|
1646
|
+
defineTool({
|
|
1647
|
+
name: "replace_page_text",
|
|
1648
|
+
description: "Replace one exact unique span in an existing MDX page; prefer this for small edits so the full page never travels through the model response",
|
|
1649
|
+
scope: "project",
|
|
1650
|
+
schema: replacePageTextSchema,
|
|
1651
|
+
handler: handleReplacePageText
|
|
1652
|
+
}),
|
|
1357
1653
|
defineTool({
|
|
1358
1654
|
name: "read_api_spec",
|
|
1359
|
-
description: "Read an OpenAPI JSON or YAML source
|
|
1655
|
+
description: "Read a bounded UTF-8 window from an OpenAPI JSON or YAML source configured by this project; follow next-start-byte until complete",
|
|
1360
1656
|
scope: "project",
|
|
1361
1657
|
schema: readApiSpecSchema,
|
|
1362
1658
|
handler: handleReadApiSpec
|
|
@@ -1405,7 +1701,7 @@ var tools = [
|
|
|
1405
1701
|
}),
|
|
1406
1702
|
defineTool({
|
|
1407
1703
|
name: "read_page",
|
|
1408
|
-
description: "Read
|
|
1704
|
+
description: "Read a bounded UTF-8 body window from a documentation page; follow next-start-byte until complete before replacing a page",
|
|
1409
1705
|
scope: "project",
|
|
1410
1706
|
schema: readPageSchema,
|
|
1411
1707
|
handler: handleReadPage
|