@farming-labs/docs 0.1.123 → 0.1.124
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/{agent-cmkIPvWo.mjs → agent-BtZrMCt8.mjs} +3 -3
- package/dist/agents-BPX4YRPs.mjs +209 -0
- package/dist/cli/index.mjs +31 -16
- package/dist/codeblocks-RacO12_K.mjs +1494 -0
- package/dist/{config-BHRL4R2v.mjs → config-B8wA38OC.mjs} +2 -2
- package/dist/{dev-C03tUSTz.mjs → dev-1VFeAWUi.mjs} +2 -2
- package/dist/{doctor-C9wz-4CE.mjs → doctor-B0nFg8oM.mjs} +3 -3
- package/dist/{downgrade-Bv7E5LV2.mjs → downgrade-BGANzvQb.mjs} +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/{init-_HAuo5Dv.mjs → init-DzQWiVN1.mjs} +1 -1
- package/dist/{mcp-veym29Yc.mjs → mcp-BvERlrP_.mjs} +1 -1
- package/dist/mcp.d.mts +1 -1
- package/dist/mcp.mjs +68 -7
- package/dist/{reading-time-Io7iRZ7S.mjs → reading-time-HpTq-0VB.mjs} +1 -0
- package/dist/{review-DEG_UnxV.mjs → review-ClNDYRnn.mjs} +1 -1
- package/dist/{robots-BkRZN-56.mjs → robots-CNLiZCay.mjs} +1 -1
- package/dist/{search-HqdPzNb3.mjs → search-BjyDm30M.mjs} +1 -1
- package/dist/{search-XmPPiA0V.d.mts → search-mF8wT9LQ.d.mts} +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/{sitemap-wtr3w_D2.mjs → sitemap-BdSdcAmS.mjs} +1 -1
- package/dist/{types-CLofDwjw.d.mts → types-hDUu4K7W.d.mts} +133 -1
- package/dist/{upgrade-DrOWQIKI.mjs → upgrade-C7DvvfTR.mjs} +1 -1
- package/package.json +2 -1
- package/dist/agents-B3kj54S3.mjs +0 -795
- /package/dist/{package-version-L4GZowaF.mjs → package-version-OrjpmqoR.mjs} +0 -0
- /package/dist/{templates-CakZBXK8.mjs → templates-CGaORZOY.mjs} +0 -0
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
import { _ as parseGeneratedAgentDocument, h as hashGeneratedAgentContent, m as GENERATED_AGENT_PROVENANCE_VERSION, v as serializeGeneratedAgentDocument } from "./search-BL7o2rXk.mjs";
|
|
2
|
+
import { E as findDocsMarkdownPage, U as renderDocsMarkdownDocument } from "./agent-BS39vnhS.mjs";
|
|
3
|
+
import "./index.mjs";
|
|
4
|
+
import { createFilesystemDocsMcpSource } from "./mcp.mjs";
|
|
5
|
+
import "./server.mjs";
|
|
6
|
+
import { a as loadProjectEnv, c as readNavTitle, d as readTopLevelStringProperty, f as resolveDocsConfigPath, i as loadDocsConfigModule, l as readNumberProperty, o as readBooleanProperty, p as resolveDocsContentDir, s as readEnvReferenceProperty, t as extractNestedObjectLiteral, u as readStringProperty } from "./config-B8wA38OC.mjs";
|
|
7
|
+
import matter from "gray-matter";
|
|
8
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
|
|
15
|
+
//#region src/cli/agent.ts
|
|
16
|
+
const DEFAULT_TTC_BASE_URL = "https://api.thetokencompany.com";
|
|
17
|
+
const DEFAULT_TTC_MODEL = "bear-1.2";
|
|
18
|
+
const DEFAULT_TTC_AGGRESSIVENESS = .3;
|
|
19
|
+
const INDEX_PAGE_BASENAMES = new Set([
|
|
20
|
+
"index",
|
|
21
|
+
"page",
|
|
22
|
+
"+page"
|
|
23
|
+
]);
|
|
24
|
+
function parseBooleanFlag(raw) {
|
|
25
|
+
if (raw === "true") return true;
|
|
26
|
+
if (raw === "false") return false;
|
|
27
|
+
throw new Error(`Invalid boolean value: ${raw}. Use true or false.`);
|
|
28
|
+
}
|
|
29
|
+
function parseIntegerFlag(raw, name) {
|
|
30
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid ${name}: ${raw}.`);
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
function parseFloatFlag(raw, name) {
|
|
35
|
+
const parsed = Number.parseFloat(raw);
|
|
36
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid ${name}: ${raw}.`);
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
function parseAgentCompactArgs(argv) {
|
|
40
|
+
const parsed = { pages: [] };
|
|
41
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
42
|
+
const arg = argv[index];
|
|
43
|
+
if (arg === "--help" || arg === "-h") {
|
|
44
|
+
parsed.help = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg === "--all") {
|
|
48
|
+
parsed.all = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === "--dry-run") {
|
|
52
|
+
parsed.dryRun = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--changed") {
|
|
56
|
+
parsed.changed = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === "--stale") {
|
|
60
|
+
parsed.stale = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === "--include-missing") {
|
|
64
|
+
parsed.includeMissing = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === "--protect-json") {
|
|
68
|
+
const nextValue = argv[index + 1];
|
|
69
|
+
if (nextValue && !nextValue.startsWith("--")) {
|
|
70
|
+
parsed.protectJson = parseBooleanFlag(nextValue);
|
|
71
|
+
index += 1;
|
|
72
|
+
} else parsed.protectJson = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!arg.startsWith("--")) {
|
|
76
|
+
parsed.pages.push(arg);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
80
|
+
const key = rawKey.trim();
|
|
81
|
+
const hasInlineValue = inlineValue !== void 0;
|
|
82
|
+
const nextValue = !hasInlineValue ? argv[index + 1] : void 0;
|
|
83
|
+
const consumeNextValue = () => {
|
|
84
|
+
if (!nextValue || nextValue.startsWith("--")) throw new Error(`Missing value for --${key}.`);
|
|
85
|
+
index += 1;
|
|
86
|
+
return nextValue;
|
|
87
|
+
};
|
|
88
|
+
const value = hasInlineValue ? inlineValue : consumeNextValue();
|
|
89
|
+
switch (key) {
|
|
90
|
+
case "page":
|
|
91
|
+
parsed.pages.push(value);
|
|
92
|
+
break;
|
|
93
|
+
case "config":
|
|
94
|
+
parsed.configPath = value;
|
|
95
|
+
break;
|
|
96
|
+
case "api-key":
|
|
97
|
+
parsed.apiKey = value;
|
|
98
|
+
break;
|
|
99
|
+
case "base-url":
|
|
100
|
+
parsed.baseUrl = value;
|
|
101
|
+
break;
|
|
102
|
+
case "api-key-env":
|
|
103
|
+
parsed.apiKeyEnv = value;
|
|
104
|
+
break;
|
|
105
|
+
case "model":
|
|
106
|
+
parsed.model = value;
|
|
107
|
+
break;
|
|
108
|
+
case "aggressiveness":
|
|
109
|
+
parsed.aggressiveness = parseFloatFlag(value, "aggressiveness");
|
|
110
|
+
break;
|
|
111
|
+
case "max-output-tokens":
|
|
112
|
+
parsed.maxOutputTokens = parseIntegerFlag(value, "max-output-tokens");
|
|
113
|
+
break;
|
|
114
|
+
case "min-output-tokens":
|
|
115
|
+
parsed.minOutputTokens = parseIntegerFlag(value, "min-output-tokens");
|
|
116
|
+
break;
|
|
117
|
+
case "protect-json":
|
|
118
|
+
parsed.protectJson = parseBooleanFlag(value);
|
|
119
|
+
break;
|
|
120
|
+
default: throw new Error(`Unknown agent compact flag: --${key}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
function normalizePathSegment(value) {
|
|
126
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
127
|
+
}
|
|
128
|
+
function normalizeUrlPath(value) {
|
|
129
|
+
const normalized = value.replace(/\/+/g, "/");
|
|
130
|
+
if (normalized === "/") return normalized;
|
|
131
|
+
return normalized.replace(/\/+$/, "");
|
|
132
|
+
}
|
|
133
|
+
function normalizeRequestedPage(entry, rawValue) {
|
|
134
|
+
const trimmed = rawValue.trim();
|
|
135
|
+
if (!trimmed || trimmed === ".") return `/${normalizePathSegment(entry) || "docs"}`;
|
|
136
|
+
if (/^https?:\/\//i.test(trimmed)) try {
|
|
137
|
+
return normalizeUrlPath(new URL(trimmed).pathname.replace(/\.md$/i, ""));
|
|
138
|
+
} catch {
|
|
139
|
+
return trimmed;
|
|
140
|
+
}
|
|
141
|
+
const withoutMarkdownSuffix = trimmed.replace(/\.md$/i, "");
|
|
142
|
+
const normalizedEntry = `/${normalizePathSegment(entry) || "docs"}`;
|
|
143
|
+
const normalized = normalizeUrlPath(withoutMarkdownSuffix.startsWith("/") ? withoutMarkdownSuffix : `/${withoutMarkdownSuffix}`);
|
|
144
|
+
if (normalized === normalizedEntry || normalized.startsWith(`${normalizedEntry}/`)) return normalized;
|
|
145
|
+
const slug = normalizePathSegment(withoutMarkdownSuffix);
|
|
146
|
+
return slug ? normalizeUrlPath(`${normalizedEntry}/${slug}`) : normalizedEntry;
|
|
147
|
+
}
|
|
148
|
+
function scanDocsPageTargets(rootDir, contentDir, entry) {
|
|
149
|
+
const contentDirAbs = path.resolve(rootDir, contentDir);
|
|
150
|
+
const targets = [];
|
|
151
|
+
const visit = (dir, slugParts) => {
|
|
152
|
+
if (!existsSync(dir)) return;
|
|
153
|
+
for (const name of readdirSync(dir).sort()) {
|
|
154
|
+
const fullPath = path.join(dir, name);
|
|
155
|
+
if (statSync(fullPath).isDirectory()) {
|
|
156
|
+
visit(fullPath, [...slugParts, name]);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (name === "agent.md") continue;
|
|
160
|
+
if (!name.endsWith(".md") && !name.endsWith(".mdx") && !name.endsWith(".svx")) continue;
|
|
161
|
+
const baseName = name.replace(/\.(md|mdx|svx)$/i, "");
|
|
162
|
+
if (!INDEX_PAGE_BASENAMES.has(baseName)) continue;
|
|
163
|
+
const slug = slugParts.join("/");
|
|
164
|
+
const url = slug ? `/${normalizePathSegment(entry)}/${slug}` : `/${normalizePathSegment(entry)}`;
|
|
165
|
+
const agentPath = path.join(dir, "agent.md");
|
|
166
|
+
targets.push({
|
|
167
|
+
slug,
|
|
168
|
+
url,
|
|
169
|
+
pagePath: fullPath,
|
|
170
|
+
pageDir: dir,
|
|
171
|
+
agentPath,
|
|
172
|
+
hasAgentFile: existsSync(agentPath)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
visit(contentDirAbs, []);
|
|
177
|
+
return targets;
|
|
178
|
+
}
|
|
179
|
+
function resolveCompressionEndpoint(rawBaseUrl) {
|
|
180
|
+
const baseUrl = rawBaseUrl ?? process.env.TOKEN_COMPANY_BASE_URL ?? process.env.THE_TOKEN_COMPANY_BASE_URL ?? DEFAULT_TTC_BASE_URL;
|
|
181
|
+
if (/\/v1\/compress\/?$/i.test(baseUrl)) return baseUrl.replace(/\/+$/, "");
|
|
182
|
+
return `${baseUrl.replace(/\/+$/, "")}/v1/compress`;
|
|
183
|
+
}
|
|
184
|
+
function runGitCommand(rootDir, args) {
|
|
185
|
+
return execFileSync("git", args, {
|
|
186
|
+
cwd: rootDir,
|
|
187
|
+
encoding: "utf-8"
|
|
188
|
+
}).trim();
|
|
189
|
+
}
|
|
190
|
+
function normalizeGitRelativePath(value) {
|
|
191
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
|
|
192
|
+
}
|
|
193
|
+
function listGitChangedFiles(rootDir, contentDir) {
|
|
194
|
+
let gitRoot;
|
|
195
|
+
try {
|
|
196
|
+
gitRoot = runGitCommand(rootDir, ["rev-parse", "--show-toplevel"]);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error("Use --changed inside a git repository.");
|
|
199
|
+
}
|
|
200
|
+
const contentDirAbs = path.resolve(rootDir, contentDir);
|
|
201
|
+
const relativeContentDir = normalizeGitRelativePath(path.relative(gitRoot, contentDirAbs));
|
|
202
|
+
if (relativeContentDir.startsWith("../")) throw new Error("Configured contentDir must live inside the current git repository for --changed.");
|
|
203
|
+
const pathspec = relativeContentDir || ".";
|
|
204
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
205
|
+
const commands = [
|
|
206
|
+
[
|
|
207
|
+
"diff",
|
|
208
|
+
"--name-only",
|
|
209
|
+
"--",
|
|
210
|
+
pathspec
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
"diff",
|
|
214
|
+
"--name-only",
|
|
215
|
+
"--cached",
|
|
216
|
+
"--",
|
|
217
|
+
pathspec
|
|
218
|
+
],
|
|
219
|
+
[
|
|
220
|
+
"ls-files",
|
|
221
|
+
"--others",
|
|
222
|
+
"--exclude-standard",
|
|
223
|
+
"--",
|
|
224
|
+
pathspec
|
|
225
|
+
]
|
|
226
|
+
];
|
|
227
|
+
for (const args of commands) {
|
|
228
|
+
const output = runGitCommand(gitRoot, args);
|
|
229
|
+
if (!output) continue;
|
|
230
|
+
for (const line of output.split("\n")) {
|
|
231
|
+
const normalized = normalizeGitRelativePath(line.trim());
|
|
232
|
+
if (!normalized) continue;
|
|
233
|
+
const absolutePath = path.resolve(gitRoot, normalized);
|
|
234
|
+
const relativeToRoot = normalizeGitRelativePath(path.relative(rootDir, absolutePath));
|
|
235
|
+
if (relativeToRoot && !relativeToRoot.startsWith("../")) changedFiles.add(relativeToRoot);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return changedFiles;
|
|
239
|
+
}
|
|
240
|
+
function resolveCompressionApiKey(explicitApiKey, explicitApiKeyEnv) {
|
|
241
|
+
const candidateKeys = [
|
|
242
|
+
explicitApiKeyEnv,
|
|
243
|
+
"TOKEN_COMPANY_API_KEY",
|
|
244
|
+
"THE_TOKEN_COMPANY_API_KEY",
|
|
245
|
+
"TTC_API_KEY"
|
|
246
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
247
|
+
const apiKey = explicitApiKey ?? candidateKeys.map((key) => process.env[key]).find(Boolean);
|
|
248
|
+
if (!apiKey) throw new Error("Missing Token Company API key. Pass --api-key, set agent.compact.apiKey/apiKeyEnv, or set TOKEN_COMPANY_API_KEY.");
|
|
249
|
+
return apiKey;
|
|
250
|
+
}
|
|
251
|
+
function readAgentCompactConfig(content) {
|
|
252
|
+
const compactBlock = extractNestedObjectLiteral(content, ["agent", "compact"]);
|
|
253
|
+
if (!compactBlock) return {};
|
|
254
|
+
const configuredApiKey = readStringProperty(compactBlock, "apiKey");
|
|
255
|
+
return {
|
|
256
|
+
apiKey: configuredApiKey,
|
|
257
|
+
apiKeyEnv: readStringProperty(compactBlock, "apiKeyEnv") ?? (!configuredApiKey ? readEnvReferenceProperty(compactBlock, "apiKey") : void 0),
|
|
258
|
+
baseUrl: readStringProperty(compactBlock, "baseUrl"),
|
|
259
|
+
model: readStringProperty(compactBlock, "model"),
|
|
260
|
+
aggressiveness: readNumberProperty(compactBlock, "aggressiveness"),
|
|
261
|
+
maxOutputTokens: readNumberProperty(compactBlock, "maxOutputTokens"),
|
|
262
|
+
minOutputTokens: readNumberProperty(compactBlock, "minOutputTokens"),
|
|
263
|
+
protectJson: readBooleanProperty(compactBlock, "protectJson")
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function readAgentCompactConfigFromModule(config) {
|
|
267
|
+
const compact = config.agent?.compact;
|
|
268
|
+
if (!compact) return {};
|
|
269
|
+
return {
|
|
270
|
+
apiKey: compact.apiKey,
|
|
271
|
+
apiKeyEnv: compact.apiKeyEnv,
|
|
272
|
+
baseUrl: compact.baseUrl,
|
|
273
|
+
model: compact.model,
|
|
274
|
+
aggressiveness: compact.aggressiveness,
|
|
275
|
+
maxOutputTokens: compact.maxOutputTokens,
|
|
276
|
+
minOutputTokens: compact.minOutputTokens,
|
|
277
|
+
protectJson: compact.protectJson
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function mergeAgentCompactOptions(defaults, overrides) {
|
|
281
|
+
return {
|
|
282
|
+
...defaults,
|
|
283
|
+
...Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0))
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function normalizeTokenBudget(value) {
|
|
287
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
288
|
+
return Math.max(1, Math.ceil(value));
|
|
289
|
+
}
|
|
290
|
+
function readPageTokenBudget(pagePath) {
|
|
291
|
+
const { data } = matter(readFileSync(pagePath, "utf-8"));
|
|
292
|
+
return normalizeTokenBudget(data.agent?.tokenBudget);
|
|
293
|
+
}
|
|
294
|
+
function buildCompactionSettingsHash(options) {
|
|
295
|
+
return hashGeneratedAgentContent(JSON.stringify({
|
|
296
|
+
model: options.model ?? DEFAULT_TTC_MODEL,
|
|
297
|
+
aggressiveness: options.aggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS,
|
|
298
|
+
maxOutputTokens: options.maxOutputTokens ?? null,
|
|
299
|
+
minOutputTokens: options.minOutputTokens ?? null,
|
|
300
|
+
protectJson: options.protectJson ?? null
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
function buildPageOptions(defaults, pagePath) {
|
|
304
|
+
const tokenBudget = readPageTokenBudget(pagePath);
|
|
305
|
+
const pageOptions = mergeAgentCompactOptions(defaults, { maxOutputTokens: tokenBudget });
|
|
306
|
+
if (pageOptions.minOutputTokens !== void 0 && pageOptions.maxOutputTokens !== void 0 && pageOptions.minOutputTokens > pageOptions.maxOutputTokens) pageOptions.minOutputTokens = pageOptions.maxOutputTokens;
|
|
307
|
+
return {
|
|
308
|
+
pageOptions,
|
|
309
|
+
tokenBudget
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function buildResolvedPageSourceDocument(page) {
|
|
313
|
+
return renderDocsMarkdownDocument({
|
|
314
|
+
...page,
|
|
315
|
+
agentRawContent: void 0
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function buildAgentSourceDocument(page) {
|
|
319
|
+
if (typeof page.agentRawContent === "string") return page.agentRawContent;
|
|
320
|
+
return renderDocsMarkdownDocument(page);
|
|
321
|
+
}
|
|
322
|
+
function readCurrentAgentDocument(target) {
|
|
323
|
+
if (!target.hasAgentFile || !existsSync(target.agentPath)) return void 0;
|
|
324
|
+
return parseGeneratedAgentDocument(readFileSync(target.agentPath, "utf-8"));
|
|
325
|
+
}
|
|
326
|
+
function shouldCompactChangedAgentFile(target) {
|
|
327
|
+
const currentDocument = readCurrentAgentDocument(target);
|
|
328
|
+
if (!currentDocument) return false;
|
|
329
|
+
if (!currentDocument.provenance) return true;
|
|
330
|
+
if (currentDocument.provenance.sourceKind !== "agent-md") return false;
|
|
331
|
+
return hashGeneratedAgentContent(currentDocument.content) !== currentDocument.provenance.outputHash;
|
|
332
|
+
}
|
|
333
|
+
function resolveSourceKindForCompaction(target, currentDocument) {
|
|
334
|
+
if (!target.hasAgentFile) return "resolved-page";
|
|
335
|
+
if (currentDocument?.provenance?.sourceKind === "resolved-page") return "resolved-page";
|
|
336
|
+
return "agent-md";
|
|
337
|
+
}
|
|
338
|
+
function buildSourceDocumentForCompaction(page, sourceKind) {
|
|
339
|
+
return sourceKind === "resolved-page" ? buildResolvedPageSourceDocument(page) : buildAgentSourceDocument(page);
|
|
340
|
+
}
|
|
341
|
+
function buildGeneratedAgentProvenance(sourceKind, sourceDocument, output, pageOptions) {
|
|
342
|
+
return {
|
|
343
|
+
version: GENERATED_AGENT_PROVENANCE_VERSION,
|
|
344
|
+
sourceKind,
|
|
345
|
+
sourceHash: hashGeneratedAgentContent(sourceDocument),
|
|
346
|
+
settingsHash: buildCompactionSettingsHash(pageOptions),
|
|
347
|
+
outputHash: hashGeneratedAgentContent(output),
|
|
348
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function inspectAgentCompactionState(page, target, defaults) {
|
|
352
|
+
const { pageOptions, tokenBudget } = buildPageOptions(defaults, target.pagePath);
|
|
353
|
+
const currentDocument = readCurrentAgentDocument(target);
|
|
354
|
+
if (!currentDocument) return {
|
|
355
|
+
status: "missing",
|
|
356
|
+
sourceKind: "resolved-page",
|
|
357
|
+
pageOptions,
|
|
358
|
+
sourceDocument: buildResolvedPageSourceDocument(page),
|
|
359
|
+
tokenBudget
|
|
360
|
+
};
|
|
361
|
+
const sourceKind = resolveSourceKindForCompaction(target, currentDocument);
|
|
362
|
+
const sourceDocument = buildSourceDocumentForCompaction(page, sourceKind);
|
|
363
|
+
if (!currentDocument.provenance) return {
|
|
364
|
+
status: "unknown",
|
|
365
|
+
sourceKind,
|
|
366
|
+
pageOptions,
|
|
367
|
+
sourceDocument,
|
|
368
|
+
tokenBudget
|
|
369
|
+
};
|
|
370
|
+
const outputModified = hashGeneratedAgentContent(currentDocument.content) !== currentDocument.provenance.outputHash;
|
|
371
|
+
if (currentDocument.provenance.sourceKind === "agent-md") return {
|
|
372
|
+
status: outputModified ? "modified" : "unknown",
|
|
373
|
+
sourceKind,
|
|
374
|
+
pageOptions,
|
|
375
|
+
sourceDocument,
|
|
376
|
+
provenance: currentDocument.provenance,
|
|
377
|
+
tokenBudget
|
|
378
|
+
};
|
|
379
|
+
const sourceChanged = hashGeneratedAgentContent(sourceDocument) !== currentDocument.provenance.sourceHash;
|
|
380
|
+
const settingsChanged = buildCompactionSettingsHash(pageOptions) !== currentDocument.provenance.settingsHash;
|
|
381
|
+
return {
|
|
382
|
+
status: outputModified && (sourceChanged || settingsChanged) ? "stale-modified" : outputModified ? "modified" : sourceChanged || settingsChanged ? "stale" : "fresh",
|
|
383
|
+
sourceKind,
|
|
384
|
+
pageOptions,
|
|
385
|
+
sourceDocument,
|
|
386
|
+
provenance: currentDocument.provenance,
|
|
387
|
+
tokenBudget
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function protectForCompression(input) {
|
|
391
|
+
const segments = [];
|
|
392
|
+
const stash = (value) => {
|
|
393
|
+
const token = `__TTC_SAFE_${segments.length}__`;
|
|
394
|
+
segments.push(value);
|
|
395
|
+
return token;
|
|
396
|
+
};
|
|
397
|
+
let result = input;
|
|
398
|
+
result = result.replace(/```[\s\S]*?```/g, stash);
|
|
399
|
+
result = result.replace(/\[[^\]]+\]\([^)]+\)/g, stash);
|
|
400
|
+
result = result.replace(/`[^`\n]+`/g, stash);
|
|
401
|
+
result = result.replace(/^(URL|Description|Related):[^\n]*$/gm, stash);
|
|
402
|
+
result = result.replace(/https?:\/\/[^\s)]+/g, stash);
|
|
403
|
+
for (let index = 0; index < segments.length; index += 1) result = result.replace(`__TTC_SAFE_${index}__`, `<ttc_safe>${segments[index]}</ttc_safe>`);
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function sanitizeCompressedOutput(output) {
|
|
407
|
+
return output.replace(/<\/?ttc_safe>/g, "");
|
|
408
|
+
}
|
|
409
|
+
async function compressDocument(input, options) {
|
|
410
|
+
const apiKey = resolveCompressionApiKey(options.apiKey, options.apiKeyEnv);
|
|
411
|
+
const endpoint = resolveCompressionEndpoint(options.baseUrl);
|
|
412
|
+
const aggressiveness = options.aggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS;
|
|
413
|
+
if (aggressiveness < 0 || aggressiveness > 1) throw new Error("Aggressiveness must be between 0.0 and 1.0.");
|
|
414
|
+
const payload = {
|
|
415
|
+
model: options.model ?? DEFAULT_TTC_MODEL,
|
|
416
|
+
input: protectForCompression(input),
|
|
417
|
+
compression_settings: {
|
|
418
|
+
aggressiveness,
|
|
419
|
+
...options.maxOutputTokens !== void 0 ? { max_output_tokens: options.maxOutputTokens } : {},
|
|
420
|
+
...options.minOutputTokens !== void 0 ? { min_output_tokens: options.minOutputTokens } : {},
|
|
421
|
+
...options.protectJson !== void 0 ? { protect_json: options.protectJson } : {}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const response = await fetch(endpoint, {
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers: {
|
|
427
|
+
Authorization: `Bearer ${apiKey}`,
|
|
428
|
+
"Content-Type": "application/json"
|
|
429
|
+
},
|
|
430
|
+
body: JSON.stringify(payload)
|
|
431
|
+
});
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
const body = await response.text();
|
|
434
|
+
throw new Error(`Token Company request failed (${response.status}): ${body || response.statusText}`);
|
|
435
|
+
}
|
|
436
|
+
const result = await response.json();
|
|
437
|
+
const sanitizedOutput = typeof result.output === "string" ? sanitizeCompressedOutput(result.output) : result.output;
|
|
438
|
+
if (typeof sanitizedOutput !== "string" || sanitizedOutput.trim().length === 0) throw new Error("Token Company response did not include a compressed output.");
|
|
439
|
+
return {
|
|
440
|
+
...result,
|
|
441
|
+
output: sanitizedOutput
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function resolveSelectedPages(pages, targets, entry, requested, includeAll) {
|
|
445
|
+
const compactableBySlug = new Map(targets.map((target) => [target.slug, target]));
|
|
446
|
+
if (includeAll) return targets.map((target) => {
|
|
447
|
+
const page = pages.find((candidate) => candidate.slug === target.slug);
|
|
448
|
+
return page ? {
|
|
449
|
+
page,
|
|
450
|
+
target
|
|
451
|
+
} : null;
|
|
452
|
+
}).filter((value) => value !== null);
|
|
453
|
+
const resolved = [];
|
|
454
|
+
const seen = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const rawRequest of requested) {
|
|
456
|
+
const page = findDocsMarkdownPage(entry, pages, normalizeRequestedPage(entry, rawRequest));
|
|
457
|
+
if (!page) throw new Error(`Could not find a docs page for "${rawRequest}".`);
|
|
458
|
+
const target = compactableBySlug.get(page.slug);
|
|
459
|
+
if (!target) throw new Error(`Page "${rawRequest}" does not use a folder-based page file, so it cannot be written to a sibling agent.md automatically.`);
|
|
460
|
+
if (seen.has(target.slug)) continue;
|
|
461
|
+
seen.add(target.slug);
|
|
462
|
+
resolved.push({
|
|
463
|
+
page,
|
|
464
|
+
target
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return resolved;
|
|
468
|
+
}
|
|
469
|
+
function filterChangedPages(rootDir, contentDir, selectedPages) {
|
|
470
|
+
const changedFiles = listGitChangedFiles(rootDir, contentDir);
|
|
471
|
+
return selectedPages.filter(({ target }) => {
|
|
472
|
+
const relativePagePath = normalizeGitRelativePath(path.relative(rootDir, target.pagePath));
|
|
473
|
+
if (changedFiles.has(relativePagePath)) return true;
|
|
474
|
+
const relativeAgentPath = normalizeGitRelativePath(path.relative(rootDir, target.agentPath));
|
|
475
|
+
return changedFiles.has(relativeAgentPath) && shouldCompactChangedAgentFile(target);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function compactAgentDocs(options = {}) {
|
|
479
|
+
const rootDir = process.cwd();
|
|
480
|
+
const loadedEnv = loadProjectEnv(rootDir);
|
|
481
|
+
for (const [key, value] of Object.entries(loadedEnv)) if (process.env[key] === void 0) process.env[key] = value;
|
|
482
|
+
const loadedConfigModule = await loadDocsConfigModule(rootDir, options.configPath);
|
|
483
|
+
const configContent = readFileSync(loadedConfigModule?.path ?? resolveDocsConfigPath(rootDir, options.configPath), "utf-8");
|
|
484
|
+
const resolvedOptions = mergeAgentCompactOptions(mergeAgentCompactOptions(readAgentCompactConfig(configContent), loadedConfigModule?.config ? readAgentCompactConfigFromModule(loadedConfigModule.config) : {}), options);
|
|
485
|
+
const entry = normalizePathSegment(loadedConfigModule?.config.entry ?? readTopLevelStringProperty(configContent, "entry") ?? "docs") || "docs";
|
|
486
|
+
const contentDir = typeof loadedConfigModule?.config.contentDir === "string" ? loadedConfigModule.config.contentDir : resolveDocsContentDir(rootDir, configContent, entry);
|
|
487
|
+
const siteTitle = typeof loadedConfigModule?.config.nav?.title === "string" ? loadedConfigModule.config.nav.title : readNavTitle(configContent) ?? "Documentation";
|
|
488
|
+
if (resolvedOptions.all && resolvedOptions.pages && resolvedOptions.pages.length > 0) throw new Error("Use either --all or specific page arguments, not both.");
|
|
489
|
+
if (resolvedOptions.includeMissing && !resolvedOptions.stale) throw new Error("Use --include-missing together with --stale.");
|
|
490
|
+
const requestedPages = resolvedOptions.pages?.filter((value) => value.trim().length > 0) ?? [];
|
|
491
|
+
if (!resolvedOptions.all && requestedPages.length === 0 && !resolvedOptions.stale && !resolvedOptions.changed) throw new Error("Pass --all, --changed, --stale, or at least one docs page slug/path to compact.");
|
|
492
|
+
const pages = await createFilesystemDocsMcpSource({
|
|
493
|
+
rootDir,
|
|
494
|
+
entry,
|
|
495
|
+
contentDir,
|
|
496
|
+
siteTitle
|
|
497
|
+
}).getPages();
|
|
498
|
+
if (pages.length === 0) throw new Error(`No docs content was found under ${contentDir}.`);
|
|
499
|
+
const selectedPages = resolveSelectedPages(pages, scanDocsPageTargets(rootDir, contentDir, entry), entry, requestedPages, resolvedOptions.all === true || resolvedOptions.stale === true && requestedPages.length === 0 || resolvedOptions.changed === true && requestedPages.length === 0);
|
|
500
|
+
const filteredPages = resolvedOptions.changed ? filterChangedPages(rootDir, contentDir, selectedPages) : selectedPages;
|
|
501
|
+
if (filteredPages.length === 0) {
|
|
502
|
+
if (resolvedOptions.changed) {
|
|
503
|
+
console.log(pc.green("No changed docs pages needed compaction."));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
throw new Error("No compactable docs pages matched the request.");
|
|
507
|
+
}
|
|
508
|
+
let created = 0;
|
|
509
|
+
let overwritten = 0;
|
|
510
|
+
let processed = 0;
|
|
511
|
+
let skippedFresh = 0;
|
|
512
|
+
let skippedModified = 0;
|
|
513
|
+
let skippedUnknown = 0;
|
|
514
|
+
let skippedMissing = 0;
|
|
515
|
+
const requestedExplicitPages = requestedPages.length > 0;
|
|
516
|
+
for (const { page, target } of filteredPages) {
|
|
517
|
+
const state = inspectAgentCompactionState(page, target, resolvedOptions);
|
|
518
|
+
if (resolvedOptions.stale) {
|
|
519
|
+
if (state.status === "fresh") {
|
|
520
|
+
skippedFresh += 1;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (state.status === "modified" || state.status === "stale-modified") {
|
|
524
|
+
skippedModified += 1;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (state.status === "unknown") {
|
|
528
|
+
skippedUnknown += 1;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (state.status === "missing") {
|
|
532
|
+
if (!(resolvedOptions.includeMissing === true && (requestedExplicitPages || state.tokenBudget !== void 0))) {
|
|
533
|
+
skippedMissing += 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const compressed = await compressDocument(state.sourceDocument, state.pageOptions);
|
|
539
|
+
const nextContent = compressed.output.trimEnd();
|
|
540
|
+
const generatedDocument = serializeGeneratedAgentDocument(nextContent, buildGeneratedAgentProvenance(state.sourceKind, state.sourceDocument, nextContent, state.pageOptions));
|
|
541
|
+
console.log(pc.dim(`Compacting ${page.url} (${compressed.original_input_tokens ?? "?"} -> ${compressed.output_tokens ?? "?"} tokens)...`));
|
|
542
|
+
if (resolvedOptions.dryRun) continue;
|
|
543
|
+
mkdirSync(target.pageDir, { recursive: true });
|
|
544
|
+
writeFileSync(target.agentPath, generatedDocument, "utf-8");
|
|
545
|
+
if (target.hasAgentFile) overwritten += 1;
|
|
546
|
+
else created += 1;
|
|
547
|
+
processed += 1;
|
|
548
|
+
}
|
|
549
|
+
if (resolvedOptions.dryRun) processed = filteredPages.length - skippedFresh - skippedModified - skippedUnknown - skippedMissing;
|
|
550
|
+
if (resolvedOptions.stale && processed === 0) {
|
|
551
|
+
console.log(pc.green("No stale generated agent.md files needed updates."));
|
|
552
|
+
if (skippedFresh + skippedModified + skippedUnknown + skippedMissing > 0) console.log(pc.dim(`Skipped ${skippedFresh} fresh, ${skippedModified} modified, ${skippedUnknown} unknown, and ${skippedMissing} missing page${skippedFresh + skippedModified + skippedUnknown + skippedMissing === 1 ? "" : "s"}.`));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const summaryPrefix = resolvedOptions.dryRun ? "Dry run complete" : "Compaction complete";
|
|
556
|
+
console.log(pc.green(`${summaryPrefix}: ${processed} page${processed === 1 ? "" : "s"} processed` + (resolvedOptions.dryRun ? "." : ` (${created} created, ${overwritten} overwritten).`)));
|
|
557
|
+
if (resolvedOptions.stale) console.log(pc.dim(`Skipped ${skippedFresh} fresh, ${skippedModified} modified, ${skippedUnknown} unknown, and ${skippedMissing} missing page${skippedFresh + skippedModified + skippedUnknown + skippedMissing === 1 ? "" : "s"}.`));
|
|
558
|
+
}
|
|
559
|
+
function printAgentCompactHelp() {
|
|
560
|
+
console.log(`
|
|
561
|
+
${pc.bold("docs agent compact")} — Generate sibling ${pc.cyan("agent.md")} files by compacting resolved docs pages.
|
|
562
|
+
|
|
563
|
+
${pc.dim("Usage:")}
|
|
564
|
+
npx @farming-labs/docs@latest ${pc.cyan("agent compact")} ${pc.dim("[page ...]")}
|
|
565
|
+
|
|
566
|
+
${pc.dim("Examples:")}
|
|
567
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact installation configuration")}
|
|
568
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact /docs/installation")}
|
|
569
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact --page installation --page configuration")}
|
|
570
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact --all")}
|
|
571
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact --changed")}
|
|
572
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact --stale")}
|
|
573
|
+
${pc.cyan("npx @farming-labs/docs@latest agent compact --stale --include-missing")}
|
|
574
|
+
|
|
575
|
+
${pc.dim("Per-page override:")}
|
|
576
|
+
Add ${pc.cyan("agent.tokenBudget")} to a page frontmatter block to override the compact output target for that page.
|
|
577
|
+
|
|
578
|
+
${pc.dim("Options:")}
|
|
579
|
+
${pc.cyan("--all")} Compact every folder-based docs page under the configured contentDir
|
|
580
|
+
${pc.cyan("--page <slug|path>")} Add a page explicitly (repeatable); positional page args work too
|
|
581
|
+
${pc.cyan("--changed")} Compact only docs pages changed in the current git working tree
|
|
582
|
+
${pc.cyan("--stale")} Re-compact only stale generated agent.md files
|
|
583
|
+
${pc.cyan("--include-missing")} With ${pc.cyan("--stale")}, also create missing agent.md files for explicit pages or pages that define ${pc.cyan("agent.tokenBudget")}
|
|
584
|
+
${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
|
|
585
|
+
${pc.cyan("--api-key <key>")} Token Company API key (or set ${pc.dim("TOKEN_COMPANY_API_KEY")})
|
|
586
|
+
${pc.cyan("--api-key-env <name>")} Custom env var name for the Token Company API key
|
|
587
|
+
${pc.cyan("--base-url <url>")} Override the Token Company API base URL (useful for tests)
|
|
588
|
+
${pc.cyan("--model <name>")} Compression model (${pc.dim("bear-1.2")} by default)
|
|
589
|
+
${pc.cyan("--aggressiveness <0-1>")} Compression intensity (${pc.dim("0.3")} by default)
|
|
590
|
+
${pc.cyan("--max-output-tokens <n>")} Pass through to Token Company compression settings
|
|
591
|
+
${pc.cyan("--min-output-tokens <n>")} Pass through to Token Company compression settings
|
|
592
|
+
${pc.cyan("--protect-json <bool>")} Preserve JSON objects during compression
|
|
593
|
+
${pc.cyan("--dry-run")} Resolve pages and call the compressor without writing files
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/code-blocks.ts
|
|
599
|
+
const execFileAsync = promisify(execFile);
|
|
600
|
+
const DEFAULT_ENV_FILES = [
|
|
601
|
+
".env.local",
|
|
602
|
+
".env.test",
|
|
603
|
+
".env"
|
|
604
|
+
];
|
|
605
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
|
|
606
|
+
function resolveDocsCodeBlocksValidateConfig(input) {
|
|
607
|
+
if (input === false || input === void 0) return {
|
|
608
|
+
enabled: false,
|
|
609
|
+
planner: { provider: "metadata" },
|
|
610
|
+
runner: {
|
|
611
|
+
provider: "local",
|
|
612
|
+
tokenEnv: "VERCEL_TOKEN",
|
|
613
|
+
projectIdEnv: "VERCEL_PROJECT_ID",
|
|
614
|
+
teamIdEnv: "VERCEL_TEAM_ID",
|
|
615
|
+
projectJson: ".vercel/project.json",
|
|
616
|
+
runtime: "node24",
|
|
617
|
+
timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS
|
|
618
|
+
},
|
|
619
|
+
envFile: DEFAULT_ENV_FILES,
|
|
620
|
+
env: {},
|
|
621
|
+
missingEnv: "skip",
|
|
622
|
+
unsupportedLanguage: "skip",
|
|
623
|
+
mode: "report"
|
|
624
|
+
};
|
|
625
|
+
const config = input === true ? {} : input;
|
|
626
|
+
const planner = normalizePlannerConfig(config.planner);
|
|
627
|
+
const runner = normalizeRunnerConfig(config.runner);
|
|
628
|
+
const envFile = Array.isArray(config.envFile) ? config.envFile : typeof config.envFile === "string" ? [config.envFile] : DEFAULT_ENV_FILES;
|
|
629
|
+
return {
|
|
630
|
+
enabled: config.enabled ?? true,
|
|
631
|
+
planner,
|
|
632
|
+
runner,
|
|
633
|
+
envFile,
|
|
634
|
+
env: config.env ?? {},
|
|
635
|
+
missingEnv: config.missingEnv ?? "skip",
|
|
636
|
+
unsupportedLanguage: config.unsupportedLanguage ?? "skip",
|
|
637
|
+
mode: config.mode ?? "report"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function parseCodeFenceInfo(info) {
|
|
641
|
+
const trimmed = info.trim();
|
|
642
|
+
if (!trimmed) return { meta: {} };
|
|
643
|
+
const firstToken = trimmed.match(/^(\S+)/)?.[1] ?? "";
|
|
644
|
+
const language = firstToken && !firstToken.includes("=") ? firstToken : void 0;
|
|
645
|
+
const attributeSource = language ? trimmed.slice(firstToken.length).trim() : trimmed;
|
|
646
|
+
const meta = {};
|
|
647
|
+
const attributePattern = /([A-Za-z_:][\w:.-]*)(?:=(?:"([^"]*)"|'([^']*)'|([^\s"']+)))?/g;
|
|
648
|
+
let match;
|
|
649
|
+
while (match = attributePattern.exec(attributeSource)) {
|
|
650
|
+
const key = match[1];
|
|
651
|
+
meta[key] = match[2] ?? match[3] ?? match[4] ?? true;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
language,
|
|
655
|
+
meta
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function extractCodeBlocksFromMarkdown(input) {
|
|
659
|
+
const blocks = [];
|
|
660
|
+
const lines = input.source.split("\n");
|
|
661
|
+
let openFence;
|
|
662
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
663
|
+
const line = lines[index] ?? "";
|
|
664
|
+
const trimmed = line.trim();
|
|
665
|
+
if (!openFence) {
|
|
666
|
+
const openMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
667
|
+
if (!openMatch) continue;
|
|
668
|
+
openFence = {
|
|
669
|
+
marker: openMatch[1],
|
|
670
|
+
info: openMatch[2]?.trim() ?? "",
|
|
671
|
+
code: [],
|
|
672
|
+
lineStart: index + 1
|
|
673
|
+
};
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (isClosingFence(trimmed, openFence.marker)) {
|
|
677
|
+
const parsed = parseCodeFenceInfo(openFence.info);
|
|
678
|
+
const meta = parsed.meta;
|
|
679
|
+
const blockIndex = blocks.length + 1;
|
|
680
|
+
const relativePath = input.relativePath ?? input.filePath;
|
|
681
|
+
blocks.push({
|
|
682
|
+
id: `${relativePath}#code-${blockIndex}`,
|
|
683
|
+
filePath: input.filePath,
|
|
684
|
+
relativePath,
|
|
685
|
+
lineStart: openFence.lineStart,
|
|
686
|
+
lineEnd: index + 1,
|
|
687
|
+
language: parsed.language,
|
|
688
|
+
title: readStringMeta(meta, "title"),
|
|
689
|
+
framework: readStringMeta(meta, "framework"),
|
|
690
|
+
packageManager: readStringMeta(meta, "packageManager"),
|
|
691
|
+
runnable: readBooleanMeta(meta, "runnable") ?? false,
|
|
692
|
+
env: readEnvMeta(meta),
|
|
693
|
+
meta,
|
|
694
|
+
code: openFence.code.join("\n")
|
|
695
|
+
});
|
|
696
|
+
openFence = void 0;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
openFence.code.push(line);
|
|
700
|
+
}
|
|
701
|
+
return blocks;
|
|
702
|
+
}
|
|
703
|
+
function collectCodeBlockTargets(options) {
|
|
704
|
+
const root = path.resolve(options.rootDir);
|
|
705
|
+
const contentRoot = path.resolve(root, options.contentDir);
|
|
706
|
+
if (!existsSync(contentRoot)) return [];
|
|
707
|
+
return walkMarkdownFiles(contentRoot).flatMap((filePath) => {
|
|
708
|
+
return extractCodeBlocksFromMarkdown({
|
|
709
|
+
source: readFileSync(filePath, "utf-8"),
|
|
710
|
+
filePath,
|
|
711
|
+
relativePath: path.relative(root, filePath).replace(/\\/g, "/")
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async function planCodeBlockTargets(targets, config) {
|
|
716
|
+
if (config.planner.provider === "metadata") return targets.map((target) => buildMetadataExecutionPlan(target, config));
|
|
717
|
+
if (config.planner.provider === "cloud") throw new Error("Hosted cloud code block planning is not available in this package yet.");
|
|
718
|
+
return planWithOpenAICompatibleProvider(targets, config);
|
|
719
|
+
}
|
|
720
|
+
async function validateCodeBlockPlans(input) {
|
|
721
|
+
const validationEnv = loadValidationEnv(input.rootDir, input.config);
|
|
722
|
+
const preflight = input.plans.map((plan) => preflightPlan(plan, input.config, validationEnv));
|
|
723
|
+
const runnable = preflight.filter((result) => result.status !== "SKIP" && !result.reason);
|
|
724
|
+
const skippedOrFailed = preflight.filter((result) => result.status === "SKIP" || result.reason);
|
|
725
|
+
const plansToRun = runnable.map((result) => result.plan);
|
|
726
|
+
if (plansToRun.length === 0) return skippedOrFailed;
|
|
727
|
+
const runResults = input.config.runner.provider === "vercel-sandbox" ? await runPlansInVercelSandbox(plansToRun, input.rootDir, input.config, validationEnv.env) : await runPlansLocally(plansToRun, input.config, validationEnv.env);
|
|
728
|
+
return [...skippedOrFailed, ...runResults].sort((a, b) => a.id.localeCompare(b.id));
|
|
729
|
+
}
|
|
730
|
+
async function validateCodeBlocks(input) {
|
|
731
|
+
const targets = collectCodeBlockTargets({
|
|
732
|
+
rootDir: input.rootDir,
|
|
733
|
+
contentDir: input.contentDir
|
|
734
|
+
});
|
|
735
|
+
const plans = await planCodeBlockTargets(targets, input.config);
|
|
736
|
+
const results = input.planOnly || input.config.mode === "plan" ? plans.map((plan) => ({
|
|
737
|
+
id: plan.id,
|
|
738
|
+
target: plan.target,
|
|
739
|
+
plan,
|
|
740
|
+
status: plan.action === "skip" ? "SKIP" : "PLAN",
|
|
741
|
+
reason: plan.reason ?? (plan.action === "skip" ? "planned skip" : "planned")
|
|
742
|
+
})) : await validateCodeBlockPlans({
|
|
743
|
+
plans,
|
|
744
|
+
rootDir: input.rootDir,
|
|
745
|
+
config: input.config
|
|
746
|
+
});
|
|
747
|
+
return {
|
|
748
|
+
summary: results.reduce((acc, result) => {
|
|
749
|
+
acc.total += 1;
|
|
750
|
+
if (result.status === "PLAN") acc.planned += 1;
|
|
751
|
+
if (result.status === "PASS") acc.pass += 1;
|
|
752
|
+
if (result.status === "SKIP") acc.skip += 1;
|
|
753
|
+
if (result.status === "FAIL") acc.fail += 1;
|
|
754
|
+
return acc;
|
|
755
|
+
}, {
|
|
756
|
+
total: 0,
|
|
757
|
+
planned: 0,
|
|
758
|
+
pass: 0,
|
|
759
|
+
skip: 0,
|
|
760
|
+
fail: 0
|
|
761
|
+
}),
|
|
762
|
+
config: input.config,
|
|
763
|
+
targets,
|
|
764
|
+
plans,
|
|
765
|
+
results
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function normalizePlannerConfig(input) {
|
|
769
|
+
if (!input) return { provider: "metadata" };
|
|
770
|
+
if (typeof input === "string") return { provider: input };
|
|
771
|
+
return {
|
|
772
|
+
provider: input.provider ?? "metadata",
|
|
773
|
+
...input
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function normalizeRunnerConfig(input) {
|
|
777
|
+
const config = typeof input === "string" ? { provider: input } : input ?? {};
|
|
778
|
+
return {
|
|
779
|
+
provider: config.provider ?? "local",
|
|
780
|
+
tokenEnv: config.tokenEnv ?? "VERCEL_TOKEN",
|
|
781
|
+
projectIdEnv: config.projectIdEnv ?? "VERCEL_PROJECT_ID",
|
|
782
|
+
teamIdEnv: config.teamIdEnv ?? "VERCEL_TEAM_ID",
|
|
783
|
+
projectJson: config.projectJson === void 0 ? ".vercel/project.json" : config.projectJson,
|
|
784
|
+
runtime: config.runtime ?? "node24",
|
|
785
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function buildMetadataExecutionPlan(target, config) {
|
|
789
|
+
const language = normalizeLanguage(target.language);
|
|
790
|
+
const template = target.framework ?? templateFromLanguage(language);
|
|
791
|
+
const requiredEnv = target.env;
|
|
792
|
+
if (readBooleanMeta(target.meta, "partial") || looksPartial(target.code)) return skipPlan(target, template, requiredEnv, "partial fragment", config.planner.provider);
|
|
793
|
+
if (!language) {
|
|
794
|
+
if (!target.runnable) return skipPlan(target, "unknown", requiredEnv, "code block is not marked runnable", config.planner.provider);
|
|
795
|
+
if (looksLikeShellCommand(target.code)) return shellPlan(target, "shell", requiredEnv, config.planner.provider);
|
|
796
|
+
return skipPlan(target, "unknown", requiredEnv, "missing language and not obviously runnable", config.planner.provider);
|
|
797
|
+
}
|
|
798
|
+
if (!target.runnable) return skipPlan(target, template, requiredEnv, "code block is not marked runnable", config.planner.provider);
|
|
799
|
+
if (isShellLanguage(language)) return shellPlan(target, template, requiredEnv, config.planner.provider);
|
|
800
|
+
if (language === "json") return syntaxPlan(target, "json", "node", requiredEnv, config.planner.provider);
|
|
801
|
+
if (language === "javascript" || language === "js" || language === "jsx") return executePlan(target, template, "node", "js", {
|
|
802
|
+
cmd: "node",
|
|
803
|
+
args: []
|
|
804
|
+
}, requiredEnv, config.planner.provider);
|
|
805
|
+
if (language === "typescript" || language === "ts" || language === "tsx") return executePlan(target, template, "tsx", "ts", {
|
|
806
|
+
cmd: "npx",
|
|
807
|
+
args: ["--yes", "tsx"]
|
|
808
|
+
}, requiredEnv, config.planner.provider);
|
|
809
|
+
if (language === "python" || language === "py") return executePlan(target, template, "python3", "py", {
|
|
810
|
+
cmd: "python3",
|
|
811
|
+
args: []
|
|
812
|
+
}, requiredEnv, config.planner.provider);
|
|
813
|
+
if (language === "ruby" || language === "rb") return executePlan(target, template, "ruby", "rb", {
|
|
814
|
+
cmd: "ruby",
|
|
815
|
+
args: []
|
|
816
|
+
}, requiredEnv, config.planner.provider);
|
|
817
|
+
if (language === "elixir" || language === "ex" || language === "exs") return executePlan(target, template, "elixir", "exs", {
|
|
818
|
+
cmd: "elixir",
|
|
819
|
+
args: []
|
|
820
|
+
}, requiredEnv, config.planner.provider);
|
|
821
|
+
if (language === "yaml" || language === "yml") return skipPlan(target, "yaml", requiredEnv, "YAML syntax validation requires a YAML parser", config.planner.provider);
|
|
822
|
+
if (!target.runnable) return skipPlan(target, template, requiredEnv, "code block is not marked runnable", config.planner.provider);
|
|
823
|
+
return skipPlan(target, template, requiredEnv, `unsupported language: ${target.language}`, config.planner.provider);
|
|
824
|
+
}
|
|
825
|
+
function executePlan(target, template, runtime, extension, command, requiredEnv, planner) {
|
|
826
|
+
const filePath = `snippet-${slugify(target.id)}.${extension}`;
|
|
827
|
+
return {
|
|
828
|
+
id: target.id,
|
|
829
|
+
target,
|
|
830
|
+
action: "execute",
|
|
831
|
+
template,
|
|
832
|
+
runtime,
|
|
833
|
+
filePath,
|
|
834
|
+
command: {
|
|
835
|
+
cmd: command.cmd,
|
|
836
|
+
args: [...command.args, filePath]
|
|
837
|
+
},
|
|
838
|
+
requiredEnv,
|
|
839
|
+
planner
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function shellPlan(target, template, requiredEnv, planner) {
|
|
843
|
+
const filePath = `snippet-${slugify(target.id)}.sh`;
|
|
844
|
+
return {
|
|
845
|
+
id: target.id,
|
|
846
|
+
target,
|
|
847
|
+
action: "execute",
|
|
848
|
+
template,
|
|
849
|
+
runtime: "bash",
|
|
850
|
+
filePath,
|
|
851
|
+
command: {
|
|
852
|
+
cmd: "bash",
|
|
853
|
+
args: [filePath]
|
|
854
|
+
},
|
|
855
|
+
requiredEnv,
|
|
856
|
+
planner
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function syntaxPlan(target, template, runtime, requiredEnv, planner) {
|
|
860
|
+
const filePath = `snippet-${slugify(target.id)}.json`;
|
|
861
|
+
return {
|
|
862
|
+
id: target.id,
|
|
863
|
+
target,
|
|
864
|
+
action: "validate-syntax",
|
|
865
|
+
template,
|
|
866
|
+
runtime,
|
|
867
|
+
filePath,
|
|
868
|
+
command: {
|
|
869
|
+
cmd: "node",
|
|
870
|
+
args: [
|
|
871
|
+
"-e",
|
|
872
|
+
`JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))`,
|
|
873
|
+
filePath
|
|
874
|
+
]
|
|
875
|
+
},
|
|
876
|
+
requiredEnv,
|
|
877
|
+
planner
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function skipPlan(target, template, requiredEnv, reason, planner) {
|
|
881
|
+
return {
|
|
882
|
+
id: target.id,
|
|
883
|
+
target,
|
|
884
|
+
action: "skip",
|
|
885
|
+
template,
|
|
886
|
+
requiredEnv,
|
|
887
|
+
reason,
|
|
888
|
+
planner
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
async function planWithOpenAICompatibleProvider(targets, config) {
|
|
892
|
+
const baseUrl = config.planner.baseUrl ?? (config.planner.baseUrlEnv ? process.env[config.planner.baseUrlEnv] : void 0) ?? (config.planner.provider === "openai" ? "https://api.openai.com/v1" : void 0);
|
|
893
|
+
const apiKey = config.planner.apiKey ?? (config.planner.apiKeyEnv ? process.env[config.planner.apiKeyEnv] : void 0);
|
|
894
|
+
if (!baseUrl) throw new Error("codeBlocks.validate planner requires baseUrl or baseUrlEnv.");
|
|
895
|
+
if (!apiKey) throw new Error(`codeBlocks.validate planner requires an API key. Set ${config.planner.apiKeyEnv ?? "apiKeyEnv"} in your environment.`);
|
|
896
|
+
const metadataPlans = targets.map((target) => buildMetadataExecutionPlan(target, config));
|
|
897
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers: {
|
|
900
|
+
Authorization: `Bearer ${apiKey}`,
|
|
901
|
+
"Content-Type": "application/json"
|
|
902
|
+
},
|
|
903
|
+
body: JSON.stringify({
|
|
904
|
+
model: config.planner.model ?? "gpt-4.1-mini",
|
|
905
|
+
temperature: 0,
|
|
906
|
+
response_format: { type: "json_object" },
|
|
907
|
+
messages: [{
|
|
908
|
+
role: "system",
|
|
909
|
+
content: "You produce JSON execution plans for documentation code blocks. Do not rewrite snippets. If the snippet is partial, missing setup, or unsafe, choose action skip. Return only JSON."
|
|
910
|
+
}, {
|
|
911
|
+
role: "user",
|
|
912
|
+
content: JSON.stringify({
|
|
913
|
+
contract: { plans: "array of {id, action: execute|validate-syntax|skip, template, runtime?, reason?, requiredEnv?: string[]}" },
|
|
914
|
+
codeBlocks: targets.map((target) => ({
|
|
915
|
+
id: target.id,
|
|
916
|
+
language: target.language,
|
|
917
|
+
title: target.title,
|
|
918
|
+
framework: target.framework,
|
|
919
|
+
packageManager: target.packageManager,
|
|
920
|
+
runnable: target.runnable,
|
|
921
|
+
env: target.env,
|
|
922
|
+
meta: target.meta,
|
|
923
|
+
code: target.code.slice(0, 8e3)
|
|
924
|
+
}))
|
|
925
|
+
})
|
|
926
|
+
}]
|
|
927
|
+
})
|
|
928
|
+
});
|
|
929
|
+
if (!response.ok) throw new Error(`Planner request failed with ${response.status} ${response.statusText}.`);
|
|
930
|
+
const content = (await response.json()).choices?.[0]?.message?.content ?? "";
|
|
931
|
+
let parsed;
|
|
932
|
+
try {
|
|
933
|
+
parsed = JSON.parse(content);
|
|
934
|
+
} catch {
|
|
935
|
+
throw new Error("Planner returned non-JSON content.");
|
|
936
|
+
}
|
|
937
|
+
const byId = new Map(metadataPlans.map((plan) => [plan.id, plan]));
|
|
938
|
+
for (const plan of parsed.plans ?? []) {
|
|
939
|
+
if (!plan.id || !byId.has(plan.id)) continue;
|
|
940
|
+
const fallback = byId.get(plan.id);
|
|
941
|
+
byId.set(plan.id, {
|
|
942
|
+
...fallback,
|
|
943
|
+
action: isPlanAction(plan.action) ? plan.action : fallback.action,
|
|
944
|
+
template: typeof plan.template === "string" && plan.template ? plan.template : fallback.template,
|
|
945
|
+
runtime: typeof plan.runtime === "string" && plan.runtime ? plan.runtime : fallback.runtime,
|
|
946
|
+
requiredEnv: Array.isArray(plan.requiredEnv) ? plan.requiredEnv.filter((value) => typeof value === "string") : fallback.requiredEnv,
|
|
947
|
+
reason: typeof plan.reason === "string" ? plan.reason : fallback.reason,
|
|
948
|
+
planner: config.planner.provider
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return metadataPlans.map((plan) => byId.get(plan.id));
|
|
952
|
+
}
|
|
953
|
+
function preflightPlan(plan, config, validationEnv) {
|
|
954
|
+
if (plan.action === "skip") return {
|
|
955
|
+
id: plan.id,
|
|
956
|
+
target: plan.target,
|
|
957
|
+
plan,
|
|
958
|
+
status: "SKIP",
|
|
959
|
+
reason: plan.reason
|
|
960
|
+
};
|
|
961
|
+
const missingEnv = plan.requiredEnv.filter((key) => !validationEnv.env[key]);
|
|
962
|
+
if (missingEnv.length > 0) {
|
|
963
|
+
const reason = `missing env: ${missingEnv.join(", ")}`;
|
|
964
|
+
return {
|
|
965
|
+
id: plan.id,
|
|
966
|
+
target: plan.target,
|
|
967
|
+
plan,
|
|
968
|
+
status: config.missingEnv === "error" ? "FAIL" : "SKIP",
|
|
969
|
+
reason
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
if (!plan.command || !plan.filePath) return {
|
|
973
|
+
id: plan.id,
|
|
974
|
+
target: plan.target,
|
|
975
|
+
plan,
|
|
976
|
+
status: config.unsupportedLanguage === "error" ? "FAIL" : "SKIP",
|
|
977
|
+
reason: "no executable command in plan"
|
|
978
|
+
};
|
|
979
|
+
return {
|
|
980
|
+
id: plan.id,
|
|
981
|
+
target: plan.target,
|
|
982
|
+
plan,
|
|
983
|
+
status: "PASS"
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
async function runPlansLocally(plans, config, env) {
|
|
987
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), "docs-codeblocks-"));
|
|
988
|
+
try {
|
|
989
|
+
return await Promise.all(plans.map(async (plan) => {
|
|
990
|
+
if (!plan.command || !plan.filePath) return skippedResult(plan, "no executable command in plan");
|
|
991
|
+
writeFileSync(path.join(tempDir, plan.filePath), plan.target.code, "utf-8");
|
|
992
|
+
try {
|
|
993
|
+
const result = await execFileAsync(plan.command.cmd, plan.command.args, {
|
|
994
|
+
cwd: tempDir,
|
|
995
|
+
env: {
|
|
996
|
+
...process.env,
|
|
997
|
+
...env
|
|
998
|
+
},
|
|
999
|
+
timeout: config.runner.timeoutMs,
|
|
1000
|
+
maxBuffer: 1024 * 1024
|
|
1001
|
+
});
|
|
1002
|
+
return {
|
|
1003
|
+
id: plan.id,
|
|
1004
|
+
target: plan.target,
|
|
1005
|
+
plan,
|
|
1006
|
+
status: "PASS",
|
|
1007
|
+
stdout: result.stdout,
|
|
1008
|
+
stderr: result.stderr,
|
|
1009
|
+
exitCode: 0
|
|
1010
|
+
};
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
const err = error;
|
|
1013
|
+
return {
|
|
1014
|
+
id: plan.id,
|
|
1015
|
+
target: plan.target,
|
|
1016
|
+
plan,
|
|
1017
|
+
status: "FAIL",
|
|
1018
|
+
stdout: err.stdout,
|
|
1019
|
+
stderr: err.stderr,
|
|
1020
|
+
exitCode: typeof err.code === "number" ? err.code : null,
|
|
1021
|
+
reason: err.signal ? `terminated by ${err.signal}` : err.message
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}));
|
|
1025
|
+
} finally {
|
|
1026
|
+
rmSync(tempDir, {
|
|
1027
|
+
force: true,
|
|
1028
|
+
recursive: true
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function runPlansInVercelSandbox(plans, rootDir, config, env) {
|
|
1033
|
+
const credentials = await resolveVercelSandboxCredentials(rootDir, config);
|
|
1034
|
+
if (credentials.missing.length > 0) return plans.map((plan) => skippedResult(plan, `missing ${credentials.missing.join(", ")}`));
|
|
1035
|
+
try {
|
|
1036
|
+
const { Sandbox } = await import("@vercel/sandbox");
|
|
1037
|
+
const sandbox = await Sandbox.create({
|
|
1038
|
+
runtime: config.runner.runtime,
|
|
1039
|
+
timeout: Math.max(config.runner.timeoutMs * Math.max(1, plans.length), config.runner.timeoutMs),
|
|
1040
|
+
env,
|
|
1041
|
+
token: credentials.token,
|
|
1042
|
+
projectId: credentials.projectId,
|
|
1043
|
+
teamId: credentials.teamId
|
|
1044
|
+
});
|
|
1045
|
+
try {
|
|
1046
|
+
return await Promise.all(plans.map(async (plan) => {
|
|
1047
|
+
if (!plan.command || !plan.filePath) return skippedResult(plan, "no executable command in plan");
|
|
1048
|
+
await sandbox.writeFiles([{
|
|
1049
|
+
path: plan.filePath,
|
|
1050
|
+
content: Buffer.from(plan.target.code)
|
|
1051
|
+
}]);
|
|
1052
|
+
const command = await sandbox.runCommand({
|
|
1053
|
+
cmd: plan.command.cmd,
|
|
1054
|
+
args: plan.command.args,
|
|
1055
|
+
cwd: "/vercel/sandbox",
|
|
1056
|
+
env
|
|
1057
|
+
});
|
|
1058
|
+
const [stdout, stderr] = await Promise.all([command.stdout(), command.stderr()]);
|
|
1059
|
+
return {
|
|
1060
|
+
id: plan.id,
|
|
1061
|
+
target: plan.target,
|
|
1062
|
+
plan,
|
|
1063
|
+
status: command.exitCode === 0 ? "PASS" : "FAIL",
|
|
1064
|
+
stdout,
|
|
1065
|
+
stderr,
|
|
1066
|
+
exitCode: command.exitCode,
|
|
1067
|
+
reason: command.exitCode === 0 ? void 0 : `exit code ${command.exitCode}`
|
|
1068
|
+
};
|
|
1069
|
+
}));
|
|
1070
|
+
} finally {
|
|
1071
|
+
await sandbox.stop().catch(() => {});
|
|
1072
|
+
}
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1075
|
+
return plans.map((plan) => skippedResult(plan, `vercel-sandbox unavailable: ${message}`));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async function resolveVercelSandboxCredentials(rootDir, config) {
|
|
1079
|
+
const projectJson = readVercelProjectJson(rootDir, config.runner.projectJson);
|
|
1080
|
+
const token = process.env[config.runner.tokenEnv];
|
|
1081
|
+
if (!token && process.env.VERCEL_OIDC_TOKEN) return { missing: [] };
|
|
1082
|
+
if (!token) return { missing: [config.runner.tokenEnv] };
|
|
1083
|
+
const envProjectId = process.env[config.runner.projectIdEnv];
|
|
1084
|
+
const envTeamId = process.env[config.runner.teamIdEnv];
|
|
1085
|
+
const discoveredProject = !(envProjectId && envTeamId) && !(projectJson?.projectId && projectJson?.orgId) ? await discoverVercelSandboxProject(token) : void 0;
|
|
1086
|
+
const projectId = envProjectId ?? projectJson?.projectId ?? discoveredProject?.projectId;
|
|
1087
|
+
const teamId = envTeamId ?? projectJson?.orgId ?? discoveredProject?.teamId;
|
|
1088
|
+
return {
|
|
1089
|
+
token,
|
|
1090
|
+
projectId,
|
|
1091
|
+
teamId,
|
|
1092
|
+
missing: [projectId ? void 0 : "Vercel project id", teamId ? void 0 : "Vercel team id"].filter((value) => Boolean(value))
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
async function discoverVercelSandboxProject(token) {
|
|
1096
|
+
try {
|
|
1097
|
+
const response = await fetch("https://api.vercel.com/v9/projects?limit=1", { headers: { Authorization: `Bearer ${token}` } });
|
|
1098
|
+
if (!response.ok) return void 0;
|
|
1099
|
+
const project = (await response.json()).projects?.[0];
|
|
1100
|
+
if (!project) return void 0;
|
|
1101
|
+
return {
|
|
1102
|
+
projectId: typeof project.id === "string" ? project.id : void 0,
|
|
1103
|
+
teamId: typeof project.accountId === "string" ? project.accountId : void 0
|
|
1104
|
+
};
|
|
1105
|
+
} catch {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function readVercelProjectJson(rootDir, projectJson) {
|
|
1110
|
+
if (projectJson === false) return void 0;
|
|
1111
|
+
const fullPath = path.resolve(rootDir, projectJson);
|
|
1112
|
+
if (!existsSync(fullPath)) return void 0;
|
|
1113
|
+
try {
|
|
1114
|
+
const parsed = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
1115
|
+
return {
|
|
1116
|
+
projectId: typeof parsed.projectId === "string" ? parsed.projectId : void 0,
|
|
1117
|
+
orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0
|
|
1118
|
+
};
|
|
1119
|
+
} catch {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function loadValidationEnv(rootDir, config) {
|
|
1124
|
+
const fileEnv = {};
|
|
1125
|
+
for (const file of config.envFile) {
|
|
1126
|
+
const fullPath = path.resolve(rootDir, file);
|
|
1127
|
+
if (!existsSync(fullPath)) continue;
|
|
1128
|
+
Object.assign(fileEnv, parseEnvFile(readFileSync(fullPath, "utf-8")));
|
|
1129
|
+
}
|
|
1130
|
+
const resolved = {};
|
|
1131
|
+
const missing = [];
|
|
1132
|
+
for (const [runtimeKey, sourceKey] of Object.entries(config.env)) {
|
|
1133
|
+
const value = process.env[sourceKey] ?? fileEnv[sourceKey];
|
|
1134
|
+
if (value === void 0) {
|
|
1135
|
+
missing.push(runtimeKey);
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
resolved[runtimeKey] = value;
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
env: resolved,
|
|
1142
|
+
missing
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function parseEnvFile(source) {
|
|
1146
|
+
const env = {};
|
|
1147
|
+
for (const line of source.split(/\r?\n/)) {
|
|
1148
|
+
const trimmed = line.trim();
|
|
1149
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1150
|
+
const equals = trimmed.indexOf("=");
|
|
1151
|
+
if (equals <= 0) continue;
|
|
1152
|
+
const key = trimmed.slice(0, equals).trim();
|
|
1153
|
+
env[key] = trimmed.slice(equals + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
1154
|
+
}
|
|
1155
|
+
return env;
|
|
1156
|
+
}
|
|
1157
|
+
function readStringMeta(meta, key) {
|
|
1158
|
+
const value = meta[key];
|
|
1159
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
1160
|
+
}
|
|
1161
|
+
function readBooleanMeta(meta, key) {
|
|
1162
|
+
const value = meta[key];
|
|
1163
|
+
if (typeof value === "boolean") return value;
|
|
1164
|
+
if (typeof value !== "string") return void 0;
|
|
1165
|
+
const normalized = value.trim().toLowerCase();
|
|
1166
|
+
if (!normalized || normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
1167
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
function readEnvMeta(meta) {
|
|
1171
|
+
const direct = meta.env;
|
|
1172
|
+
const values = /* @__PURE__ */ new Set();
|
|
1173
|
+
if (typeof direct === "string") for (const item of direct.split(/[,\s]+/)) {
|
|
1174
|
+
const trimmed = item.trim();
|
|
1175
|
+
if (trimmed) values.add(trimmed);
|
|
1176
|
+
}
|
|
1177
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
1178
|
+
if (!key.startsWith("env:") && !key.startsWith("env.")) continue;
|
|
1179
|
+
const envName = key.slice(4).trim();
|
|
1180
|
+
if (envName && value !== false) values.add(envName);
|
|
1181
|
+
}
|
|
1182
|
+
return [...values];
|
|
1183
|
+
}
|
|
1184
|
+
function walkMarkdownFiles(root) {
|
|
1185
|
+
const files = [];
|
|
1186
|
+
const ignored = new Set([
|
|
1187
|
+
".git",
|
|
1188
|
+
".next",
|
|
1189
|
+
".nuxt",
|
|
1190
|
+
".svelte-kit",
|
|
1191
|
+
"dist",
|
|
1192
|
+
"node_modules",
|
|
1193
|
+
"out"
|
|
1194
|
+
]);
|
|
1195
|
+
function visit(dir) {
|
|
1196
|
+
for (const entry of readdirSync(dir)) {
|
|
1197
|
+
if (ignored.has(entry)) continue;
|
|
1198
|
+
const fullPath = path.join(dir, entry);
|
|
1199
|
+
if (statSync(fullPath).isDirectory()) visit(fullPath);
|
|
1200
|
+
else if (/\.(?:md|mdx)$/i.test(entry)) files.push(fullPath);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
visit(root);
|
|
1204
|
+
return files.sort();
|
|
1205
|
+
}
|
|
1206
|
+
function normalizeLanguage(language) {
|
|
1207
|
+
return language?.trim().toLowerCase();
|
|
1208
|
+
}
|
|
1209
|
+
function isClosingFence(trimmedLine, marker) {
|
|
1210
|
+
return new RegExp(`^${marker[0]}{${marker.length},}[ \\t]*$`).test(trimmedLine);
|
|
1211
|
+
}
|
|
1212
|
+
function isShellLanguage(language) {
|
|
1213
|
+
return [
|
|
1214
|
+
"bash",
|
|
1215
|
+
"sh",
|
|
1216
|
+
"shell",
|
|
1217
|
+
"zsh",
|
|
1218
|
+
"curl"
|
|
1219
|
+
].includes(language);
|
|
1220
|
+
}
|
|
1221
|
+
function templateFromLanguage(language) {
|
|
1222
|
+
if (!language) return "unknown";
|
|
1223
|
+
if (isShellLanguage(language)) return "shell";
|
|
1224
|
+
if (language === "js" || language === "javascript" || language === "jsx") return "node";
|
|
1225
|
+
if (language === "ts" || language === "typescript" || language === "tsx") return "typescript";
|
|
1226
|
+
if (language === "py" || language === "python") return "python";
|
|
1227
|
+
return language;
|
|
1228
|
+
}
|
|
1229
|
+
function looksLikeShellCommand(code) {
|
|
1230
|
+
return /^(?:\s*(?:npm|pnpm|yarn|bun|npx|curl|git|node|python3?|deno|uv|pip)\b)/m.test(code);
|
|
1231
|
+
}
|
|
1232
|
+
function looksPartial(code) {
|
|
1233
|
+
const trimmed = code.trim();
|
|
1234
|
+
if (!trimmed) return true;
|
|
1235
|
+
return /^\.\.\.$/m.test(trimmed) || /\/\/\s*\.\.\.|#\s*\.\.\./.test(trimmed);
|
|
1236
|
+
}
|
|
1237
|
+
function slugify(value) {
|
|
1238
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "snippet";
|
|
1239
|
+
}
|
|
1240
|
+
function isPlanAction(value) {
|
|
1241
|
+
return value === "execute" || value === "validate-syntax" || value === "skip";
|
|
1242
|
+
}
|
|
1243
|
+
function skippedResult(plan, reason) {
|
|
1244
|
+
return {
|
|
1245
|
+
id: plan.id,
|
|
1246
|
+
target: plan.target,
|
|
1247
|
+
plan,
|
|
1248
|
+
status: "SKIP",
|
|
1249
|
+
reason
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/cli/codeblocks.ts
|
|
1255
|
+
function parseInlineFlag(arg) {
|
|
1256
|
+
const [rawKey, value] = arg.slice(2).split("=", 2);
|
|
1257
|
+
return {
|
|
1258
|
+
key: rawKey.trim(),
|
|
1259
|
+
value
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function parseCodeBlocksValidateArgs(argv) {
|
|
1263
|
+
const parsed = {};
|
|
1264
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1265
|
+
const arg = argv[index];
|
|
1266
|
+
if (arg === "--help" || arg === "-h") {
|
|
1267
|
+
parsed.help = true;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (arg === "--json") {
|
|
1271
|
+
parsed.json = true;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (arg === "--plan") {
|
|
1275
|
+
parsed.plan = true;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (arg === "--run") {
|
|
1279
|
+
parsed.run = true;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (arg.startsWith("--config=")) {
|
|
1283
|
+
const value = parseInlineFlag(arg).value;
|
|
1284
|
+
if (!value) throw new Error("Missing value for --config.");
|
|
1285
|
+
parsed.configPath = value;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (arg === "--config") {
|
|
1289
|
+
const value = argv[index + 1];
|
|
1290
|
+
if (!value || value.startsWith("--")) throw new Error("Missing value for --config.");
|
|
1291
|
+
parsed.configPath = value;
|
|
1292
|
+
index += 1;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
throw new Error(`Unknown codeblocks validate flag: ${arg}.`);
|
|
1296
|
+
}
|
|
1297
|
+
return parsed;
|
|
1298
|
+
}
|
|
1299
|
+
function printCodeBlocksValidateHelp() {
|
|
1300
|
+
console.log(`
|
|
1301
|
+
${pc.bold("@farming-labs/docs codeblocks validate")}
|
|
1302
|
+
|
|
1303
|
+
${pc.dim("Usage:")}
|
|
1304
|
+
pnpm exec docs codeblocks validate
|
|
1305
|
+
pnpm exec docs codeblocks validate --plan
|
|
1306
|
+
pnpm exec docs codeblocks validate --json
|
|
1307
|
+
pnpm exec docs codeblocks validate --config docs.config.ts
|
|
1308
|
+
|
|
1309
|
+
${pc.dim("Options:")}
|
|
1310
|
+
${pc.cyan("--plan")} Build the execution plan without running code
|
|
1311
|
+
${pc.cyan("--run")} Force execution even when config mode is ${pc.dim("\"plan\"")}
|
|
1312
|
+
${pc.cyan("--json")} Print machine-readable output
|
|
1313
|
+
${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
|
|
1314
|
+
${pc.cyan("-h, --help")} Show this help message
|
|
1315
|
+
`);
|
|
1316
|
+
}
|
|
1317
|
+
async function runCodeBlocksValidate(options = {}) {
|
|
1318
|
+
const rootDir = process.cwd();
|
|
1319
|
+
const loaded = await loadDocsConfigModule(rootDir, options.configPath, { silent: options.json });
|
|
1320
|
+
const configPath = loaded?.path ?? resolveDocsConfigPath(rootDir, options.configPath);
|
|
1321
|
+
const configContent = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
1322
|
+
const entry = loaded?.config.entry ?? readTopLevelStringProperty(configContent, "entry") ?? "docs";
|
|
1323
|
+
const contentDir = loaded?.config.contentDir ?? resolveDocsContentDir(rootDir, configContent, entry);
|
|
1324
|
+
const config = resolveDocsCodeBlocksValidateConfig(loaded?.config.codeBlocks?.validate ?? readStaticCodeBlocksValidateConfig(configContent));
|
|
1325
|
+
if (!config.enabled) {
|
|
1326
|
+
const disabledReport = {
|
|
1327
|
+
summary: {
|
|
1328
|
+
total: 0,
|
|
1329
|
+
planned: 0,
|
|
1330
|
+
pass: 0,
|
|
1331
|
+
skip: 0,
|
|
1332
|
+
fail: 0
|
|
1333
|
+
},
|
|
1334
|
+
config,
|
|
1335
|
+
targets: [],
|
|
1336
|
+
plans: [],
|
|
1337
|
+
results: []
|
|
1338
|
+
};
|
|
1339
|
+
if (options.json) console.log(JSON.stringify(redactReport(disabledReport), null, 2));
|
|
1340
|
+
else console.log(pc.yellow("codeBlocks.validate is disabled. Add `codeBlocks: { validate: true }` to docs.config.ts."));
|
|
1341
|
+
return disabledReport;
|
|
1342
|
+
}
|
|
1343
|
+
const effectiveConfig = {
|
|
1344
|
+
...config,
|
|
1345
|
+
mode: options.run ? "report" : config.mode
|
|
1346
|
+
};
|
|
1347
|
+
const planOnly = options.plan === true || effectiveConfig.mode === "plan";
|
|
1348
|
+
const report = await validateCodeBlocks({
|
|
1349
|
+
rootDir,
|
|
1350
|
+
contentDir,
|
|
1351
|
+
config: effectiveConfig,
|
|
1352
|
+
planOnly: options.plan
|
|
1353
|
+
});
|
|
1354
|
+
if (options.json) console.log(JSON.stringify(redactReport(report), null, 2));
|
|
1355
|
+
else printCodeBlocksReport(report, planOnly);
|
|
1356
|
+
if (!options.plan && report.summary.fail > 0) process.exitCode = 1;
|
|
1357
|
+
return report;
|
|
1358
|
+
}
|
|
1359
|
+
function readStaticCodeBlocksValidateConfig(content) {
|
|
1360
|
+
const block = extractNestedObjectLiteral(content, ["codeBlocks"]);
|
|
1361
|
+
if (!block) return void 0;
|
|
1362
|
+
if (/\bvalidate\s*:\s*true\b/.test(block)) return true;
|
|
1363
|
+
if (/\bvalidate\s*:\s*false\b/.test(block)) return false;
|
|
1364
|
+
const validateBlock = extractNestedObjectLiteral(content, ["codeBlocks", "validate"]);
|
|
1365
|
+
if (validateBlock) {
|
|
1366
|
+
const config = {};
|
|
1367
|
+
const enabled = readBooleanProperty(validateBlock, "enabled");
|
|
1368
|
+
const mode = readStringProperty(validateBlock, "mode");
|
|
1369
|
+
const missingEnv = readStringProperty(validateBlock, "missingEnv");
|
|
1370
|
+
const unsupportedLanguage = readStringProperty(validateBlock, "unsupportedLanguage");
|
|
1371
|
+
const envFile = readStringArrayProperty(validateBlock, "envFile");
|
|
1372
|
+
if (enabled !== void 0) config.enabled = enabled;
|
|
1373
|
+
if (mode === "plan" || mode === "report") config.mode = mode;
|
|
1374
|
+
if (missingEnv === "skip" || missingEnv === "warn" || missingEnv === "error") config.missingEnv = missingEnv;
|
|
1375
|
+
if (unsupportedLanguage === "skip" || unsupportedLanguage === "warn" || unsupportedLanguage === "error") config.unsupportedLanguage = unsupportedLanguage;
|
|
1376
|
+
if (envFile) config.envFile = envFile;
|
|
1377
|
+
const planner = readStaticPlannerConfig(extractNestedObjectLiteral(content, [
|
|
1378
|
+
"codeBlocks",
|
|
1379
|
+
"validate",
|
|
1380
|
+
"planner"
|
|
1381
|
+
]));
|
|
1382
|
+
if (planner) config.planner = planner;
|
|
1383
|
+
const runner = readStaticRunnerConfig(extractNestedObjectLiteral(content, [
|
|
1384
|
+
"codeBlocks",
|
|
1385
|
+
"validate",
|
|
1386
|
+
"runner"
|
|
1387
|
+
]));
|
|
1388
|
+
if (runner) config.runner = runner;
|
|
1389
|
+
const env = readStringRecord(extractNestedObjectLiteral(content, [
|
|
1390
|
+
"codeBlocks",
|
|
1391
|
+
"validate",
|
|
1392
|
+
"env"
|
|
1393
|
+
]));
|
|
1394
|
+
if (env && Object.keys(env).length > 0) config.env = env;
|
|
1395
|
+
return config;
|
|
1396
|
+
}
|
|
1397
|
+
if (/\bvalidate\s*:\s*\{/.test(block)) return true;
|
|
1398
|
+
}
|
|
1399
|
+
function readStaticPlannerConfig(block) {
|
|
1400
|
+
if (!block) return void 0;
|
|
1401
|
+
const provider = readStringProperty(block, "provider");
|
|
1402
|
+
const model = readStringProperty(block, "model");
|
|
1403
|
+
const baseUrl = readStringProperty(block, "baseUrl");
|
|
1404
|
+
const baseUrlEnv = readStringProperty(block, "baseUrlEnv");
|
|
1405
|
+
const apiKeyEnv = readStringProperty(block, "apiKeyEnv");
|
|
1406
|
+
if (provider !== "metadata" && provider !== "openai" && provider !== "openai-compatible" && provider !== "cloud") return;
|
|
1407
|
+
return {
|
|
1408
|
+
provider,
|
|
1409
|
+
...model ? { model } : {},
|
|
1410
|
+
...baseUrl ? { baseUrl } : {},
|
|
1411
|
+
...baseUrlEnv ? { baseUrlEnv } : {},
|
|
1412
|
+
...apiKeyEnv ? { apiKeyEnv } : {}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function readStaticRunnerConfig(block) {
|
|
1416
|
+
if (!block) return void 0;
|
|
1417
|
+
const provider = readStringProperty(block, "provider");
|
|
1418
|
+
const tokenEnv = readStringProperty(block, "tokenEnv");
|
|
1419
|
+
const projectIdEnv = readStringProperty(block, "projectIdEnv");
|
|
1420
|
+
const teamIdEnv = readStringProperty(block, "teamIdEnv");
|
|
1421
|
+
const projectJson = readStringProperty(block, "projectJson");
|
|
1422
|
+
const runtime = readStringProperty(block, "runtime");
|
|
1423
|
+
const timeoutMs = readNumberProperty(block, "timeoutMs");
|
|
1424
|
+
if (provider !== "local" && provider !== "vercel-sandbox" && provider !== "cloud") return;
|
|
1425
|
+
return {
|
|
1426
|
+
provider,
|
|
1427
|
+
...tokenEnv ? { tokenEnv } : {},
|
|
1428
|
+
...projectIdEnv ? { projectIdEnv } : {},
|
|
1429
|
+
...teamIdEnv ? { teamIdEnv } : {},
|
|
1430
|
+
...projectJson ? { projectJson } : {},
|
|
1431
|
+
...runtime === "node24" || runtime === "node22" || runtime === "python3.13" ? { runtime } : {},
|
|
1432
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function readStringArrayProperty(content, key) {
|
|
1436
|
+
const single = readStringProperty(content, key);
|
|
1437
|
+
if (single) return [single];
|
|
1438
|
+
const match = content.match(new RegExp(`\\b${key}\\b\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
1439
|
+
if (!match) return void 0;
|
|
1440
|
+
const values = [...match[1].matchAll(/["']([^"']+)["']/g)].map((item) => item[1]);
|
|
1441
|
+
return values.length > 0 ? values : void 0;
|
|
1442
|
+
}
|
|
1443
|
+
function readStringRecord(block) {
|
|
1444
|
+
if (!block) return void 0;
|
|
1445
|
+
const record = {};
|
|
1446
|
+
for (const pattern of [/(?:^|,)\s*([A-Za-z_$][\w$]*)\s*:\s*["']([^"']+)["']/g, /(?:^|,)\s*["']([^"']+)["']\s*:\s*["']([^"']+)["']/g]) {
|
|
1447
|
+
let match;
|
|
1448
|
+
while (match = pattern.exec(block)) record[match[1]] = match[2];
|
|
1449
|
+
}
|
|
1450
|
+
return record;
|
|
1451
|
+
}
|
|
1452
|
+
function printCodeBlocksReport(report, planOnly) {
|
|
1453
|
+
const label = planOnly ? "Code block plan" : "Code block validation";
|
|
1454
|
+
console.log(pc.bold(label));
|
|
1455
|
+
console.log([
|
|
1456
|
+
...report.summary.planned > 0 ? [`${pc.cyan(`${report.summary.planned} planned`)}`] : [],
|
|
1457
|
+
`${pc.green(`${report.summary.pass} pass`)}`,
|
|
1458
|
+
`${pc.yellow(`${report.summary.skip} skip`)}`,
|
|
1459
|
+
`${report.summary.fail > 0 ? pc.red(`${report.summary.fail} fail`) : pc.dim("0 fail")}`,
|
|
1460
|
+
`${report.targets.length} code blocks`
|
|
1461
|
+
].join(pc.dim(" • ")));
|
|
1462
|
+
if (report.results.length === 0) return;
|
|
1463
|
+
console.log();
|
|
1464
|
+
for (const result of report.results) {
|
|
1465
|
+
const status = result.status === "PLAN" ? pc.cyan(result.status) : result.status === "PASS" ? pc.green(result.status) : result.status === "FAIL" ? pc.red(result.status) : pc.yellow(result.status);
|
|
1466
|
+
const location = `${result.target.relativePath}:${result.target.lineStart}`;
|
|
1467
|
+
const detail = result.reason ?? result.plan.reason ?? result.plan.template;
|
|
1468
|
+
console.log(`${status} ${pc.cyan(location)} ${pc.dim(detail)}`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function redactReport(report) {
|
|
1472
|
+
return {
|
|
1473
|
+
...report,
|
|
1474
|
+
config: {
|
|
1475
|
+
...report.config,
|
|
1476
|
+
planner: {
|
|
1477
|
+
...report.config.planner,
|
|
1478
|
+
apiKey: report.config.planner.apiKey ? "[REDACTED]" : void 0
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
results: report.results.map((result) => ({
|
|
1482
|
+
...result,
|
|
1483
|
+
stdout: trimOutput(result.stdout),
|
|
1484
|
+
stderr: trimOutput(result.stderr)
|
|
1485
|
+
}))
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function trimOutput(value) {
|
|
1489
|
+
if (!value) return value;
|
|
1490
|
+
return value.length > 4e3 ? `${value.slice(0, 4e3)}...` : value;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
//#endregion
|
|
1494
|
+
export { readPageTokenBudget as a, printAgentCompactHelp as i, inspectAgentCompactionState as n, scanDocsPageTargets as o, parseCodeBlocksValidateArgs, printCodeBlocksValidateHelp, parseAgentCompactArgs as r, runCodeBlocksValidate, compactAgentDocs as t };
|