@tangle-network/agent-docs 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.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # agent-docs
2
+
3
+ A deterministic **repo surface + dependency map** for any TypeScript package, with an optional **DeepWiki** narrative augment for public repos.
4
+
5
+ It answers "what does this package actually export, and how do its parts depend on each other?" — from source, so it can't drift — and gates that answer in CI. For public repos it can also pull DeepWiki's AI-generated architecture wiki alongside, clearly separated so the non-deterministic prose never contaminates the part CI trusts.
6
+
7
+ ## Why
8
+
9
+ Hand-maintained "modules" tables and architecture docs lie the moment code moves. The mature fix (Microsoft's api-extractor "api-report", docs-as-tests) is to generate the surface from source and fail CI when the committed copy is stale. agent-docs is that pattern, shaped for **multi-entry packages** (a `exports` menu with many subpaths) and **agent readability** — it emits the real dependency graph between subpaths, not just a symbol list, and a machine-readable JSON alongside the markdown.
10
+
11
+ ## Two layers
12
+
13
+ | Layer | Source | Deterministic? | Gated by `--check`? |
14
+ |---|---|---|---|
15
+ | **Code map + API** (`CODEMAP.md`, `api/*.md`, `codemap.json`) | The TypeScript compiler walking real exports + the import graph | Yes | **Yes** |
16
+ | **Agent-readable surface** (`llms.txt`, `llms-full.txt`) | Same extraction, emitted in the [llms.txt](https://llmstxt.org) format | Yes | **Yes** |
17
+ | **Architecture wiki** (`WIKI.md`) | DeepWiki's MCP (`read_wiki_structure` / `ask_question`) | No (LLM prose) | **Never** |
18
+
19
+ The `llms.txt` files are the differentiator: an agent-readable index (consumable by GitMCP, Context7, Cursor, Claude) that is **type-derived**, so it can't rot — `--check` fails when it diverges from the real exports. Every `llms.txt` generator in the wild is hand-written or crawl-inferred and drifts silently; this one is gated. (It is not a search/SEO artifact — Google doesn't consume llms.txt — its value is coding agents pulling it.)
20
+
21
+ The split is deliberate: an LLM must never write the artifact CI depends on. The deterministic files are the contract; `WIKI.md` is a reading aid.
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # regenerate the deterministic map (docs/CODEMAP.md + docs/api/*.md + docs/codemap.json)
27
+ npx @tangle-network/agent-docs
28
+
29
+ # CI gate — exit 1 if the committed map is stale
30
+ npx @tangle-network/agent-docs --check
31
+
32
+ # also write docs/WIKI.md from DeepWiki (public GitHub repos only)
33
+ npx @tangle-network/agent-docs --deepwiki
34
+
35
+ # options
36
+ npx @tangle-network/agent-docs --repo path/to/repo --out docs
37
+ ```
38
+
39
+ Wire the gate into CI (it needs only the `typescript` the repo already has):
40
+
41
+ ```yaml
42
+ - run: npx @tangle-network/agent-docs --check
43
+ ```
44
+
45
+ ## Entry-point detection
46
+
47
+ Auto-detected, in priority order — no config needed for the common cases:
48
+
49
+ 1. `agent-docs.config.{mjs,json}` — explicit `entries` override
50
+ 2. `tsup.config.ts` `entry` map
51
+ 3. `package.json` `exports` (reverse-mapped from `dist` → `src`)
52
+ 4. `src/index.ts`
53
+
54
+ ```js
55
+ // agent-docs.config.mjs (only if auto-detection misses)
56
+ export default {
57
+ entries: ['src/index.ts', 'src/server/index.ts'],
58
+ out: 'docs',
59
+ deepwiki: true, // augment when the repo is public
60
+ }
61
+ ```
62
+
63
+ ## Public vs private repos
64
+
65
+ - **Private repos:** the deterministic core runs fully offline — no network, no LLM, no external service. This is the whole tool for a private repo.
66
+ - **Public repos:** add `--deepwiki` to pull the free, no-auth DeepWiki wiki + Q&A for the repo. If the repo isn't indexed yet, the augment is skipped and the deterministic map is unaffected. (Index a public repo once at `deepwiki.com/<owner>/<repo>`.)
67
+
68
+ ## Requirements
69
+
70
+ Node ≥ 20, and `typescript` resolvable from the target repo (it's a peer dependency — every TS repo has it). agent-docs carries no runtime dependencies of its own.
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,571 @@
1
+ // src/index.ts
2
+ import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
3
+ import { basename, dirname as dirname3, join as join5, resolve as resolve2 } from "path";
4
+
5
+ // src/config.ts
6
+ import { execFileSync } from "child_process";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { pathToFileURL } from "url";
10
+ async function loadConfig(repoRoot) {
11
+ for (const name of ["agent-docs.config.mjs", "agent-docs.config.js"]) {
12
+ const p = join(repoRoot, name);
13
+ if (existsSync(p)) {
14
+ const mod = await import(pathToFileURL(p).href);
15
+ return mod.default ?? mod.config ?? {};
16
+ }
17
+ }
18
+ const jsonPath = join(repoRoot, "agent-docs.config.json");
19
+ if (existsSync(jsonPath)) return JSON.parse(readFileSync(jsonPath, "utf8"));
20
+ return {};
21
+ }
22
+ function parseRemoteUrl(url) {
23
+ if (!url) return null;
24
+ const ssh = url.match(/git@([^:]+):([^/]+)\/(.+?)(?:\.git)?$/);
25
+ const https = url.match(/https?:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+?)(?:\.git)?$/);
26
+ const parsed = ssh ?? https;
27
+ if (!parsed) return null;
28
+ const [, host, owner, repo] = parsed;
29
+ return { host, owner, repo, slug: `${owner}/${repo}` };
30
+ }
31
+ function detectRepoSlug(repoRoot) {
32
+ let url;
33
+ try {
34
+ url = execFileSync("git", ["-C", repoRoot, "remote", "get-url", "origin"], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"]
37
+ }).trim();
38
+ } catch {
39
+ const cfg = join(repoRoot, ".git", "config");
40
+ if (!existsSync(cfg)) return null;
41
+ const m = readFileSync(cfg, "utf8").match(/\[remote "origin"\][^[]*?url\s*=\s*(.+)/);
42
+ if (!m) return null;
43
+ url = m[1].trim();
44
+ }
45
+ return parseRemoteUrl(url);
46
+ }
47
+
48
+ // src/deepwiki.ts
49
+ var ENDPOINT = "https://mcp.deepwiki.com/mcp";
50
+ var PROTOCOL = "2025-06-18";
51
+ function parseBody(contentType, text) {
52
+ if (contentType.includes("text/event-stream")) {
53
+ const out = [];
54
+ for (const line of text.split(/\r?\n/)) {
55
+ const m = line.match(/^data:\s*(.*)$/);
56
+ if (!m) continue;
57
+ try {
58
+ out.push(JSON.parse(m[1]));
59
+ } catch {
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+ try {
65
+ return [JSON.parse(text)];
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+ async function post(body, sessionId, signal) {
71
+ const headers = {
72
+ "content-type": "application/json",
73
+ accept: "application/json, text/event-stream"
74
+ };
75
+ if (sessionId) headers["mcp-session-id"] = sessionId;
76
+ const res = await fetch(ENDPOINT, { method: "POST", headers, body: JSON.stringify(body), signal });
77
+ const text = await res.text();
78
+ return { res, messages: parseBody(res.headers.get("content-type") ?? "", text) };
79
+ }
80
+ function resultOf(messages, id) {
81
+ const msg = messages.find((m) => m && m.id === id);
82
+ if (!msg || msg.error) return null;
83
+ return msg.result ?? null;
84
+ }
85
+ function toolText(result) {
86
+ if (!result) return "";
87
+ const content = Array.isArray(result.content) ? result.content : [];
88
+ return content.map((c) => c && typeof c.text === "string" ? c.text : "").filter(Boolean).join("\n").trim();
89
+ }
90
+ async function fetchDeepwiki(slug, { ask = [], timeoutMs = 25e3 } = {}) {
91
+ const ac = new AbortController();
92
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
93
+ try {
94
+ const init = await post(
95
+ {
96
+ jsonrpc: "2.0",
97
+ id: 1,
98
+ method: "initialize",
99
+ params: {
100
+ protocolVersion: PROTOCOL,
101
+ capabilities: {},
102
+ clientInfo: { name: "agent-docs", version: "0.1.0" }
103
+ }
104
+ },
105
+ void 0,
106
+ ac.signal
107
+ );
108
+ const sessionId = init.res.headers.get("mcp-session-id") ?? void 0;
109
+ if (!resultOf(init.messages, 1)) return null;
110
+ await post({ jsonrpc: "2.0", method: "notifications/initialized" }, sessionId, ac.signal).catch(() => {
111
+ });
112
+ const call = async (id2, name, args) => {
113
+ const { messages } = await post(
114
+ { jsonrpc: "2.0", id: id2, method: "tools/call", params: { name, arguments: args } },
115
+ sessionId,
116
+ ac.signal
117
+ );
118
+ return toolText(resultOf(messages, id2));
119
+ };
120
+ const structure = await call(2, "read_wiki_structure", { repoName: slug });
121
+ if (!structure || /not\s+found|to index it/i.test(structure)) return null;
122
+ const answers = [];
123
+ let id = 3;
124
+ for (const q of ask) {
125
+ const a = await call(id++, "ask_question", { repoName: slug, question: q });
126
+ if (a) answers.push({ q, a });
127
+ }
128
+ return { structure, answers };
129
+ } catch {
130
+ return null;
131
+ } finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+
136
+ // src/entries.ts
137
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
138
+ import { dirname, join as join2, relative } from "path";
139
+ function resolveEntries(repoRoot, config, ts) {
140
+ const srcRoot = join2(repoRoot, config.srcDir ?? "src");
141
+ const rels = (config.entries && config.entries.length ? config.entries : null) ?? fromTsup(repoRoot, ts) ?? fromPackageExports(repoRoot) ?? fromDefault(repoRoot);
142
+ if (!rels || rels.length === 0) {
143
+ throw new Error(
144
+ 'agent-docs: no entry points detected. Add a agent-docs.config.json with { "entries": ["src/index.ts", ...] }.'
145
+ );
146
+ }
147
+ const seen = /* @__PURE__ */ new Set();
148
+ const entries = [];
149
+ for (const rel of rels) {
150
+ const abs = join2(repoRoot, rel);
151
+ if (seen.has(abs)) continue;
152
+ seen.add(abs);
153
+ if (!existsSync2(abs)) continue;
154
+ entries.push(toEntry(repoRoot, abs, srcRoot, rel));
155
+ }
156
+ return entries.sort((a, b) => a.exportPath.localeCompare(b.exportPath));
157
+ }
158
+ function toEntry(repoRoot, abs, srcRoot, rel) {
159
+ const underSrc = abs === srcRoot || abs.startsWith(srcRoot + "/");
160
+ const key = (underSrc ? relative(srcRoot, abs) : relative(repoRoot, abs)).replace(/\.[tj]sx?$/, "");
161
+ const id = key === "index" ? "." : key.replace(/\/index$/, "");
162
+ const exportPath = id === "." ? "." : `./${id}`;
163
+ const apiName = (id === "." ? "index" : id).replace(/\//g, "-");
164
+ const topDir = key.split("/")[0];
165
+ return { id, exportPath, file: abs, apiName, topDir, srcDir: dirname(abs), rel: rel.replace(/\\/g, "/") };
166
+ }
167
+ function fromTsup(repoRoot, ts) {
168
+ if (!ts) return null;
169
+ const configPath = ["tsup.config.ts", "tsup.config.js", "tsup.config.mjs"].map((n) => join2(repoRoot, n)).find((p) => existsSync2(p));
170
+ if (!configPath) return null;
171
+ const sf = ts.createSourceFile(configPath, readFileSync2(configPath, "utf8"), ts.ScriptTarget.Latest, true);
172
+ let init;
173
+ const visit = (node) => {
174
+ if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) && node.name.text === "entry" && !init) {
175
+ init = node.initializer;
176
+ }
177
+ ts.forEachChild(node, visit);
178
+ };
179
+ visit(sf);
180
+ if (!init) return null;
181
+ const files = [];
182
+ if (ts.isObjectLiteralExpression(init)) {
183
+ for (const prop of init.properties) {
184
+ if (ts.isPropertyAssignment(prop) && ts.isStringLiteral(prop.initializer)) files.push(prop.initializer.text);
185
+ }
186
+ } else if (ts.isArrayLiteralExpression(init)) {
187
+ for (const el of init.elements) if (ts.isStringLiteral(el)) files.push(el.text);
188
+ }
189
+ return files.length ? files : null;
190
+ }
191
+ function fromPackageExports(repoRoot) {
192
+ const pkgPath = join2(repoRoot, "package.json");
193
+ if (!existsSync2(pkgPath)) return null;
194
+ let pkg;
195
+ try {
196
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
197
+ } catch {
198
+ return null;
199
+ }
200
+ const exp = pkg.exports;
201
+ if (!exp || typeof exp !== "object") return null;
202
+ const files = [];
203
+ for (const [sub, val] of Object.entries(exp)) {
204
+ if (sub.includes("*") || sub.endsWith("package.json") || /\.(css|json)$/.test(sub)) continue;
205
+ const target = typeof val === "string" ? val : val && typeof val === "object" ? val.types ?? val.import ?? val.default ?? val.require : void 0;
206
+ if (typeof target !== "string") continue;
207
+ const src = distToSrc(target);
208
+ if (src && existsSync2(join2(repoRoot, src))) files.push(src);
209
+ }
210
+ return files.length ? files : null;
211
+ }
212
+ function distToSrc(target) {
213
+ let s = target.replace(/^\.\//, "");
214
+ s = s.replace(/^dist\//, "src/");
215
+ s = s.replace(/\.d\.ts$/, ".ts").replace(/\.d\.mts$/, ".ts").replace(/\.m?js$/, ".ts");
216
+ return s;
217
+ }
218
+ function fromDefault(repoRoot) {
219
+ for (const rel of ["src/index.ts", "src/index.tsx", "index.ts", "src/index.mts"]) {
220
+ if (existsSync2(join2(repoRoot, rel))) return [rel];
221
+ }
222
+ return null;
223
+ }
224
+
225
+ // src/extract.ts
226
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
227
+ import { dirname as dirname2, join as join3, relative as relative2, resolve } from "path";
228
+ function createProgram(ts, repoRoot, entries, tsconfigName = "tsconfig.json") {
229
+ const tsconfigPath = join3(repoRoot, tsconfigName);
230
+ let options = { allowJs: true };
231
+ if (existsSync3(tsconfigPath)) {
232
+ const cfg = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
233
+ const parsed = ts.parseJsonConfigFileContent(cfg.config ?? {}, ts.sys, repoRoot);
234
+ options = parsed.options;
235
+ }
236
+ options = { ...options, noEmit: true, skipLibCheck: true, allowJs: true };
237
+ return ts.createProgram(
238
+ entries.map((e) => e.file),
239
+ options
240
+ );
241
+ }
242
+ var truncate = (s, n) => s.length > n ? `${s.slice(0, n - 1).trimEnd()}\u2026` : s;
243
+ function firstSentence(text) {
244
+ const flat = text.replace(/\s+/g, " ").trim();
245
+ if (!flat) return "";
246
+ const m = flat.match(/^(.*?[.!?])(\s|$)/);
247
+ return truncate(m ? m[1] : flat, 200);
248
+ }
249
+ function describe(ts, checker, exported) {
250
+ let sym = exported;
251
+ if (sym.flags & ts.SymbolFlags.Alias) {
252
+ try {
253
+ sym = checker.getAliasedSymbol(sym);
254
+ } catch {
255
+ sym = exported;
256
+ }
257
+ }
258
+ const name = exported.getName();
259
+ const decl = sym.valueDeclaration ?? sym.declarations?.[0];
260
+ const f = sym.flags;
261
+ let kind;
262
+ let signature;
263
+ if (f & ts.SymbolFlags.Class) {
264
+ kind = "class";
265
+ signature = `class ${name}`;
266
+ } else if (f & ts.SymbolFlags.Interface) {
267
+ kind = "interface";
268
+ signature = `interface ${name}`;
269
+ } else if (f & ts.SymbolFlags.TypeAlias) {
270
+ kind = "type";
271
+ signature = `type ${name}`;
272
+ } else if (f & ts.SymbolFlags.Enum || f & ts.SymbolFlags.EnumMember) {
273
+ kind = "enum";
274
+ signature = `enum ${name}`;
275
+ } else if (f & (ts.SymbolFlags.Module | ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule)) {
276
+ kind = "namespace";
277
+ signature = `namespace ${name}`;
278
+ } else if (decl) {
279
+ const type = checker.getTypeOfSymbolAtLocation(sym, decl);
280
+ const callable = type.getCallSignatures().length > 0;
281
+ kind = f & ts.SymbolFlags.Function || callable ? "function" : "const";
282
+ signature = truncate(checker.typeToString(type).replace(/\s+/g, " "), 120);
283
+ } else {
284
+ kind = "value";
285
+ signature = name;
286
+ }
287
+ let doc = ts.displayPartsToString(exported.getDocumentationComment(checker)).trim();
288
+ if (!doc && sym !== exported) doc = ts.displayPartsToString(sym.getDocumentationComment(checker)).trim();
289
+ return { name, kind, signature, doc: firstSentence(doc) };
290
+ }
291
+ function exportsOf(ts, program, checker, entry) {
292
+ const sf = program.getSourceFile(entry.file);
293
+ if (!sf) return { error: `no source file for ${entry.rel}` };
294
+ const moduleSym = checker.getSymbolAtLocation(sf);
295
+ if (!moduleSym) return { error: `no module symbol for ${entry.rel} (empty or non-module?)` };
296
+ const exports = checker.getExportsOfModule(moduleSym).filter((s) => !s.getName().startsWith("__")).map((s) => describe(ts, checker, s)).sort((a, b) => a.name.localeCompare(b.name));
297
+ return { exports };
298
+ }
299
+ var walk = (dir) => existsSync3(dir) ? readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
300
+ const p = join3(dir, e.name);
301
+ if (e.isDirectory()) return walk(p);
302
+ return /\.(ts|tsx|mts|cts)$/.test(e.name) ? [p] : [];
303
+ }) : [];
304
+ function siblingDeps(entry, srcRoot, topDirs) {
305
+ const deps = /* @__PURE__ */ new Set();
306
+ for (const file of walk(entry.srcDir)) {
307
+ const text = readFileSync3(file, "utf8");
308
+ for (const m of text.matchAll(/(?:from|import)\s*\(?\s*['"](\.[^'"]*)['"]/g)) {
309
+ const abs = resolve(dirname2(file), m[1]);
310
+ if (!abs.startsWith(srcRoot + "/")) continue;
311
+ const seg = relative2(srcRoot, abs).split("/")[0];
312
+ if (seg && seg !== entry.topDir && topDirs.has(seg)) deps.add(seg);
313
+ }
314
+ }
315
+ return [...deps].sort();
316
+ }
317
+ function extractRows(ts, repoRoot, entries, config = {}) {
318
+ const srcRoot = join3(repoRoot, config.srcDir ?? "src");
319
+ const topDirs = new Set(entries.map((e) => e.topDir));
320
+ const program = createProgram(ts, repoRoot, entries, config.tsconfig);
321
+ const checker = program.getTypeChecker();
322
+ return entries.map((entry) => {
323
+ const { exports = [], error } = exportsOf(ts, program, checker, entry);
324
+ return { entry, exports, error, deps: siblingDeps(entry, srcRoot, topDirs) };
325
+ });
326
+ }
327
+
328
+ // src/render.ts
329
+ var HEADER = "<!-- GENERATED by agent-docs \u2014 DO NOT EDIT BY HAND. Regenerate with `agent-docs`. -->";
330
+ function buildFiles(rows, meta) {
331
+ const out = meta.out ?? "docs";
332
+ const files = /* @__PURE__ */ new Map();
333
+ files.set(`${out}/CODEMAP.md`, renderCodemap(rows, meta));
334
+ files.set(`${out}/codemap.json`, renderJson(rows, meta));
335
+ files.set(`${out}/llms.txt`, renderLlmsTxt(rows, meta));
336
+ files.set(`${out}/llms-full.txt`, renderLlmsFull(rows, meta));
337
+ for (const row of rows) files.set(`${out}/api/${row.entry.apiName}.md`, renderApiPage(row));
338
+ return files;
339
+ }
340
+ function purpose(meta) {
341
+ return meta.description ?? `Deterministic export surface for ${meta.title}.`;
342
+ }
343
+ function renderLlmsTxt(rows, meta) {
344
+ const lines = [`# ${meta.title}`, "", `> ${purpose(meta)}`, ""];
345
+ lines.push(
346
+ `_Generated by agent-docs from ${meta.sourceLabel}; ${rows.length} entr${rows.length === 1 ? "y" : "ies"}. Regenerate with \`agent-docs\`; full inlined surface in [llms-full.txt](llms-full.txt)._`,
347
+ "",
348
+ "## Modules",
349
+ ""
350
+ );
351
+ for (const { entry, exports, error } of rows) {
352
+ const count = error ? "introspection failed" : `${exports.length} export${exports.length === 1 ? "" : "s"}`;
353
+ const names = error || !exports.length ? "" : ` \u2014 ${exports.slice(0, 8).map((e) => e.name).join(", ")}${exports.length > 8 ? ", \u2026" : ""}`;
354
+ lines.push(`- [\`${entry.exportPath}\`](api/${entry.apiName}.md): ${count}${names}`);
355
+ }
356
+ return `${lines.join("\n").trimEnd()}
357
+ `;
358
+ }
359
+ function renderLlmsFull(rows, meta) {
360
+ const lines = [`# ${meta.title} \u2014 full API surface`, "", `> ${purpose(meta)}`, ""];
361
+ lines.push(
362
+ `_Generated by agent-docs from ${meta.sourceLabel}; ${rows.length} entries, every export with its signature. Regenerate with \`agent-docs\`._`
363
+ );
364
+ for (const { entry, exports, error, deps } of rows) {
365
+ lines.push("", `## \`${entry.exportPath}\``, "");
366
+ lines.push(`Source: \`${entry.rel}\`${deps.length ? ` \xB7 depends on ${deps.map((d) => `\`${d}\``).join(", ")}` : ""}`, "");
367
+ if (error) {
368
+ lines.push(`> Could not introspect: ${error}`);
369
+ continue;
370
+ }
371
+ if (!exports.length) {
372
+ lines.push("_No public exports._");
373
+ continue;
374
+ }
375
+ for (const e of exports) {
376
+ lines.push(`### \`${e.name}\``, "", `\`${e.kind}\`${e.doc ? ` \u2014 ${e.doc}` : ""}`, "", "```ts", e.signature, "```", "");
377
+ }
378
+ }
379
+ return `${lines.join("\n").trimEnd()}
380
+ `;
381
+ }
382
+ function renderWiki(meta) {
383
+ if (!meta.deepwiki) return null;
384
+ const out = meta.out ?? "docs";
385
+ const lines = [
386
+ HEADER.replace(
387
+ "DO NOT EDIT BY HAND. Regenerate with `agent-docs`.",
388
+ "AI-GENERATED via DeepWiki \u2014 non-authoritative, regenerated with `agent-docs --deepwiki`."
389
+ ),
390
+ "",
391
+ `# ${meta.title} \u2014 architecture (DeepWiki)`,
392
+ "",
393
+ `> AI-generated by [DeepWiki](https://deepwiki.com/${meta.slug}); a reading aid, **not** a contract. The source of truth is [\`CODEMAP.md\`](./CODEMAP.md), extracted from source.`,
394
+ ""
395
+ ];
396
+ if (meta.deepwiki.structure) lines.push("## Wiki pages", "", "```", meta.deepwiki.structure.trim(), "```", "");
397
+ for (const { q, a } of meta.deepwiki.answers ?? []) lines.push(`## ${q}`, "", a.trim(), "");
398
+ return { path: `${out}/WIKI.md`, content: `${lines.join("\n").trimEnd()}
399
+ ` };
400
+ }
401
+ function renderCodemap(rows, meta) {
402
+ const title = meta.title ?? "code map";
403
+ const lines = [HEADER, "", `# ${title} code map`, ""];
404
+ lines.push(`_${rows.length} entr${rows.length === 1 ? "y" : "ies"} \u2014 ${meta.sourceLabel}. Regenerate with \`agent-docs\`._`, "");
405
+ lines.push("| Entry | Exports | Depends on |", "|---|---|---|");
406
+ for (const { entry, exports, error, deps } of rows) {
407
+ const link = `[\`${entry.exportPath}\`](api/${entry.apiName}.md)`;
408
+ const count = error ? "\u2014" : String(exports.length);
409
+ const dep = deps.length ? deps.map((d) => `\`${d}\``).join(", ") : "\u2014";
410
+ lines.push(`| ${link} | ${count} | ${dep} |`);
411
+ }
412
+ lines.push("", "---", "");
413
+ for (const { entry, exports, error, deps } of rows) {
414
+ lines.push(`## \`${entry.exportPath}\``, "");
415
+ lines.push(`Source: \`${entry.rel}\` \xB7 ${error ? "introspection failed" : `${exports.length} exports`}`, "");
416
+ if (deps.length) lines.push(`Depends on: ${deps.map((d) => `\`${d}\``).join(", ")}`, "");
417
+ if (error) lines.push(`> Could not introspect: ${error}`, "");
418
+ else if (exports.length) lines.push(exports.map((e) => `\`${e.name}\``).join(", "), "", `[Full API \u2192](api/${entry.apiName}.md)`, "");
419
+ else lines.push("_No public exports._", "");
420
+ }
421
+ return `${lines.join("\n").trimEnd()}
422
+ `;
423
+ }
424
+ function renderApiPage({ entry, exports, error }) {
425
+ const lines = [HEADER, "", `# \`${entry.exportPath}\``, "", `Source: \`${entry.rel}\``, ""];
426
+ if (error) return `${[...lines, `> Could not introspect this entry: ${error}`].join("\n").trimEnd()}
427
+ `;
428
+ lines.push(`${exports.length} export${exports.length === 1 ? "" : "s"}.`, "");
429
+ for (const e of exports) {
430
+ lines.push(`### \`${e.name}\``, "");
431
+ lines.push(`\`${e.kind}\`${e.doc ? ` \u2014 ${e.doc}` : ""}`, "");
432
+ lines.push("```ts", e.signature, "```", "");
433
+ }
434
+ return `${lines.join("\n").trimEnd()}
435
+ `;
436
+ }
437
+ function renderJson(rows, meta) {
438
+ const payload = {
439
+ generator: "agent-docs",
440
+ title: meta.title,
441
+ source: meta.sourceLabel,
442
+ slug: meta.slug ?? null,
443
+ entries: rows.map(({ entry, exports, error, deps }) => ({
444
+ id: entry.exportPath,
445
+ source: entry.rel,
446
+ dependsOn: deps,
447
+ error: error ?? null,
448
+ exports: exports.map((e) => ({ name: e.name, kind: e.kind, signature: e.signature, doc: e.doc || null }))
449
+ }))
450
+ };
451
+ return `${JSON.stringify(payload, null, 2)}
452
+ `;
453
+ }
454
+
455
+ // src/ts.ts
456
+ import { createRequire } from "module";
457
+ import { join as join4 } from "path";
458
+ import { pathToFileURL as pathToFileURL2 } from "url";
459
+ async function loadTs(repoRoot) {
460
+ const candidates = [];
461
+ try {
462
+ candidates.push(createRequire(join4(repoRoot, "package.json")).resolve("typescript"));
463
+ } catch {
464
+ }
465
+ try {
466
+ candidates.push(createRequire(import.meta.url).resolve("typescript"));
467
+ } catch {
468
+ }
469
+ for (const path of candidates) {
470
+ try {
471
+ const mod = await import(pathToFileURL2(path).href);
472
+ return mod.default ?? mod;
473
+ } catch {
474
+ }
475
+ }
476
+ throw new Error(
477
+ "could not load 'typescript'. Run agent-docs inside a repo that has typescript installed (it's a peer dependency)."
478
+ );
479
+ }
480
+
481
+ // src/index.ts
482
+ var DEFAULT_ASK = [
483
+ "What is the high-level architecture and what are the main components?",
484
+ "How do the main components fit together in a typical request or data flow?"
485
+ ];
486
+ function readPackageDescription(root) {
487
+ const pkg = join5(root, "package.json");
488
+ if (!existsSync4(pkg)) return void 0;
489
+ try {
490
+ const d = JSON.parse(readFileSync4(pkg, "utf8")).description;
491
+ return typeof d === "string" && d ? d : void 0;
492
+ } catch {
493
+ return void 0;
494
+ }
495
+ }
496
+ function describeSource(root, config) {
497
+ if (config.entries?.length) return "entries from agent-docs.config";
498
+ if (["tsup.config.ts", "tsup.config.js", "tsup.config.mjs"].some((n) => existsSync4(join5(root, n)))) return "tsup.config `entry`";
499
+ const pkg = join5(root, "package.json");
500
+ if (existsSync4(pkg)) {
501
+ try {
502
+ if (JSON.parse(readFileSync4(pkg, "utf8")).exports) return "package.json `exports`";
503
+ } catch {
504
+ }
505
+ }
506
+ return "src/index";
507
+ }
508
+ async function analyze(repoRoot, opts = {}) {
509
+ const root = resolve2(repoRoot);
510
+ const config = await loadConfig(root);
511
+ const ts = await loadTs(root);
512
+ const entries = resolveEntries(root, config, ts);
513
+ const rows = extractRows(ts, root, entries, config);
514
+ const slug = detectRepoSlug(root);
515
+ const meta = {
516
+ out: opts.out ?? config.out ?? "docs",
517
+ title: config.title ?? slug?.repo ?? basename(root),
518
+ description: readPackageDescription(root),
519
+ sourceLabel: describeSource(root, config),
520
+ slug: slug?.slug
521
+ };
522
+ const wantWiki = opts.deepwiki ?? config.deepwiki ?? false;
523
+ let deepwikiTried = false;
524
+ if (wantWiki && slug && /(^|\.)github\.com$/i.test(slug.host)) {
525
+ deepwikiTried = true;
526
+ const dw = await fetchDeepwiki(slug.slug, { ask: opts.ask ?? config.ask ?? DEFAULT_ASK });
527
+ if (dw) meta.deepwiki = dw;
528
+ }
529
+ return { root, rows, meta, slug, wantWiki, deepwikiTried, files: buildFiles(rows, meta), wiki: renderWiki(meta) };
530
+ }
531
+ async function write(repoRoot, opts = {}) {
532
+ const r = await analyze(repoRoot, opts);
533
+ const apiDir = join5(r.root, r.meta.out, "api");
534
+ if (existsSync4(apiDir)) rmSync(apiDir, { recursive: true, force: true });
535
+ const written = [];
536
+ for (const [rel, content] of r.files) {
537
+ const abs = join5(r.root, rel);
538
+ mkdirSync(dirname3(abs), { recursive: true });
539
+ writeFileSync(abs, content);
540
+ written.push(rel);
541
+ }
542
+ if (r.wiki) {
543
+ const abs = join5(r.root, r.wiki.path);
544
+ mkdirSync(dirname3(abs), { recursive: true });
545
+ writeFileSync(abs, r.wiki.content);
546
+ written.push(r.wiki.path);
547
+ }
548
+ return { ...r, written };
549
+ }
550
+ async function check(repoRoot, opts = {}) {
551
+ const r = await analyze(repoRoot, { ...opts, deepwiki: false });
552
+ const stale = [];
553
+ const missing = [];
554
+ for (const [rel, content] of r.files) {
555
+ const abs = join5(r.root, rel);
556
+ if (!existsSync4(abs)) missing.push(rel);
557
+ else if (readFileSync4(abs, "utf8") !== content) stale.push(rel);
558
+ }
559
+ const apiDir = join5(r.root, r.meta.out, "api");
560
+ const orphan = existsSync4(apiDir) ? readdirSync2(apiDir).filter((f) => f.endsWith(".md")).map((f) => `${r.meta.out}/api/${f}`).filter((rel) => !r.files.has(rel)) : [];
561
+ return { ok: stale.length + missing.length + orphan.length === 0, stale, missing, orphan, meta: r.meta };
562
+ }
563
+
564
+ export {
565
+ parseRemoteUrl,
566
+ detectRepoSlug,
567
+ fetchDeepwiki,
568
+ analyze,
569
+ write,
570
+ check
571
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ check,
4
+ write
5
+ } from "./chunk-EKQKRTAN.js";
6
+
7
+ // src/cli.ts
8
+ var HELP = `agent-docs \u2014 deterministic repo surface + dependency map for any TypeScript package.
9
+
10
+ Usage:
11
+ agent-docs [gen] Regenerate docs/CODEMAP.md + docs/api/*.md + docs/codemap.json
12
+ agent-docs --check Exit 1 if the committed deterministic docs are stale (CI gate)
13
+ agent-docs --deepwiki Also write docs/WIKI.md from DeepWiki (public GitHub repos only)
14
+
15
+ Options:
16
+ --repo <path> Repo root (default: cwd)
17
+ --out <dir> Output dir (default: docs, or agent-docs.config \`out\`)
18
+ -h, --help
19
+
20
+ Entry points are auto-detected: agent-docs.config > tsup.config \`entry\` > package.json \`exports\` > src/index.
21
+ The deterministic files are what --check gates on; the DeepWiki WIKI.md is a non-authoritative reading aid and is never gated.`;
22
+ function parseArgs(argv) {
23
+ const args2 = { repo: process.cwd(), check: false, deepwiki: false, help: false };
24
+ const rest = argv.slice(2);
25
+ for (let i = 0; i < rest.length; i++) {
26
+ const a = rest[i];
27
+ if (a === "--check" || a === "check") args2.check = true;
28
+ else if (a === "--deepwiki") args2.deepwiki = true;
29
+ else if (a === "--repo") args2.repo = rest[++i];
30
+ else if (a === "--out") args2.out = rest[++i];
31
+ else if (a === "-h" || a === "--help") args2.help = true;
32
+ else if (a === "gen") {
33
+ } else if (a.startsWith("-")) {
34
+ console.error(`agent-docs: unknown option ${a}`);
35
+ process.exit(2);
36
+ }
37
+ }
38
+ return args2;
39
+ }
40
+ var args = parseArgs(process.argv);
41
+ if (args.help) {
42
+ console.log(HELP);
43
+ process.exit(0);
44
+ }
45
+ try {
46
+ if (args.check) {
47
+ const r2 = await check(args.repo, { out: args.out });
48
+ if (r2.ok) {
49
+ console.log("agent-docs: docs are fresh");
50
+ process.exit(0);
51
+ }
52
+ console.error("agent-docs: docs are STALE \u2014 run `agent-docs` and commit the result.");
53
+ for (const f of r2.missing) console.error(` missing: ${f}`);
54
+ for (const f of r2.stale) console.error(` changed: ${f}`);
55
+ for (const f of r2.orphan) console.error(` orphaned: ${f}`);
56
+ process.exit(1);
57
+ }
58
+ const r = await write(args.repo, { out: args.out, deepwiki: args.deepwiki });
59
+ const det = r.written.filter((f) => !f.endsWith("WIKI.md")).length;
60
+ console.log(`agent-docs: wrote ${det} files under ${r.meta.out}/ (${r.rows.length} entries)`);
61
+ if (args.deepwiki) {
62
+ if (r.wiki) console.log(`agent-docs: + ${r.wiki.path} (DeepWiki: ${r.slug?.slug})`);
63
+ else if (r.deepwikiTried)
64
+ console.log(`agent-docs: DeepWiki skipped \u2014 ${r.slug?.slug ?? "repo"} not indexed or unreachable (deterministic map unaffected)`);
65
+ else console.log("agent-docs: DeepWiki needs a public github.com remote; skipped");
66
+ }
67
+ } catch (err) {
68
+ console.error(`agent-docs: ${err instanceof Error ? err.message : String(err)}`);
69
+ process.exit(2);
70
+ }
@@ -0,0 +1,123 @@
1
+ import * as typescript from 'typescript';
2
+
3
+ /** The TypeScript compiler module, loaded from the target repo at runtime. */
4
+ type TS = typeof typescript;
5
+ interface Entry {
6
+ /** Public subpath id: `.` for the root, else e.g. `chat-routes`. */
7
+ id: string;
8
+ /** Export path as written in `exports`: `.` or `./chat-routes`. */
9
+ exportPath: string;
10
+ /** Absolute path to the entry source file. */
11
+ file: string;
12
+ /** Flattened name for the API page file: `index` or `chat-routes`. */
13
+ apiName: string;
14
+ /** First path segment under the source root — the sibling-graph key. */
15
+ topDir: string;
16
+ /** Directory containing the entry (scanned for the import graph). */
17
+ srcDir: string;
18
+ /** Repo-relative source path, for display. */
19
+ rel: string;
20
+ }
21
+ type ExportKind = 'class' | 'interface' | 'type' | 'enum' | 'namespace' | 'function' | 'const' | 'value';
22
+ interface ExportInfo {
23
+ name: string;
24
+ kind: ExportKind;
25
+ signature: string;
26
+ doc: string;
27
+ }
28
+ interface Row {
29
+ entry: Entry;
30
+ exports: ExportInfo[];
31
+ error?: string;
32
+ deps: string[];
33
+ }
34
+ interface CartographConfig {
35
+ entries?: string[];
36
+ srcDir?: string;
37
+ tsconfig?: string;
38
+ out?: string;
39
+ title?: string;
40
+ deepwiki?: boolean;
41
+ ask?: string[];
42
+ }
43
+ interface RepoSlug {
44
+ host: string;
45
+ owner: string;
46
+ repo: string;
47
+ slug: string;
48
+ }
49
+ interface DeepwikiResult {
50
+ structure: string;
51
+ answers: Array<{
52
+ q: string;
53
+ a: string;
54
+ }>;
55
+ }
56
+ interface Meta {
57
+ out: string;
58
+ title: string;
59
+ /** One-line purpose (from package.json `description`), for the llms.txt blockquote. */
60
+ description?: string;
61
+ sourceLabel: string;
62
+ slug?: string;
63
+ deepwiki?: DeepwikiResult;
64
+ }
65
+ interface AnalyzeOptions {
66
+ out?: string;
67
+ deepwiki?: boolean;
68
+ ask?: string[];
69
+ }
70
+ interface AnalyzeResult {
71
+ root: string;
72
+ rows: Row[];
73
+ meta: Meta;
74
+ slug: RepoSlug | null;
75
+ wantWiki: boolean;
76
+ deepwikiTried: boolean;
77
+ files: Map<string, string>;
78
+ wiki: {
79
+ path: string;
80
+ content: string;
81
+ } | null;
82
+ }
83
+ interface CheckResult {
84
+ ok: boolean;
85
+ stale: string[];
86
+ missing: string[];
87
+ orphan: string[];
88
+ meta: Meta;
89
+ }
90
+
91
+ /** Pure parse of a git remote URL → `{ host, owner, repo, slug }` or null. */
92
+ declare function parseRemoteUrl(url: string | null | undefined): RepoSlug | null;
93
+ /**
94
+ * `{ host, owner, repo, slug }` from the git `origin` remote, or null. Uses
95
+ * `git` (worktree-safe) and falls back to parsing `.git/config`. Only GitHub
96
+ * remotes carry a DeepWiki-usable slug; the caller decides what to do with it.
97
+ */
98
+ declare function detectRepoSlug(repoRoot: string): RepoSlug | null;
99
+
100
+ /**
101
+ * Fetch DeepWiki context for `slug` (`owner/repo`). Returns
102
+ * `{ structure, answers: [{q, a}] }` or null. `ask` is a list of questions to
103
+ * put to `ask_question`.
104
+ */
105
+ declare function fetchDeepwiki(slug: string, { ask, timeoutMs }?: {
106
+ ask?: string[];
107
+ timeoutMs?: number;
108
+ }): Promise<DeepwikiResult | null>;
109
+
110
+ /** Extract the surface + graph (+ optional DeepWiki) and build the output files. */
111
+ declare function analyze(repoRoot: string, opts?: AnalyzeOptions): Promise<AnalyzeResult>;
112
+ /** Regenerate all files to disk. Cleans stale `api/*.md` first. */
113
+ declare function write(repoRoot: string, opts?: AnalyzeOptions): Promise<AnalyzeResult & {
114
+ written: string[];
115
+ }>;
116
+ /**
117
+ * Compare the freshly-extracted DETERMINISTIC files against what's committed.
118
+ * Forces DeepWiki off — the gate only ever inspects the deterministic surface,
119
+ * so a `WIKI.md` (non-deterministic) never trips it.
120
+ */
121
+ declare function check(repoRoot: string, opts?: AnalyzeOptions): Promise<CheckResult>;
122
+
123
+ export { type AnalyzeOptions, type AnalyzeResult, type CartographConfig, type CheckResult, type DeepwikiResult, type Entry, type ExportInfo, type ExportKind, type Meta, type RepoSlug, type Row, type TS, analyze, check, detectRepoSlug, fetchDeepwiki, parseRemoteUrl, write };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ analyze,
3
+ check,
4
+ detectRepoSlug,
5
+ fetchDeepwiki,
6
+ parseRemoteUrl,
7
+ write
8
+ } from "./chunk-EKQKRTAN.js";
9
+ export {
10
+ analyze,
11
+ check,
12
+ detectRepoSlug,
13
+ fetchDeepwiki,
14
+ parseRemoteUrl,
15
+ write
16
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@tangle-network/agent-docs",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic repo surface + dependency map for any TypeScript package, with an optional DeepWiki narrative augment for public repos.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "agent-docs": "dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "tsup && node --test 'tests/*.test.mjs'",
27
+ "prepublishOnly": "tsup"
28
+ },
29
+ "peerDependencies": {
30
+ "typescript": ">=5.0.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "typescript": { "optional": false }
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.6.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/tangle-network/agent-docs.git"
47
+ }
48
+ }