@texuli/tools 0.0.0-stage → 0.1.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/dist/a2ui/from-a2ui.d.ts +35 -0
  4. package/dist/a2ui/from-a2ui.js +180 -0
  5. package/dist/a2ui/index.d.ts +11 -0
  6. package/dist/a2ui/index.js +11 -0
  7. package/dist/a2ui/props.d.ts +10 -0
  8. package/dist/a2ui/props.js +94 -0
  9. package/dist/a2ui/schema.d.ts +26 -0
  10. package/dist/a2ui/schema.js +193 -0
  11. package/dist/a2ui/to-a2ui.d.ts +30 -0
  12. package/dist/a2ui/to-a2ui.js +119 -0
  13. package/dist/a2ui/types.d.ts +71 -0
  14. package/dist/a2ui/types.js +11 -0
  15. package/dist/cli-add/add.d.ts +21 -0
  16. package/dist/cli-add/add.js +241 -0
  17. package/dist/cli-add/cli.d.ts +25 -0
  18. package/dist/cli-add/cli.js +157 -0
  19. package/dist/cli-add/index.d.ts +12 -0
  20. package/dist/cli-add/index.js +11 -0
  21. package/dist/cli-add/paths.d.ts +4 -0
  22. package/dist/cli-add/paths.js +4 -0
  23. package/dist/cli-add/provenance.d.ts +32 -0
  24. package/dist/cli-add/provenance.js +78 -0
  25. package/dist/cli-add/registry.d.ts +19 -0
  26. package/dist/cli-add/registry.js +50 -0
  27. package/dist/cli-add/types.d.ts +71 -0
  28. package/dist/cli-add/types.js +4 -0
  29. package/dist/cli-init/cli.d.ts +25 -0
  30. package/dist/cli-init/cli.js +212 -0
  31. package/dist/cli-init/config.d.ts +10 -0
  32. package/dist/cli-init/config.js +62 -0
  33. package/dist/cli-init/detect.d.ts +13 -0
  34. package/dist/cli-init/detect.js +42 -0
  35. package/dist/cli-init/files.d.ts +18 -0
  36. package/dist/cli-init/files.js +59 -0
  37. package/dist/cli-init/index.d.ts +14 -0
  38. package/dist/cli-init/index.js +13 -0
  39. package/dist/cli-init/init.d.ts +14 -0
  40. package/dist/cli-init/init.js +157 -0
  41. package/dist/cli-init/safe-path.d.ts +2 -0
  42. package/dist/cli-init/safe-path.js +26 -0
  43. package/dist/cli-init/theme-apply.d.ts +2 -0
  44. package/dist/cli-init/theme-apply.js +69 -0
  45. package/dist/cli-init/themes.d.ts +37 -0
  46. package/dist/cli-init/themes.js +83 -0
  47. package/dist/cli-init/types.d.ts +50 -0
  48. package/dist/cli-init/types.js +1 -0
  49. package/dist/cli-init/wire.d.ts +20 -0
  50. package/dist/cli-init/wire.js +90 -0
  51. package/dist/cli-update/cli.d.ts +26 -0
  52. package/dist/cli-update/cli.js +161 -0
  53. package/dist/cli-update/index.d.ts +11 -0
  54. package/dist/cli-update/index.js +10 -0
  55. package/dist/cli-update/lines.d.ts +38 -0
  56. package/dist/cli-update/lines.js +189 -0
  57. package/dist/cli-update/provenance.d.ts +14 -0
  58. package/dist/cli-update/provenance.js +51 -0
  59. package/dist/cli-update/types.d.ts +83 -0
  60. package/dist/cli-update/types.js +1 -0
  61. package/dist/cli-update/update.d.ts +12 -0
  62. package/dist/cli-update/update.js +261 -0
  63. package/dist/elements/content.d.ts +15 -0
  64. package/dist/elements/content.js +53 -0
  65. package/dist/elements/define.d.ts +49 -0
  66. package/dist/elements/define.js +281 -0
  67. package/dist/elements/index.d.ts +6 -0
  68. package/dist/elements/index.js +6 -0
  69. package/dist/golden/axe.d.ts +32 -0
  70. package/dist/golden/axe.js +22 -0
  71. package/dist/golden/catalog.d.ts +24 -0
  72. package/dist/golden/catalog.js +85 -0
  73. package/dist/golden/cli.d.ts +9 -0
  74. package/dist/golden/cli.js +45 -0
  75. package/dist/golden/guidance.d.ts +12 -0
  76. package/dist/golden/guidance.js +171 -0
  77. package/dist/golden/index.d.ts +17 -0
  78. package/dist/golden/index.js +17 -0
  79. package/dist/golden/outline.d.ts +21 -0
  80. package/dist/golden/outline.js +68 -0
  81. package/dist/golden/paint.d.ts +21 -0
  82. package/dist/golden/paint.js +103 -0
  83. package/dist/golden/report.d.ts +28 -0
  84. package/dist/golden/report.js +64 -0
  85. package/dist/golden/score.d.ts +16 -0
  86. package/dist/golden/score.js +116 -0
  87. package/dist/golden/sinks.d.ts +8 -0
  88. package/dist/golden/sinks.js +11 -0
  89. package/dist/golden/tasks.d.ts +4 -0
  90. package/dist/golden/tasks.js +686 -0
  91. package/dist/golden/types.d.ts +54 -0
  92. package/dist/golden/types.js +9 -0
  93. package/dist/index.d.ts +8 -0
  94. package/dist/index.js +8 -0
  95. package/dist/internal/paths.d.ts +56 -0
  96. package/dist/internal/paths.js +186 -0
  97. package/dist/llms/bundle.d.ts +6 -0
  98. package/dist/llms/bundle.js +47 -0
  99. package/dist/llms/cli.d.ts +24 -0
  100. package/dist/llms/cli.js +104 -0
  101. package/dist/llms/format.d.ts +35 -0
  102. package/dist/llms/format.js +166 -0
  103. package/dist/llms/index.d.ts +9 -0
  104. package/dist/llms/index.js +8 -0
  105. package/dist/llms/types.d.ts +51 -0
  106. package/dist/llms/types.js +1 -0
  107. package/dist/llms/write.d.ts +14 -0
  108. package/dist/llms/write.js +49 -0
  109. package/dist/mcp/index.d.ts +2 -0
  110. package/dist/mcp/index.js +2 -0
  111. package/dist/mcp/server.d.ts +20 -0
  112. package/dist/mcp/server.js +326 -0
  113. package/dist/mcp/stdio.d.ts +3 -0
  114. package/dist/mcp/stdio.js +34 -0
  115. package/package.json +63 -4
@@ -0,0 +1,54 @@
1
+ import type { CatalogTreeInput, RenderTier } from '@texuli/catalog/render/renderer';
2
+ /** One pre-registered interface task of the agent golden set. */
3
+ export interface GoldenTask {
4
+ /** Stable id: `g<NN>-<kebab-name>`. */
5
+ id: string;
6
+ title: string;
7
+ /** The prompt a model receives. Fictional content only. */
8
+ brief: string;
9
+ /** Who authors the tree. The golden set is agent content: `untrusted`. */
10
+ tier: RenderTier;
11
+ /** Component ids the interface needs (the scorer's coverage check). */
12
+ mustInclude: readonly string[];
13
+ /**
14
+ * `dontUseWhen` lines the brief deliberately baits (it mentions navigation,
15
+ * sign-in or a purchase the interface must leave to the host app). Each is
16
+ * an exact manifest line with a detector in `guidance.ts`.
17
+ */
18
+ traps: readonly string[];
19
+ /** A reference tree that passes every check. */
20
+ reference: CatalogTreeInput;
21
+ }
22
+ /** The checks the scorer runs, in order. */
23
+ export declare const GOLDEN_CHECKS: readonly ["schema", "dontUseWhen", "sinks", "grammar", "axe", "coverage"];
24
+ export type GoldenCheckId = (typeof GOLDEN_CHECKS)[number];
25
+ /**
26
+ * `skipped` when a check cannot run: axe without a DOM runner or after a
27
+ * schema failure, grammar when nothing rendered, sinks and guidance when the
28
+ * output is not a tree. A skipped check never counts as passed.
29
+ */
30
+ export type GoldenCheckStatus = 'pass' | 'fail' | 'skipped';
31
+ export interface GoldenCheckResult {
32
+ check: GoldenCheckId;
33
+ status: GoldenCheckStatus;
34
+ /** Why it failed (stable, human-readable lines). Empty on pass. */
35
+ issues: readonly string[];
36
+ }
37
+ export interface GoldenScore {
38
+ taskId: string;
39
+ /** True when no check failed (a skipped check does not fail). */
40
+ passed: boolean;
41
+ /** Passed checks over checks that ran. */
42
+ score: number;
43
+ checks: readonly GoldenCheckResult[];
44
+ }
45
+ /** What the axe runner reports: one entry per violated rule. */
46
+ export interface GoldenAxeViolation {
47
+ id: string;
48
+ impact?: string | null;
49
+ nodes: number;
50
+ /** First offending node's markup, for the report. */
51
+ sample?: string;
52
+ }
53
+ /** Runs axe on a painted document and returns its violations. */
54
+ export type GoldenAxeRunner = (root: Document) => Promise<readonly GoldenAxeViolation[]>;
@@ -0,0 +1,9 @@
1
+ /** The checks the scorer runs, in order. */
2
+ export const GOLDEN_CHECKS = [
3
+ 'schema',
4
+ 'dontUseWhen',
5
+ 'sinks',
6
+ 'grammar',
7
+ 'axe',
8
+ 'coverage',
9
+ ];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@texuli/tools` — agent and developer tools.
3
+ * Subpaths land as PLAN_001 tasks ship them.
4
+ */
5
+ export { type A2uiDiagnostic, type A2uiMessage, a2uiToCatalogTree, catalogTreeToA2ui, validateA2uiMessages, } from './a2ui/index.js';
6
+ export { type AddableComponent, type AddCliArgs, type AddOptions, type AddResult, addComponent, DEFAULT_ADD_REGISTRY, getAddable, hasProvenance, isPrimitivesPath, listAddableIds, parseAddArgs, planAdd, provenanceHeader, runAddCli, } from './cli-add/index.js';
7
+ export { diffComponent, type UpdateFile, type UpdateFileStatus, type UpdateOptions, type UpdateResult, updateComponent, } from './cli-update/index.js';
8
+ export { buildLlmsBundle, componentDocPath, componentHref, formatComponentMarkdown, formatLlmsFullTxt, formatLlmsLink, formatLlmsTxt, type LlmsBundle, type LlmsBundleOptions, type LlmsCliArgs, type LlmsFile, type LlmsWriteResult, parseLlmsArgs, runLlmsCli, writeLlmsBundle, } from './llms/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@texuli/tools` — agent and developer tools.
3
+ * Subpaths land as PLAN_001 tasks ship them.
4
+ */
5
+ export { a2uiToCatalogTree, catalogTreeToA2ui, validateA2uiMessages, } from "./a2ui/index.js";
6
+ export { addComponent, DEFAULT_ADD_REGISTRY, getAddable, hasProvenance, isPrimitivesPath, listAddableIds, parseAddArgs, planAdd, provenanceHeader, runAddCli, } from "./cli-add/index.js";
7
+ export { diffComponent, updateComponent, } from "./cli-update/index.js";
8
+ export { buildLlmsBundle, componentDocPath, componentHref, formatComponentMarkdown, formatLlmsFullTxt, formatLlmsLink, formatLlmsTxt, parseLlmsArgs, runLlmsCli, writeLlmsBundle, } from "./llms/index.js";
@@ -0,0 +1,56 @@
1
+ import type { AddableComponent } from '../cli-add/types.js';
2
+ /** True when `candidate` escapes `root` (after resolve). */
3
+ export declare function escapesRoot(root: string, candidate: string): boolean;
4
+ /**
5
+ * Resolve a destination directory under `cwd`.
6
+ * Returns an error string when the path escapes cwd or is absolute.
7
+ */
8
+ export declare function resolveContainedOutDir(cwd: string, outRelativeOrDefault: string): {
9
+ ok: true;
10
+ outDir: string;
11
+ } | {
12
+ ok: false;
13
+ error: string;
14
+ };
15
+ /**
16
+ * Walk from `targetPath` up to (and including) `cwdReal`, refusing any symlink.
17
+ * Missing leaf is fine (we will create it). Every existing ancestor must not
18
+ * be a symlink.
19
+ */
20
+ export declare function assertNoSymlinkOnPath(targetPath: string, cwdReal: string): {
21
+ ok: true;
22
+ } | {
23
+ ok: false;
24
+ error: string;
25
+ };
26
+ /**
27
+ * Validate a registry entry's package and directory fields.
28
+ * Both must be relative, normalized, without `..`, absolute form, or NUL.
29
+ */
30
+ export declare function validateRegistryEntry(entry: AddableComponent): {
31
+ ok: true;
32
+ } | {
33
+ ok: false;
34
+ error: string;
35
+ };
36
+ /**
37
+ * Ensure `sourceDir` resolves inside `sourceRoot/packages/<package>`.
38
+ */
39
+ export declare function resolveContainedSourceDir(sourceRoot: string, entry: AddableComponent): {
40
+ ok: true;
41
+ sourceDir: string;
42
+ packageRoot: string;
43
+ } | {
44
+ ok: false;
45
+ error: string;
46
+ };
47
+ /**
48
+ * Ensure a source file's real path stays under sourceDir (no symlink escape).
49
+ */
50
+ export declare function assertSourceFileInside(sourceDir: string, filePath: string): {
51
+ ok: true;
52
+ realFile: string;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Shared path containment for `@texuli/tools` file writers (`cli-add`, `llms`, …).
3
+ * Every write stays under a resolved root; every read that claims a source
4
+ * tree stays under that tree. Symlinks on a destination path are refused.
5
+ */
6
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
7
+ import { isAbsolute, normalize, relative, resolve, sep } from 'node:path';
8
+ /** True when `candidate` escapes `root` (after resolve). */
9
+ export function escapesRoot(root, candidate) {
10
+ const rootResolved = resolve(root);
11
+ const candidateResolved = resolve(candidate);
12
+ const rel = relative(rootResolved, candidateResolved);
13
+ if (rel === '')
14
+ return false;
15
+ return rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel);
16
+ }
17
+ function realPathIfExists(path) {
18
+ try {
19
+ if (existsSync(path))
20
+ return realpathSync(path);
21
+ }
22
+ catch {
23
+ // fall through
24
+ }
25
+ return resolve(path);
26
+ }
27
+ /**
28
+ * Resolve a destination directory under `cwd`.
29
+ * Returns an error string when the path escapes cwd or is absolute.
30
+ */
31
+ export function resolveContainedOutDir(cwd, outRelativeOrDefault) {
32
+ const cwdReal = realPathIfExists(cwd);
33
+ if (isAbsolute(outRelativeOrDefault) ||
34
+ /^[A-Za-z]:[\\/]/.test(outRelativeOrDefault) ||
35
+ outRelativeOrDefault.startsWith('\\\\')) {
36
+ return {
37
+ ok: false,
38
+ error: `Destination must be relative to the project root (got absolute path: ${outRelativeOrDefault}).`,
39
+ };
40
+ }
41
+ const resolved = resolve(cwdReal, outRelativeOrDefault);
42
+ const rel = relative(cwdReal, resolved);
43
+ if (rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) {
44
+ return {
45
+ ok: false,
46
+ error: `Destination escapes the project root: ${outRelativeOrDefault}`,
47
+ };
48
+ }
49
+ return { ok: true, outDir: resolved };
50
+ }
51
+ /**
52
+ * Walk from `targetPath` up to (and including) `cwdReal`, refusing any symlink.
53
+ * Missing leaf is fine (we will create it). Every existing ancestor must not
54
+ * be a symlink.
55
+ */
56
+ export function assertNoSymlinkOnPath(targetPath, cwdReal) {
57
+ const cwdResolved = resolve(cwdReal);
58
+ let current = resolve(targetPath);
59
+ const relToCwd = relative(cwdResolved, current);
60
+ if (relToCwd.startsWith(`..${sep}`) || relToCwd === '..' || isAbsolute(relToCwd)) {
61
+ return { ok: false, error: `Path escapes the project root: ${targetPath}` };
62
+ }
63
+ while (true) {
64
+ if (existsSync(current)) {
65
+ try {
66
+ const st = lstatSync(current);
67
+ if (st.isSymbolicLink()) {
68
+ return {
69
+ ok: false,
70
+ error: `Refusing to write through a symlink: ${current}`,
71
+ };
72
+ }
73
+ }
74
+ catch (error) {
75
+ return {
76
+ ok: false,
77
+ error: `Cannot inspect path ${current}: ${error instanceof Error ? error.message : String(error)}`,
78
+ };
79
+ }
80
+ }
81
+ if (current === cwdResolved)
82
+ break;
83
+ const parent = resolve(current, '..');
84
+ if (parent === current)
85
+ break;
86
+ const relParent = relative(cwdResolved, parent);
87
+ if (relParent.startsWith(`..${sep}`) || relParent === '..' || isAbsolute(relParent)) {
88
+ break;
89
+ }
90
+ current = parent;
91
+ }
92
+ return { ok: true };
93
+ }
94
+ /**
95
+ * Validate a registry entry's package and directory fields.
96
+ * Both must be relative, normalized, without `..`, absolute form, or NUL.
97
+ */
98
+ export function validateRegistryEntry(entry) {
99
+ const checkRelativeField = (name, value) => {
100
+ if (typeof value !== 'string' || value.length === 0) {
101
+ return `Registry ${name} must be a non-empty string.`;
102
+ }
103
+ if (value.includes('\0')) {
104
+ return `Registry ${name} must not contain NUL.`;
105
+ }
106
+ if (isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\')) {
107
+ return `Registry ${name} must be relative (got absolute: ${value}).`;
108
+ }
109
+ // Inspect raw segments before normalize() collapses `a/../b` into `b`.
110
+ const rawSegments = value.split(/[/\\]/);
111
+ if (rawSegments.some((s) => s === '..')) {
112
+ return `Registry ${name} must not contain '..' segments (got: ${value}).`;
113
+ }
114
+ const normalized = normalize(value);
115
+ const segments = normalized.split(/[/\\]/).filter((s) => s.length > 0);
116
+ if (segments.some((s) => s === '..') ||
117
+ normalized === '..' ||
118
+ normalized.startsWith(`..${sep}`)) {
119
+ return `Registry ${name} must not contain '..' segments (got: ${value}).`;
120
+ }
121
+ return null;
122
+ };
123
+ const packageError = checkRelativeField('package', entry.package);
124
+ if (packageError)
125
+ return { ok: false, error: packageError };
126
+ // package is a bare name or @scope/name (one slash max for scope).
127
+ const pkgSegments = entry.package.split('/').filter(Boolean);
128
+ if (entry.package.includes('\\') || pkgSegments.length > 2) {
129
+ return { ok: false, error: `Registry package must be a package name (got: ${entry.package}).` };
130
+ }
131
+ if (pkgSegments.length === 2 && !entry.package.startsWith('@')) {
132
+ return { ok: false, error: `Registry package must be a package name (got: ${entry.package}).` };
133
+ }
134
+ const directoryError = checkRelativeField('directory', entry.directory);
135
+ if (directoryError)
136
+ return { ok: false, error: directoryError };
137
+ return { ok: true };
138
+ }
139
+ /**
140
+ * Ensure `sourceDir` resolves inside `sourceRoot/packages/<package>`.
141
+ */
142
+ export function resolveContainedSourceDir(sourceRoot, entry) {
143
+ const entryCheck = validateRegistryEntry(entry);
144
+ if (!entryCheck.ok)
145
+ return entryCheck;
146
+ const rootReal = realPathIfExists(sourceRoot);
147
+ const packagesRoot = resolve(rootReal, 'packages');
148
+ const packageRoot = resolve(packagesRoot, entry.package);
149
+ if (escapesRoot(packagesRoot, packageRoot)) {
150
+ return {
151
+ ok: false,
152
+ error: `Package path escapes packages/: ${entry.package}`,
153
+ };
154
+ }
155
+ const sourceDir = resolve(packageRoot, entry.directory);
156
+ if (escapesRoot(packageRoot, sourceDir)) {
157
+ return {
158
+ ok: false,
159
+ error: `Source directory escapes package root: ${entry.directory}`,
160
+ };
161
+ }
162
+ return { ok: true, sourceDir, packageRoot };
163
+ }
164
+ /**
165
+ * Ensure a source file's real path stays under sourceDir (no symlink escape).
166
+ */
167
+ export function assertSourceFileInside(sourceDir, filePath) {
168
+ const sourceReal = realPathIfExists(sourceDir);
169
+ let fileReal;
170
+ try {
171
+ fileReal = existsSync(filePath) ? realpathSync(filePath) : resolve(filePath);
172
+ }
173
+ catch (error) {
174
+ return {
175
+ ok: false,
176
+ error: `Cannot resolve source file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
177
+ };
178
+ }
179
+ if (escapesRoot(sourceReal, fileReal)) {
180
+ return {
181
+ ok: false,
182
+ error: `Source file escapes component directory: ${filePath}`,
183
+ };
184
+ }
185
+ return { ok: true, realFile: fileReal };
186
+ }
@@ -0,0 +1,6 @@
1
+ import type { LlmsBundle, LlmsBundleOptions } from './types.js';
2
+ /**
3
+ * Build `llms.txt`, `llms-full.txt` and per-component Markdown from catalog data.
4
+ * Pure: no filesystem.
5
+ */
6
+ export declare function buildLlmsBundle(options: LlmsBundleOptions): LlmsBundle;
@@ -0,0 +1,47 @@
1
+ import { componentDocPath, formatComponentMarkdown, formatLlmsFullTxt, formatLlmsTxt, } from "./format.js";
2
+ function entriesFromOptions(options) {
3
+ if (options.catalog) {
4
+ return [...options.catalog.entries];
5
+ }
6
+ if (options.manifests) {
7
+ return [...options.manifests];
8
+ }
9
+ throw new Error('LlmsBundleOptions requires either catalog or manifests.');
10
+ }
11
+ /**
12
+ * Build `llms.txt`, `llms-full.txt` and per-component Markdown from catalog data.
13
+ * Pure: no filesystem.
14
+ */
15
+ export function buildLlmsBundle(options) {
16
+ const entries = entriesFromOptions(options);
17
+ const baseUrl = options.baseUrl ?? '';
18
+ const title = options.title ?? 'Texuli';
19
+ const summary = options.summary ??
20
+ 'Texuli is a Svelte 5 component system with a closed agent catalog and a sink-free renderer.';
21
+ const llmsTxt = formatLlmsTxt(entries, {
22
+ title,
23
+ summary,
24
+ intro: options.intro,
25
+ baseUrl,
26
+ splitTiers: options.splitTiers,
27
+ });
28
+ const llmsFullTxt = formatLlmsFullTxt(entries, { title, baseUrl });
29
+ const components = [...entries]
30
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
31
+ .map((entry) => ({
32
+ path: componentDocPath(entry.id),
33
+ body: formatComponentMarkdown(entry, { baseUrl, fromComponents: true }),
34
+ }));
35
+ const files = [
36
+ { path: 'llms.txt', body: llmsTxt },
37
+ { path: 'llms-full.txt', body: llmsFullTxt },
38
+ ...components,
39
+ ].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
40
+ return {
41
+ llmsTxt,
42
+ llmsFullTxt,
43
+ components,
44
+ files,
45
+ entryCount: entries.length,
46
+ };
47
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI: generate llms.txt + agent docs from the live catalog manifests.
4
+ *
5
+ * pnpm exec tsx packages/tools/src/llms/cli.ts --out apps/website/public/llms --json
6
+ */
7
+ export interface LlmsCliArgs {
8
+ outDir: string;
9
+ baseUrl: string;
10
+ title: string;
11
+ dryRun: boolean;
12
+ json: boolean;
13
+ help: boolean;
14
+ }
15
+ export declare function parseLlmsArgs(argv: string[]): LlmsCliArgs;
16
+ export declare function runLlmsCli(argv?: string[]): Promise<{
17
+ ok: true;
18
+ files: number;
19
+ outDir: string;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ }>;
24
+ export declare function isMainModule(metaUrl: string): boolean;
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ /**
4
+ * CLI: generate llms.txt + agent docs from the live catalog manifests.
5
+ *
6
+ * pnpm exec tsx packages/tools/src/llms/cli.ts --out apps/website/public/llms --json
7
+ */
8
+ import { fileURLToPath } from 'node:url';
9
+ import { buildCatalog } from '@texuli/catalog/build';
10
+ import { discoverManifestGroups } from '@texuli/catalog/build/node';
11
+ import { buildLlmsBundle } from "./bundle.js";
12
+ import { writeLlmsBundle } from "./write.js";
13
+ export function parseLlmsArgs(argv) {
14
+ const args = {
15
+ outDir: 'dist/llms',
16
+ baseUrl: '',
17
+ title: 'Texuli',
18
+ dryRun: false,
19
+ json: false,
20
+ help: false,
21
+ };
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i] ?? '';
24
+ if (arg === '--help' || arg === '-h')
25
+ args.help = true;
26
+ else if (arg === '--dry-run')
27
+ args.dryRun = true;
28
+ else if (arg === '--json')
29
+ args.json = true;
30
+ else if (arg === '--out' || arg === '-o') {
31
+ args.outDir = argv[++i] ?? args.outDir;
32
+ }
33
+ else if (arg === '--base-url') {
34
+ args.baseUrl = argv[++i] ?? '';
35
+ }
36
+ else if (arg === '--title') {
37
+ args.title = argv[++i] ?? args.title;
38
+ }
39
+ else if (arg.startsWith('-')) {
40
+ throw new Error(`Unknown flag: ${arg}`);
41
+ }
42
+ }
43
+ return args;
44
+ }
45
+ export async function runLlmsCli(argv = process.argv.slice(2)) {
46
+ try {
47
+ const args = parseLlmsArgs(argv);
48
+ if (args.help) {
49
+ const help = `Usage: texuli-llms [--out <dir>] [--base-url <url>] [--title <name>] [--dry-run] [--json]
50
+
51
+ Generate llms.txt, llms-full.txt and components/<id>.md from catalog manifests.
52
+ `;
53
+ if (!args.json)
54
+ process.stdout.write(help);
55
+ else
56
+ process.stdout.write(`${JSON.stringify({ ok: true, help }, null, 2)}\n`);
57
+ return { ok: true, files: 0, outDir: args.outDir };
58
+ }
59
+ const groups = await discoverManifestGroups();
60
+ const { catalog, hash } = await buildCatalog(groups);
61
+ const bundle = buildLlmsBundle({
62
+ catalog,
63
+ baseUrl: args.baseUrl,
64
+ title: args.title,
65
+ });
66
+ const result = await writeLlmsBundle(args.outDir, bundle, { dryRun: args.dryRun });
67
+ if (args.json) {
68
+ process.stdout.write(`${JSON.stringify({
69
+ ok: true,
70
+ outDir: result.outDir,
71
+ dryRun: result.dryRun,
72
+ entryCount: bundle.entryCount,
73
+ files: result.written,
74
+ catalogHash: hash,
75
+ }, null, 2)}\n`);
76
+ }
77
+ else {
78
+ process.stdout.write(`Wrote ${result.written.length} files to ${result.outDir} (${bundle.entryCount} components)${result.dryRun ? ' [dry-run]' : ''}\n`);
79
+ }
80
+ return { ok: true, files: result.written.length, outDir: result.outDir };
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ process.stderr.write(`${message}\n`);
85
+ return { ok: false, error: message };
86
+ }
87
+ }
88
+ export function isMainModule(metaUrl) {
89
+ const entry = process.argv[1];
90
+ if (!entry)
91
+ return false;
92
+ try {
93
+ return fileURLToPath(metaUrl) === entry;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ if (isMainModule(import.meta.url)) {
100
+ runLlmsCli().then((result) => {
101
+ if (!result.ok)
102
+ process.exitCode = 1;
103
+ });
104
+ }
@@ -0,0 +1,35 @@
1
+ import type { CatalogEntry } from '@texuli/catalog/build';
2
+ import type { CatalogManifest } from '@texuli/catalog/manifest';
3
+ /** Stable path for a component doc: `components/<id>.md`. */
4
+ export declare function componentDocPath(id: string): string;
5
+ /** Link href for llms.txt (relative or absolute). */
6
+ export declare function componentHref(id: string, baseUrl?: string): string;
7
+ /**
8
+ * Full Markdown page for one catalog entry (agent-facing).
9
+ * No HTML; plain Markdown only.
10
+ */
11
+ export declare function formatComponentMarkdown(entry: CatalogManifest | CatalogEntry, options?: {
12
+ baseUrl?: string;
13
+ relatedLinks?: boolean;
14
+ fromComponents?: boolean;
15
+ }): string;
16
+ /** One llms.txt link line. */
17
+ export declare function formatLlmsLink(entry: CatalogManifest | CatalogEntry, baseUrl?: string): string;
18
+ /**
19
+ * Compact `llms.txt` index (llmstxt.org-style).
20
+ * Sections: overview, agent-emittable (Tier U + developer), app chrome.
21
+ */
22
+ export declare function formatLlmsTxt(entries: readonly (CatalogManifest | CatalogEntry)[], options?: {
23
+ title?: string;
24
+ summary?: string;
25
+ intro?: readonly string[];
26
+ baseUrl?: string;
27
+ splitTiers?: boolean;
28
+ }): string;
29
+ /**
30
+ * `llms-full.txt`: every component Markdown page concatenated with clear separators.
31
+ */
32
+ export declare function formatLlmsFullTxt(entries: readonly (CatalogManifest | CatalogEntry)[], options?: {
33
+ baseUrl?: string;
34
+ title?: string;
35
+ }): string;