docspack 0.0.1 → 0.1.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/bin/docspack.js +25 -0
  4. package/dist/build.d.ts +31 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +435 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +763 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/config.d.ts +41 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +118 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/db.d.ts +60 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +204 -0
  19. package/dist/db.js.map +1 -0
  20. package/dist/discovery.d.ts +31 -0
  21. package/dist/discovery.d.ts.map +1 -0
  22. package/dist/discovery.js +126 -0
  23. package/dist/discovery.js.map +1 -0
  24. package/dist/doctor.d.ts +25 -0
  25. package/dist/doctor.d.ts.map +1 -0
  26. package/dist/doctor.js +276 -0
  27. package/dist/doctor.js.map +1 -0
  28. package/dist/document.d.ts +13 -0
  29. package/dist/document.d.ts.map +1 -0
  30. package/dist/document.js +47 -0
  31. package/dist/document.js.map +1 -0
  32. package/dist/errors.d.ts +9 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/errors.js +10 -0
  35. package/dist/errors.js.map +1 -0
  36. package/dist/exports.d.ts +20 -0
  37. package/dist/exports.d.ts.map +1 -0
  38. package/dist/exports.js +100 -0
  39. package/dist/exports.js.map +1 -0
  40. package/dist/feedback.d.ts +68 -0
  41. package/dist/feedback.d.ts.map +1 -0
  42. package/dist/feedback.js +0 -0
  43. package/dist/feedback.js.map +1 -0
  44. package/dist/html.d.ts +4 -0
  45. package/dist/html.d.ts.map +1 -0
  46. package/dist/html.js +23 -0
  47. package/dist/html.js.map +1 -0
  48. package/dist/http.d.ts +30 -0
  49. package/dist/http.d.ts.map +1 -0
  50. package/dist/http.js +144 -0
  51. package/dist/http.js.map +1 -0
  52. package/dist/index.d.ts +25 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +25 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/init/detect.d.ts +16 -0
  57. package/dist/init/detect.d.ts.map +1 -0
  58. package/dist/init/detect.js +120 -0
  59. package/dist/init/detect.js.map +1 -0
  60. package/dist/init/plan.d.ts +43 -0
  61. package/dist/init/plan.d.ts.map +1 -0
  62. package/dist/init/plan.js +145 -0
  63. package/dist/init/plan.js.map +1 -0
  64. package/dist/init/run.d.ts +28 -0
  65. package/dist/init/run.d.ts.map +1 -0
  66. package/dist/init/run.js +96 -0
  67. package/dist/init/run.js.map +1 -0
  68. package/dist/init/templates.d.ts +24 -0
  69. package/dist/init/templates.d.ts.map +1 -0
  70. package/dist/init/templates.js +181 -0
  71. package/dist/init/templates.js.map +1 -0
  72. package/dist/init/write.d.ts +20 -0
  73. package/dist/init/write.d.ts.map +1 -0
  74. package/dist/init/write.js +56 -0
  75. package/dist/init/write.js.map +1 -0
  76. package/dist/kinds.d.ts +14 -0
  77. package/dist/kinds.d.ts.map +1 -0
  78. package/dist/kinds.js +15 -0
  79. package/dist/kinds.js.map +1 -0
  80. package/dist/llms-txt.d.ts +25 -0
  81. package/dist/llms-txt.d.ts.map +1 -0
  82. package/dist/llms-txt.js +94 -0
  83. package/dist/llms-txt.js.map +1 -0
  84. package/dist/mcp.d.ts +15 -0
  85. package/dist/mcp.d.ts.map +1 -0
  86. package/dist/mcp.js +158 -0
  87. package/dist/mcp.js.map +1 -0
  88. package/dist/preview.d.ts +18 -0
  89. package/dist/preview.d.ts.map +1 -0
  90. package/dist/preview.js +72 -0
  91. package/dist/preview.js.map +1 -0
  92. package/dist/prompt.d.ts +27 -0
  93. package/dist/prompt.d.ts.map +1 -0
  94. package/dist/prompt.js +79 -0
  95. package/dist/prompt.js.map +1 -0
  96. package/dist/search.d.ts +41 -0
  97. package/dist/search.d.ts.map +1 -0
  98. package/dist/search.js +60 -0
  99. package/dist/search.js.map +1 -0
  100. package/dist/snippet.d.ts +20 -0
  101. package/dist/snippet.d.ts.map +1 -0
  102. package/dist/snippet.js +29 -0
  103. package/dist/snippet.js.map +1 -0
  104. package/dist/spec.d.ts +38 -0
  105. package/dist/spec.d.ts.map +1 -0
  106. package/dist/spec.js +105 -0
  107. package/dist/spec.js.map +1 -0
  108. package/dist/style.d.ts +33 -0
  109. package/dist/style.d.ts.map +1 -0
  110. package/dist/style.js +94 -0
  111. package/dist/style.js.map +1 -0
  112. package/dist/submit.d.ts +61 -0
  113. package/dist/submit.d.ts.map +1 -0
  114. package/dist/submit.js +111 -0
  115. package/dist/submit.js.map +1 -0
  116. package/dist/sync.d.ts +29 -0
  117. package/dist/sync.d.ts.map +1 -0
  118. package/dist/sync.js +73 -0
  119. package/dist/sync.js.map +1 -0
  120. package/dist/verify.d.ts +44 -0
  121. package/dist/verify.d.ts.map +1 -0
  122. package/dist/verify.js +291 -0
  123. package/dist/verify.js.map +1 -0
  124. package/package.json +60 -5
  125. package/src/build.ts +572 -0
  126. package/src/cli.ts +883 -0
  127. package/src/config.ts +158 -0
  128. package/src/db.ts +261 -0
  129. package/src/discovery.ts +161 -0
  130. package/src/doctor.ts +344 -0
  131. package/src/document.ts +59 -0
  132. package/src/errors.ts +10 -0
  133. package/src/exports.ts +120 -0
  134. package/src/feedback.ts +0 -0
  135. package/src/html.ts +24 -0
  136. package/src/http.ts +190 -0
  137. package/src/index.ts +132 -0
  138. package/src/init/detect.ts +142 -0
  139. package/src/init/plan.ts +215 -0
  140. package/src/init/run.ts +142 -0
  141. package/src/init/templates.ts +200 -0
  142. package/src/init/write.ts +83 -0
  143. package/src/kinds.ts +17 -0
  144. package/src/llms-txt.ts +116 -0
  145. package/src/mcp.ts +196 -0
  146. package/src/preview.ts +98 -0
  147. package/src/prompt.ts +103 -0
  148. package/src/search.ts +96 -0
  149. package/src/snippet.ts +30 -0
  150. package/src/spec.ts +138 -0
  151. package/src/style.ts +111 -0
  152. package/src/submit.ts +189 -0
  153. package/src/sync.ts +112 -0
  154. package/src/verify.ts +355 -0
  155. package/bin/cli.js +0 -2
package/src/cli.ts ADDED
@@ -0,0 +1,883 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { posix } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { defaultStorePath, Store, silenceSqliteWarning } from "./db.js";
7
+ import { discoverPackages } from "./discovery.js";
8
+ import { type DoctorReport, runDoctor } from "./doctor.js";
9
+ import { DocspackError } from "./errors.js";
10
+ import { renderTree } from "./init/write.js";
11
+ import { previewPackage } from "./preview.js";
12
+ import { DEFAULT_LIMIT, DEFAULT_MAX_TOKENS, queryDocs, renderAnswer } from "./search.js";
13
+ import { AGENTS_SNIPPET, FEEDBACK_SNIPPET } from "./snippet.js";
14
+ import { syncProject } from "./sync.js";
15
+ import { verifyProject } from "./verify.js";
16
+
17
+ // The MCP SDK costs ~200ms to load and the Markdown converter ~60ms. Neither is needed to
18
+ // answer a query, so the commands that need them import them at the point of use. Every
19
+ // `docspack ask` pays for what it uses and nothing else.
20
+
21
+ const OPTIONS = {
22
+ help: { type: "boolean", short: "h" },
23
+ version: { type: "boolean", short: "v" },
24
+ json: { type: "boolean" },
25
+ quiet: { type: "boolean", short: "q" },
26
+ cwd: { type: "string" },
27
+ store: { type: "string" },
28
+ force: { type: "boolean" },
29
+ package: { type: "string", short: "p" },
30
+ limit: { type: "string" },
31
+ "max-tokens": { type: "string" },
32
+ all: { type: "boolean" },
33
+ from: { type: "string" },
34
+ openapi: { type: "string" },
35
+ out: { type: "string" },
36
+ name: { type: "string" },
37
+ "pkg-version": { type: "string" },
38
+ pages: { type: "string" },
39
+ mirror: { type: "string" },
40
+ mode: { type: "string" },
41
+ template: { type: "string" },
42
+ community: { type: "boolean" },
43
+ workflow: { type: "boolean" },
44
+ "no-workflow": { type: "boolean" },
45
+ "no-build": { type: "boolean" },
46
+ yes: { type: "boolean", short: "y" },
47
+ "dry-run": { type: "boolean" },
48
+ strict: { type: "boolean" },
49
+ "package-dir": { type: "string" },
50
+ chunk: { type: "string" },
51
+ kind: { type: "string" },
52
+ evidence: { type: "string" },
53
+ expected: { type: "string" },
54
+ actual: { type: "string" },
55
+ repro: { type: "string" },
56
+ } as const;
57
+
58
+ const HELP = `docspack — local, version-locked documentation for AI agents
59
+
60
+ Usage
61
+ docspack <command> [options]
62
+
63
+ Commands
64
+ sync Index the docs packages this project depends on
65
+ ask <question> Answer from the local index — the command to give an agent
66
+ search <query> Same index, formatted for a human reading the terminal
67
+ list Show this project's docs packages and their index state
68
+ verify Check the docs still describe the code you installed
69
+ feedback <sub> Record documentation problems: add, list, submit, remove
70
+ mcp Serve the index over MCP instead, as a long-lived process
71
+ sources List curated sources that \`docspack build\` can fetch
72
+
73
+ Authoring
74
+ init Scaffold a documentation package, then build and check it
75
+ build [source] Generate the .llms/ payload for publishing
76
+ doctor Check a package the way the indexer and a reviewer would
77
+ preview <query> Answer a query from the local package, as an agent would
78
+
79
+ How it works
80
+ Documentation ships as npm packages named @vendor/docspack or
81
+ @docspack-community/<name>. Add them to package.json, run \`docspack sync\`, and
82
+ every chunk is indexed into a shared SQLite database with FTS5. Agents query
83
+ that index locally: no network, no scraping, always the installed version.
84
+
85
+ Giving an agent access
86
+ Any agent with a shell can run \`docspack ask\`, so one line in AGENTS.md or
87
+ CLAUDE.md is the whole setup — no server, no per-agent configuration, and
88
+ nothing resident when nobody is asking:
89
+
90
+ ${AGENTS_SNIPPET.map((line) => ` ${line}`).join("\n")}
91
+
92
+ Add the block below as well if you want the agent to record documentation
93
+ problems it runs into. It writes to a local file; nothing is sent:
94
+
95
+ ${FEEDBACK_SNIPPET.map((line) => ` ${line}`).join("\n")}
96
+
97
+ \`docspack mcp\` serves the same index over the Model Context Protocol for
98
+ clients that prefer a tool definition. Both return identical text.
99
+
100
+ Recording documentation problems
101
+ \`docspack feedback add\` writes a finding to .docspack/feedback.jsonl in this
102
+ project. Nothing is transmitted: docspack contains no code that can send a
103
+ report anywhere, and it never will.
104
+
105
+ docspack feedback add --chunk @acme/docspack@1.4.0/api-auth \\
106
+ --kind drift --evidence "client.setKey is not exported; setApiKey is"
107
+
108
+ A finding has to be falsifiable. \`drift\` must name the identifier that
109
+ drifted. \`incorrect\` and \`missing\` must carry --expected, --actual and
110
+ --repro, so a claim that cannot show its work cannot be recorded at all.
111
+ Recording the same problem again increments a counter instead of adding a
112
+ second entry.
113
+
114
+ \`docspack feedback submit\` prints a prefilled GitHub issue URL for every
115
+ finding whose vendor asked to receive it, and routes nothing anywhere else.
116
+ A vendor opts in from their own package.json:
117
+
118
+ "docspack": {
119
+ "feedback": {
120
+ "github": "acme/sdk",
121
+ "labels": ["documentation"],
122
+ "accepts": ["drift"]
123
+ }
124
+ }
125
+
126
+ No \`feedback\` block means no channel and no route. \`accepts\` narrows which
127
+ kinds reach them. A human opens the link, reads it, and files it.
128
+
129
+ Options
130
+ -y, --yes init: skip prompts and use flags plus detected defaults
131
+ --dry-run init: print the file tree and write nothing
132
+ --mirror <id|url> init: bootstrap from a published llms.txt
133
+ --community init: scaffold under @docspack-community
134
+ --no-workflow init: skip the release workflow
135
+ --no-build init: scaffold only
136
+ --template <t> init: full (default) or minimal
137
+ --strict doctor: treat warnings as failures
138
+ --package-dir <d> doctor, preview, verify: the package to inspect (default: .)
139
+ --chunk <id> feedback add: the chunk the problem is in
140
+ --kind <k> feedback add: drift, incorrect or missing
141
+ --evidence <text> feedback add: the claim, in one line
142
+ --expected <text> feedback add: what the documentation led you to expect
143
+ --actual <text> feedback add: what happened instead
144
+ --repro <code> feedback add: code that demonstrates it
145
+ --force sync: re-index packages already in the store; init: overwrite files
146
+ -p, --package <s> search: only packages whose name contains this text
147
+ --limit <n> search: maximum chunks to return (default ${DEFAULT_LIMIT})
148
+ --max-tokens <n> search: token ceiling for the result set (default ${DEFAULT_MAX_TOKENS})
149
+ --all search: the whole store, not just this project;
150
+ feedback remove: every recorded finding
151
+ --from <dir> build: directory of Markdown to package
152
+ --openapi <file> build: OpenAPI JSON to package, one chunk per operation
153
+ --name <name> build: package name, e.g. @acme/docspack
154
+ --pkg-version <v> build: package version
155
+ --out <dir> build: output directory (default: the current directory)
156
+ --pages <n> build: maximum documents to fetch from a remote source
157
+ --store <path> Use a different index (default: ${defaultStorePath()})
158
+ --cwd <dir> Run in a different directory
159
+ --json Machine-readable output
160
+ -q, --quiet Only print errors
161
+ -h, --help Show this help
162
+ -v, --version Show the version
163
+
164
+ Examples
165
+ npx docspack sync
166
+ npx docspack ask "how do I verify a webhook signature"
167
+ npx docspack search "webhook signature" --package stripe
168
+ npx docspack init
169
+ npx docspack init --name @acme/docspack --from ./docs --yes
170
+ npx docspack doctor --strict
171
+ npx docspack preview "how do I authenticate"
172
+ npx docspack verify
173
+ npx docspack feedback list
174
+ npx docspack feedback submit
175
+ `;
176
+
177
+ type Values = {
178
+ [K in keyof typeof OPTIONS]?: (typeof OPTIONS)[K]["type"] extends "boolean" ? boolean : string;
179
+ };
180
+
181
+ const color = process.env.NO_COLOR === undefined && process.stdout.isTTY === true;
182
+ const paint = (code: string, text: string): string =>
183
+ color ? `\u001b[${code}m${text}\u001b[0m` : text;
184
+ const bold = (text: string): string => paint("1", text);
185
+ const dim = (text: string): string => paint("2", text);
186
+ const green = (text: string): string => paint("32", text);
187
+ const yellow = (text: string): string => paint("33", text);
188
+ const red = (text: string): string => paint("31", text);
189
+
190
+ function version(): string {
191
+ const url = new URL("../package.json", import.meta.url);
192
+ const parsed: unknown = JSON.parse(readFileSync(fileURLToPath(url), "utf8"));
193
+ const value = (parsed as { version?: unknown }).version;
194
+ return typeof value === "string" ? value : "0.0.0";
195
+ }
196
+
197
+ function integer(raw: string | undefined, flag: string): number | undefined {
198
+ if (raw === undefined) return undefined;
199
+ const value = Number(raw);
200
+ if (!Number.isInteger(value) || value < 0) {
201
+ throw new DocspackError(`${flag} expects a non-negative integer, got "${raw}"`);
202
+ }
203
+ return value;
204
+ }
205
+
206
+ function plural(count: number, noun: string): string {
207
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
208
+ }
209
+
210
+ /** First few lines of prose from a chunk, skipping blanks and provenance comments. */
211
+ function preview(content: string, lines: number): string[] {
212
+ return content
213
+ .split("\n")
214
+ .filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("<!--"))
215
+ .slice(0, lines);
216
+ }
217
+
218
+ function reportDoctor(report: DoctorReport, quiet: boolean): void {
219
+ for (const finding of report.findings) {
220
+ const mark =
221
+ finding.severity === "error"
222
+ ? red("error")
223
+ : finding.severity === "warn"
224
+ ? yellow("warn")
225
+ : dim("info");
226
+ const where = finding.where === undefined ? "" : dim(` ${finding.where}`);
227
+ process.stderr.write(`${mark} ${finding.message}${where}\n`);
228
+ if (finding.fix !== undefined) process.stderr.write(` ${dim(finding.fix)}\n`);
229
+ }
230
+ if (quiet) return;
231
+
232
+ if (report.ok) {
233
+ const notes = report.findings.length > 0 ? ` (${plural(report.findings.length, "note")})` : "";
234
+ process.stdout.write(
235
+ `${green("ok")} ${plural(report.chunks, "chunk")}, ~${report.tokens} tokens${notes}\n`,
236
+ );
237
+ } else {
238
+ process.stderr.write(`${red("failed")} the package is not ready to publish\n`);
239
+ }
240
+ }
241
+
242
+ function openStore(values: Values): Store {
243
+ return Store.open(values.store ?? defaultStorePath());
244
+ }
245
+
246
+ async function main(argv: readonly string[]): Promise<number> {
247
+ let values: Values;
248
+ let positionals: string[];
249
+ try {
250
+ const parsed = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true });
251
+ values = parsed.values;
252
+ positionals = parsed.positionals;
253
+ } catch (error) {
254
+ process.stderr.write(
255
+ `${red("error")} ${error instanceof Error ? error.message : String(error)}\n\n`,
256
+ );
257
+ process.stderr.write(HELP);
258
+ return 2;
259
+ }
260
+
261
+ if (values.version === true) {
262
+ process.stdout.write(`${version()}\n`);
263
+ return 0;
264
+ }
265
+
266
+ const [command = "help", ...rest] = positionals;
267
+ if (values.help === true || command === "help") {
268
+ process.stdout.write(HELP);
269
+ return 0;
270
+ }
271
+
272
+ const cwd = values.cwd ?? process.cwd();
273
+ const quiet = values.quiet === true;
274
+ const json = values.json === true;
275
+
276
+ switch (command) {
277
+ case "sync": {
278
+ const store = openStore(values);
279
+ try {
280
+ const result = await syncProject({
281
+ cwd,
282
+ store,
283
+ ...(values.force === true ? { force: true } : {}),
284
+ ...(quiet || json
285
+ ? {}
286
+ : {
287
+ onProgress: (message: string): void => {
288
+ process.stderr.write(`${dim(message)}\n`);
289
+ },
290
+ }),
291
+ });
292
+
293
+ if (json) {
294
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
295
+ return result.packages.length === 0 && result.problems.length > 0 ? 1 : 0;
296
+ }
297
+
298
+ for (const pkg of result.packages) {
299
+ const mark = pkg.status === "indexed" ? green("+") : dim("=");
300
+ const trust = pkg.trusted ? "" : ` ${yellow("(community)")}`;
301
+ process.stdout.write(
302
+ `${mark} ${bold(pkg.id)} ${pkg.chunks} chunks ${dim(pkg.status)}${trust}\n`,
303
+ );
304
+ }
305
+ for (const problem of result.problems) {
306
+ process.stderr.write(`${yellow("!")} ${problem}\n`);
307
+ }
308
+ if (result.packages.length === 0 && !quiet) {
309
+ process.stdout.write(
310
+ "No documentation packages found. Add one, for example:\n pnpm add -D @docspack-community/<name>\n",
311
+ );
312
+ } else if (!quiet) {
313
+ process.stdout.write(
314
+ [
315
+ "",
316
+ "Give an agent access with one line in AGENTS.md or CLAUDE.md:",
317
+ ...AGENTS_SNIPPET.map((line) => dim(` ${line}`)),
318
+ "",
319
+ "Add this too if you want the agent to record documentation problems:",
320
+ ...FEEDBACK_SNIPPET.map((line) => dim(` ${line}`)),
321
+ "",
322
+ ].join("\n"),
323
+ );
324
+ }
325
+ return 0;
326
+ } finally {
327
+ store.close();
328
+ }
329
+ }
330
+
331
+ case "ask": {
332
+ const question = rest.join(" ");
333
+ if (question.length === 0) {
334
+ throw new DocspackError('Usage: docspack ask "<question>"');
335
+ }
336
+
337
+ const store = openStore(values);
338
+ try {
339
+ const result = await queryDocs({
340
+ cwd,
341
+ store,
342
+ query: question,
343
+ ...(values.package === undefined ? {} : { packageFilter: values.package }),
344
+ ...(() => {
345
+ const limit = integer(values.limit, "--limit");
346
+ return limit === undefined ? {} : { limit };
347
+ })(),
348
+ ...(() => {
349
+ const maxTokens = integer(values["max-tokens"], "--max-tokens");
350
+ return maxTokens === undefined ? {} : { maxTokens };
351
+ })(),
352
+ ...(values.all === true ? { scoped: false } : {}),
353
+ });
354
+
355
+ if (json) {
356
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
357
+ } else {
358
+ // Exactly what the MCP tool returns, so both interfaces answer identically.
359
+ process.stdout.write(`${renderAnswer(result, question)}\n`);
360
+ }
361
+ // Answering "nothing matched" is a successful answer, not a failed command.
362
+ return 0;
363
+ } finally {
364
+ store.close();
365
+ }
366
+ }
367
+
368
+ case "search": {
369
+ const query = rest.join(" ");
370
+ if (query.length === 0) throw new DocspackError("Usage: docspack search <query>");
371
+
372
+ const store = openStore(values);
373
+ try {
374
+ const result = await queryDocs({
375
+ cwd,
376
+ store,
377
+ query,
378
+ ...(values.package === undefined ? {} : { packageFilter: values.package }),
379
+ ...(() => {
380
+ const limit = integer(values.limit, "--limit");
381
+ return limit === undefined ? {} : { limit };
382
+ })(),
383
+ ...(() => {
384
+ const maxTokens = integer(values["max-tokens"], "--max-tokens");
385
+ return maxTokens === undefined ? {} : { maxTokens };
386
+ })(),
387
+ ...(values.all === true ? { scoped: false } : {}),
388
+ });
389
+
390
+ if (json) {
391
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
392
+ return 0;
393
+ }
394
+ if (result.hits.length === 0) {
395
+ process.stdout.write(`No local documentation matched "${query}".\n`);
396
+ return 1;
397
+ }
398
+
399
+ for (const hit of result.hits) {
400
+ const trust = hit.trusted ? "" : ` ${yellow("(community)")}`;
401
+ process.stdout.write(
402
+ `${bold(hit.chunkId)}${trust} ${dim(`${hit.tokens} tokens · ${hit.filePath}`)}\n`,
403
+ );
404
+ for (const line of preview(hit.content, 4)) {
405
+ process.stdout.write(` ${line}\n`);
406
+ }
407
+ process.stdout.write("\n");
408
+ }
409
+ process.stdout.write(
410
+ dim(`${result.tokens} tokens across ${plural(result.hits.length, "chunk")}\n`),
411
+ );
412
+ return 0;
413
+ } finally {
414
+ store.close();
415
+ }
416
+ }
417
+
418
+ case "list": {
419
+ const store = openStore(values);
420
+ try {
421
+ const { packages, problems } = await discoverPackages(cwd);
422
+ const rows = packages.map((pkg) => ({
423
+ id: pkg.id,
424
+ name: pkg.name,
425
+ version: pkg.version,
426
+ chunks: pkg.manifest.chunks.length,
427
+ trusted: pkg.trusted,
428
+ indexed: store.hasPackage(pkg.id),
429
+ }));
430
+
431
+ if (json) {
432
+ process.stdout.write(`${JSON.stringify({ packages: rows, problems }, null, 2)}\n`);
433
+ return 0;
434
+ }
435
+ if (rows.length === 0) {
436
+ process.stdout.write("This project has no documentation packages.\n");
437
+ }
438
+ for (const row of rows) {
439
+ const state = row.indexed ? green("indexed") : yellow("not indexed");
440
+ const trust = row.trusted ? "" : ` ${yellow("(community)")}`;
441
+ process.stdout.write(`${bold(row.id)} ${row.chunks} chunks ${state}${trust}\n`);
442
+ }
443
+ for (const problem of problems) process.stderr.write(`${yellow("!")} ${problem}\n`);
444
+ return 0;
445
+ } finally {
446
+ store.close();
447
+ }
448
+ }
449
+
450
+ case "verify": {
451
+ const report = await verifyProject({
452
+ cwd,
453
+ ...(values["package-dir"] === undefined ? {} : { packageDir: values["package-dir"] }),
454
+ });
455
+
456
+ if (json) {
457
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
458
+ return report.ok ? 0 : 1;
459
+ }
460
+
461
+ if (report.packages.length === 0) {
462
+ process.stdout.write("This project has no documentation packages.\n");
463
+ return 0;
464
+ }
465
+
466
+ for (const pkg of report.packages) {
467
+ if (pkg.status === "skipped") {
468
+ if (!quiet)
469
+ process.stdout.write(`${dim("-")} ${bold(pkg.id)} ${dim(pkg.reason ?? "")}\n`);
470
+ continue;
471
+ }
472
+
473
+ const mark = pkg.findings.length === 0 ? green("ok") : red("drift");
474
+ process.stdout.write(
475
+ `${mark} ${bold(pkg.id)} ${dim(`documents ${pkg.documents ?? ""} · ${plural(pkg.checked, "name")} checked, ${pkg.matched} declared`)}\n`,
476
+ );
477
+ for (const finding of pkg.findings) {
478
+ process.stderr.write(
479
+ ` ${yellow(finding.entity)} is not declared; did the API become ${bold(finding.suggestion)}?\n`,
480
+ );
481
+ process.stderr.write(` ${dim(`${finding.chunkId} · ${finding.file}`)}\n`);
482
+ }
483
+ }
484
+
485
+ if (!report.ok && !quiet) {
486
+ process.stderr.write(
487
+ `\n${dim("These are local findings. Fix the documentation, or report them to the\nmaintainer yourself — docspack never sends anything anywhere.")}\n`,
488
+ );
489
+ }
490
+ return report.ok ? 0 : 1;
491
+ }
492
+
493
+ case "feedback": {
494
+ const { addFinding, feedbackPath, readFeedback, removeFindings } = await import(
495
+ "./feedback.js"
496
+ );
497
+ const [sub = "list", ...args] = rest;
498
+
499
+ switch (sub) {
500
+ case "add": {
501
+ if (values.chunk === undefined || values.evidence === undefined) {
502
+ throw new DocspackError(
503
+ 'Usage: docspack feedback add --chunk <id> --kind <k> --evidence "<claim>"',
504
+ { hint: "See `docspack --help` for what each kind requires." },
505
+ );
506
+ }
507
+
508
+ const { finding, repeat } = await addFinding({
509
+ cwd,
510
+ chunkId: values.chunk,
511
+ kind: values.kind ?? "drift",
512
+ evidence: values.evidence,
513
+ ...(values.expected === undefined ? {} : { expected: values.expected }),
514
+ ...(values.actual === undefined ? {} : { actual: values.actual }),
515
+ ...(values.repro === undefined ? {} : { repro: values.repro }),
516
+ });
517
+
518
+ if (json) {
519
+ process.stdout.write(`${JSON.stringify({ finding, repeat }, null, 2)}\n`);
520
+ return 0;
521
+ }
522
+ if (!quiet) {
523
+ process.stdout.write(
524
+ repeat
525
+ ? `${dim("=")} ${bold(finding.fingerprint)} already recorded, now ${finding.seen}×\n`
526
+ : `${green("+")} ${bold(finding.fingerprint)} recorded in ${feedbackPath(cwd)}\n`,
527
+ );
528
+ process.stdout.write(dim("Nothing was sent. Review with `docspack feedback list`.\n"));
529
+ }
530
+ return 0;
531
+ }
532
+
533
+ case "list": {
534
+ const findings = await readFeedback(cwd);
535
+
536
+ if (json) {
537
+ process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`);
538
+ return 0;
539
+ }
540
+ if (findings.length === 0) {
541
+ process.stdout.write("No documentation problems recorded in this project.\n");
542
+ return 0;
543
+ }
544
+
545
+ for (const finding of findings) {
546
+ const repeats = finding.seen > 1 ? ` ${yellow(`seen ${finding.seen}×`)}` : "";
547
+ process.stdout.write(
548
+ `${bold(finding.fingerprint)} ${finding.kind} ${dim(finding.chunkId)}${repeats}\n`,
549
+ );
550
+ process.stdout.write(` ${finding.evidence}\n`);
551
+ if (finding.repro !== undefined) {
552
+ process.stdout.write(` ${dim(`repro: ${finding.repro.split("\n")[0] ?? ""}`)}\n`);
553
+ }
554
+ }
555
+ if (!quiet) {
556
+ process.stdout.write(
557
+ dim(
558
+ `\n${plural(findings.length, "finding")} in ${feedbackPath(cwd)}, none of it sent anywhere.\n`,
559
+ ),
560
+ );
561
+ }
562
+ return 0;
563
+ }
564
+
565
+ case "submit": {
566
+ const { prepareSubmission } = await import("./submit.js");
567
+ const report = await prepareSubmission({
568
+ cwd,
569
+ docspackVersion: version(),
570
+ ...(values.package === undefined ? {} : { packageFilter: values.package }),
571
+ });
572
+
573
+ if (json) {
574
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
575
+ return 0;
576
+ }
577
+ if (report.routed.length === 0 && report.blocked.length === 0) {
578
+ process.stdout.write("No documentation problems recorded in this project.\n");
579
+ return 0;
580
+ }
581
+
582
+ for (const { packageId: id, channel } of report.channels) {
583
+ process.stdout.write(
584
+ `${bold(id)} accepts documentation problems at ${channel.github}\n`,
585
+ );
586
+ if (channel.policy !== undefined) {
587
+ process.stdout.write(` ${yellow("Read first:")} ${channel.policy}\n`);
588
+ }
589
+ }
590
+
591
+ for (const finding of report.routed) {
592
+ process.stdout.write(`\n${dim("─".repeat(72))}\n`);
593
+ process.stdout.write(`${bold(finding.fingerprint)} ${dim(finding.chunkId)}\n\n`);
594
+ process.stdout.write(`${finding.title}\n\n${finding.body}\n\n`);
595
+ if (finding.labels.length > 0) {
596
+ process.stdout.write(dim(`Labels: ${finding.labels.join(", ")}\n`));
597
+ }
598
+ if (finding.containsCode) {
599
+ process.stdout.write(
600
+ `${yellow("!")} This report quotes your reproduction. Check it for anything private before filing.\n`,
601
+ );
602
+ }
603
+ process.stdout.write(
604
+ finding.url === undefined
605
+ ? dim("Too long for a prefilled link — open a blank issue and paste the above.\n")
606
+ : `${green("Open:")} ${finding.url}\n`,
607
+ );
608
+ }
609
+
610
+ if (report.blocked.length > 0) {
611
+ process.stdout.write(`\n${bold("Not routed")}\n`);
612
+ for (const finding of report.blocked) {
613
+ const why =
614
+ finding.reason === "no-channel"
615
+ ? "the package declares no feedback channel"
616
+ : `the vendor accepts only: ${(finding.accepts ?? []).join(", ")}`;
617
+ process.stdout.write(
618
+ ` ${finding.fingerprint} ${finding.kind} ${dim(finding.chunkId)}\n ${dim(why)}\n`,
619
+ );
620
+ }
621
+ }
622
+
623
+ if (!quiet) {
624
+ process.stdout.write(
625
+ [
626
+ "",
627
+ dim("Nothing has been sent. docspack has no code that can send a report. Open a"),
628
+ dim(
629
+ "link, read what is prefilled, edit it, and file it under your own name — then",
630
+ ),
631
+ dim("`docspack feedback remove <fingerprint>` to clear it from this list."),
632
+ "",
633
+ ].join("\n"),
634
+ );
635
+ }
636
+ return 0;
637
+ }
638
+
639
+ case "remove": {
640
+ const removed = await removeFindings({
641
+ cwd,
642
+ ...(values.all === true ? { all: true } : { fingerprints: args }),
643
+ });
644
+
645
+ if (json) {
646
+ process.stdout.write(`${JSON.stringify({ removed }, null, 2)}\n`);
647
+ return 0;
648
+ }
649
+ process.stdout.write(`${green("-")} removed ${plural(removed, "finding")}\n`);
650
+ return 0;
651
+ }
652
+
653
+ default:
654
+ throw new DocspackError(`Unknown feedback subcommand "${sub}"`, {
655
+ hint: "Use add, list, submit or remove.",
656
+ });
657
+ }
658
+ }
659
+
660
+ case "mcp": {
661
+ // stdout is the protocol channel from here on.
662
+ const { startMcpServer } = await import("./mcp.js");
663
+ const store = openStore(values);
664
+ await startMcpServer({
665
+ cwd,
666
+ store,
667
+ version: version(),
668
+ storePath: values.store ?? defaultStorePath(),
669
+ });
670
+ return 0;
671
+ }
672
+
673
+ case "build": {
674
+ const { buildPackage } = await import("./build.js");
675
+ const result = await buildPackage({
676
+ out: values.out ?? cwd,
677
+ ...(rest[0] === undefined ? {} : { source: rest[0] }),
678
+ ...(values.from === undefined ? {} : { from: values.from }),
679
+ ...(values.openapi === undefined ? {} : { openapi: values.openapi }),
680
+ ...(values.name === undefined ? {} : { name: values.name }),
681
+ ...(values["pkg-version"] === undefined ? {} : { version: values["pkg-version"] }),
682
+ ...(() => {
683
+ const pages = integer(values.pages, "--pages");
684
+ return pages === undefined ? {} : { pages };
685
+ })(),
686
+ ...(quiet || json
687
+ ? {}
688
+ : {
689
+ onProgress: (message: string): void => {
690
+ process.stderr.write(`${dim(message)}\n`);
691
+ },
692
+ }),
693
+ });
694
+
695
+ if (json) {
696
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
697
+ return 0;
698
+ }
699
+ process.stdout.write(
700
+ `${green("+")} ${bold(`${result.name}@${result.version}`)} ${result.chunks} chunks ${dim(`~${result.tokens} tokens`)}\n`,
701
+ );
702
+ for (const warning of result.warnings) process.stderr.write(`${yellow("!")} ${warning}\n`);
703
+ if (!quiet) {
704
+ process.stdout.write(
705
+ `\nWrote ${result.dir}/.llms and ${result.dir}/llms.txt.\nPublish with \`npm publish\`, then add it to a project and run \`docspack sync\`.\n`,
706
+ );
707
+ }
708
+ return 0;
709
+ }
710
+
711
+ case "init": {
712
+ const { runInit } = await import("./init/run.js");
713
+ const result = await runInit({
714
+ cwd,
715
+ docspackVersion: version(),
716
+ ...(rest[0] === undefined ? {} : { name: rest[0] }),
717
+ ...(values.name === undefined ? {} : { name: values.name }),
718
+ ...(values["pkg-version"] === undefined ? {} : { version: values["pkg-version"] }),
719
+ ...(values.out === undefined ? {} : { out: values.out }),
720
+ ...(values.from === undefined ? {} : { from: values.from }),
721
+ ...(values.openapi === undefined ? {} : { openapi: values.openapi }),
722
+ ...(values.mirror === undefined ? {} : { mirror: values.mirror }),
723
+ ...(values.mode === undefined ? {} : { mode: values.mode as "standalone" | "in-repo" }),
724
+ ...(values.template === undefined
725
+ ? {}
726
+ : { template: values.template as "full" | "minimal" }),
727
+ ...(values.community === true ? { community: true } : {}),
728
+ workflow: values["no-workflow"] === true ? false : values.workflow !== false,
729
+ ...(values.yes === true || json ? { yes: true } : {}),
730
+ ...(values["dry-run"] === true ? { dryRun: true } : {}),
731
+ ...(values.force === true ? { force: true } : {}),
732
+ ...(values["no-build"] === true ? { build: false } : {}),
733
+ ...(quiet || json
734
+ ? {}
735
+ : {
736
+ onProgress: (message: string): void => {
737
+ process.stderr.write(`${dim(message)}\n`);
738
+ },
739
+ }),
740
+ });
741
+
742
+ if (json) {
743
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
744
+ return result.cancelled ? 1 : 0;
745
+ }
746
+ if (result.cancelled) {
747
+ process.stdout.write("Nothing was written.\n");
748
+ return 1;
749
+ }
750
+
751
+ process.stdout.write(`${renderTree(result.plan, result.written)}\n`);
752
+ const skipped = result.written.filter((file) => file.status === "skipped");
753
+ if (skipped.length > 0) {
754
+ process.stderr.write(
755
+ `${yellow("!")} ${skipped.length} file(s) already existed and were kept. Re-run with --force to replace them.\n`,
756
+ );
757
+ }
758
+ if (values["dry-run"] === true) {
759
+ process.stdout.write(dim("\n--dry-run: nothing was written.\n"));
760
+ return 0;
761
+ }
762
+
763
+ if (result.build !== undefined) {
764
+ process.stdout.write(
765
+ `\n${green("+")} ${bold(`${result.build.name}@${result.build.version}`)} ${result.build.chunks} chunks ${dim(`~${result.build.tokens} tokens`)}\n`,
766
+ );
767
+ }
768
+ if (result.doctor !== undefined) reportDoctor(result.doctor, quiet);
769
+
770
+ const source =
771
+ result.plan.input.kind === "seeded"
772
+ ? `${result.plan.dir}/docs/`
773
+ : result.plan.input.kind === "mirror"
774
+ ? result.plan.input.value
775
+ : posix.normalize(posix.join(result.plan.dir, result.plan.input.value));
776
+
777
+ process.stdout.write(
778
+ [
779
+ "",
780
+ "Next:",
781
+ ` $EDITOR ${source}${" ".repeat(Math.max(1, 24 - source.length))}write the documentation`,
782
+ ` cd ${result.plan.dir} && npx docspack build regenerate the payload`,
783
+ ' npx docspack preview "…" see what an agent would receive',
784
+ " npm publish when doctor is happy",
785
+ "",
786
+ ].join("\n"),
787
+ );
788
+ return 0;
789
+ }
790
+
791
+ case "doctor": {
792
+ const dir = values["package-dir"] ?? cwd;
793
+ const report = await runDoctor({
794
+ dir,
795
+ ...(values.strict === true ? { strict: true } : {}),
796
+ });
797
+
798
+ if (json) {
799
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
800
+ return report.ok ? 0 : 1;
801
+ }
802
+ reportDoctor(report, quiet);
803
+ return report.ok ? 0 : 1;
804
+ }
805
+
806
+ case "preview": {
807
+ const query = rest.join(" ");
808
+ if (query.length === 0) throw new DocspackError("Usage: docspack preview <query>");
809
+
810
+ const result = await previewPackage({
811
+ dir: values["package-dir"] ?? cwd,
812
+ query,
813
+ ...(() => {
814
+ const limit = integer(values.limit, "--limit");
815
+ return limit === undefined ? {} : { limit };
816
+ })(),
817
+ ...(() => {
818
+ const maxTokens = integer(values["max-tokens"], "--max-tokens");
819
+ return maxTokens === undefined ? {} : { maxTokens };
820
+ })(),
821
+ });
822
+
823
+ if (json) {
824
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
825
+ return 0;
826
+ }
827
+ if (result.hits.length === 0) {
828
+ process.stdout.write(
829
+ `Nothing in this package matched "${query}" (${result.indexed} chunks indexed).\n`,
830
+ );
831
+ return 1;
832
+ }
833
+ for (const hit of result.hits) {
834
+ process.stdout.write(
835
+ `${bold(hit.chunkId)} ${dim(`${hit.tokens} tokens · ${hit.filePath}`)}\n`,
836
+ );
837
+ for (const line of preview(hit.content, 4)) process.stdout.write(` ${line}\n`);
838
+ process.stdout.write("\n");
839
+ }
840
+ process.stdout.write(
841
+ dim(
842
+ `${result.tokens} tokens across ${plural(result.hits.length, "chunk")} — what an agent would receive\n`,
843
+ ),
844
+ );
845
+ return 0;
846
+ }
847
+
848
+ case "sources": {
849
+ const { entries: registryEntries } = await import("@docspack/registry");
850
+ if (json) {
851
+ process.stdout.write(`${JSON.stringify(registryEntries, null, 2)}\n`);
852
+ return 0;
853
+ }
854
+ for (const entry of registryEntries) {
855
+ process.stdout.write(`${bold(entry.id)} ${entry.name}\n ${dim(entry.description)}\n`);
856
+ }
857
+ return 0;
858
+ }
859
+
860
+ default:
861
+ process.stderr.write(`${red("error")} unknown command "${command}"\n\n`);
862
+ process.stderr.write(HELP);
863
+ return 2;
864
+ }
865
+ }
866
+
867
+ silenceSqliteWarning();
868
+
869
+ main(process.argv.slice(2))
870
+ .then((code) => {
871
+ process.exitCode = code;
872
+ })
873
+ .catch((error: unknown) => {
874
+ if (error instanceof DocspackError) {
875
+ process.stderr.write(`${red("error")} ${error.message}\n`);
876
+ if (error.hint !== undefined) process.stderr.write(`${error.hint}\n`);
877
+ } else {
878
+ process.stderr.write(
879
+ `${red("error")} ${error instanceof Error ? error.stack : String(error)}\n`,
880
+ );
881
+ }
882
+ process.exitCode = 1;
883
+ });