@justyork/repo-mind 0.3.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 (177) hide show
  1. package/README.md +110 -0
  2. package/dist/ab-demo/arm-baseline.d.ts +8 -0
  3. package/dist/ab-demo/arm-baseline.js +40 -0
  4. package/dist/ab-demo/arm-repomind.d.ts +7 -0
  5. package/dist/ab-demo/arm-repomind.js +35 -0
  6. package/dist/ab-demo/estimate-tokens.d.ts +3 -0
  7. package/dist/ab-demo/estimate-tokens.js +10 -0
  8. package/dist/ab-demo/load-questions.d.ts +2 -0
  9. package/dist/ab-demo/load-questions.js +65 -0
  10. package/dist/ab-demo/paths.d.ts +5 -0
  11. package/dist/ab-demo/paths.js +31 -0
  12. package/dist/ab-demo/run-ab.d.ts +7 -0
  13. package/dist/ab-demo/run-ab.js +128 -0
  14. package/dist/ab-demo/run-arms.d.ts +3 -0
  15. package/dist/ab-demo/run-arms.js +67 -0
  16. package/dist/ab-demo/session-overhead.d.ts +3 -0
  17. package/dist/ab-demo/session-overhead.js +68 -0
  18. package/dist/ab-demo/types.d.ts +65 -0
  19. package/dist/ab-demo/types.js +1 -0
  20. package/dist/ab-demo/validate-corpus.d.ts +3 -0
  21. package/dist/ab-demo/validate-corpus.js +38 -0
  22. package/dist/check/collect-violations.d.ts +11 -0
  23. package/dist/check/collect-violations.js +127 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +147 -0
  26. package/dist/commands/check.d.ts +6 -0
  27. package/dist/commands/check.js +19 -0
  28. package/dist/commands/export.d.ts +8 -0
  29. package/dist/commands/export.js +80 -0
  30. package/dist/commands/init.d.ts +4 -0
  31. package/dist/commands/init.js +86 -0
  32. package/dist/commands/prepare.d.ts +7 -0
  33. package/dist/commands/prepare.js +61 -0
  34. package/dist/commands/setup.d.ts +11 -0
  35. package/dist/commands/setup.js +84 -0
  36. package/dist/commands/sync-links.d.ts +7 -0
  37. package/dist/commands/sync-links.js +41 -0
  38. package/dist/commands/ui.d.ts +5 -0
  39. package/dist/commands/ui.js +83 -0
  40. package/dist/index/asset-file.d.ts +4 -0
  41. package/dist/index/asset-file.js +26 -0
  42. package/dist/index/doc-index.d.ts +21 -0
  43. package/dist/index/doc-index.js +231 -0
  44. package/dist/index/knowledge-file.d.ts +4 -0
  45. package/dist/index/knowledge-file.js +17 -0
  46. package/dist/index/link-index.d.ts +41 -0
  47. package/dist/index/link-index.js +150 -0
  48. package/dist/index/path-inference.d.ts +9 -0
  49. package/dist/index/path-inference.js +33 -0
  50. package/dist/index/resolve-asset-href.d.ts +2 -0
  51. package/dist/index/resolve-asset-href.js +65 -0
  52. package/dist/index/resolve-md-href.d.ts +7 -0
  53. package/dist/index/resolve-md-href.js +116 -0
  54. package/dist/index/slug.d.ts +5 -0
  55. package/dist/index/slug.js +43 -0
  56. package/dist/index/types.d.ts +44 -0
  57. package/dist/index/types.js +71 -0
  58. package/dist/mcp/server.d.ts +1 -0
  59. package/dist/mcp/server.js +155 -0
  60. package/dist/package-version.d.ts +2 -0
  61. package/dist/package-version.js +15 -0
  62. package/dist/prepare/auto-links.d.ts +22 -0
  63. package/dist/prepare/auto-links.js +124 -0
  64. package/dist/prepare/prepare-docs.d.ts +36 -0
  65. package/dist/prepare/prepare-docs.js +106 -0
  66. package/dist/tools/explore-graph.d.ts +25 -0
  67. package/dist/tools/explore-graph.js +84 -0
  68. package/dist/tools/get-doc.d.ts +11 -0
  69. package/dist/tools/get-doc.js +21 -0
  70. package/dist/tools/get-glossary-term.d.ts +9 -0
  71. package/dist/tools/get-glossary-term.js +41 -0
  72. package/dist/tools/list-docs.d.ts +19 -0
  73. package/dist/tools/list-docs.js +35 -0
  74. package/dist/tools/search-docs.d.ts +14 -0
  75. package/dist/tools/search-docs.js +80 -0
  76. package/dist/ui/api-handlers.d.ts +12 -0
  77. package/dist/ui/api-handlers.js +223 -0
  78. package/dist/ui/catalog-meta.d.ts +4 -0
  79. package/dist/ui/catalog-meta.js +47 -0
  80. package/dist/ui/db/drafts-db.d.ts +49 -0
  81. package/dist/ui/db/drafts-db.js +179 -0
  82. package/dist/ui/diff.d.ts +8 -0
  83. package/dist/ui/diff.js +58 -0
  84. package/dist/ui/docs-watcher.d.ts +13 -0
  85. package/dist/ui/docs-watcher.js +59 -0
  86. package/dist/ui/draft-api.d.ts +7 -0
  87. package/dist/ui/draft-api.js +413 -0
  88. package/dist/ui/fs-operations.d.ts +52 -0
  89. package/dist/ui/fs-operations.js +304 -0
  90. package/dist/ui/fs-tree.d.ts +28 -0
  91. package/dist/ui/fs-tree.js +148 -0
  92. package/dist/ui/graph-all.d.ts +5 -0
  93. package/dist/ui/graph-all.js +50 -0
  94. package/dist/ui/link-cascade.d.ts +9 -0
  95. package/dist/ui/link-cascade.js +113 -0
  96. package/dist/ui/parse-multipart.d.ts +12 -0
  97. package/dist/ui/parse-multipart.js +64 -0
  98. package/dist/ui/publish.d.ts +14 -0
  99. package/dist/ui/publish.js +83 -0
  100. package/dist/ui/safe-path.d.ts +6 -0
  101. package/dist/ui/safe-path.js +42 -0
  102. package/dist/ui/serve-asset.d.ts +4 -0
  103. package/dist/ui/serve-asset.js +49 -0
  104. package/dist/ui/server.d.ts +17 -0
  105. package/dist/ui/server.js +237 -0
  106. package/dist/ui/stats.d.ts +9 -0
  107. package/dist/ui/stats.js +23 -0
  108. package/dist/ui/templates.d.ts +12 -0
  109. package/dist/ui/templates.js +39 -0
  110. package/dist/ui/upload-asset.d.ts +11 -0
  111. package/dist/ui/upload-asset.js +61 -0
  112. package/package.json +55 -0
  113. package/templates/adr-example.md +27 -0
  114. package/templates/agent-instruction-example.md +20 -0
  115. package/templates/combat-system-example.md +27 -0
  116. package/templates/feature-spec-example.md +27 -0
  117. package/templates/glossary-term-example.md +15 -0
  118. package/templates/open-question-example.md +26 -0
  119. package/ui/dist/assets/arc-DhC0JPue.js +1 -0
  120. package/ui/dist/assets/architectureDiagram-3BPJPVTR-Cun_Ijrv.js +36 -0
  121. package/ui/dist/assets/blockDiagram-GPEHLZMM-CgiNAArN.js +132 -0
  122. package/ui/dist/assets/c4Diagram-AAUBKEIU-BIwHcwcH.js +10 -0
  123. package/ui/dist/assets/channel-CNwAp9ic.js +1 -0
  124. package/ui/dist/assets/chunk-2J33WTMH-DXRgHPpp.js +1 -0
  125. package/ui/dist/assets/chunk-4BX2VUAB-BTb70kIb.js +1 -0
  126. package/ui/dist/assets/chunk-55IACEB6-BrAelyhX.js +1 -0
  127. package/ui/dist/assets/chunk-727SXJPM-BlYnlPdj.js +206 -0
  128. package/ui/dist/assets/chunk-AQP2D5EJ-DSPgdKZ8.js +231 -0
  129. package/ui/dist/assets/chunk-FMBD7UC4-BhH8ir2K.js +15 -0
  130. package/ui/dist/assets/chunk-ND2GUHAM-DCAuTSxB.js +1 -0
  131. package/ui/dist/assets/chunk-QZHKN3VN-DtYEkbYr.js +1 -0
  132. package/ui/dist/assets/classDiagram-4FO5ZUOK-DnHeGLmR.js +1 -0
  133. package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-DnHeGLmR.js +1 -0
  134. package/ui/dist/assets/cose-bilkent-S5V4N54A-CAM4jLYo.js +1 -0
  135. package/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  136. package/ui/dist/assets/dagre-BM42HDAG-CISbgani.js +4 -0
  137. package/ui/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  138. package/ui/dist/assets/diagram-2AECGRRQ-BmXargwF.js +43 -0
  139. package/ui/dist/assets/diagram-5GNKFQAL-COlrLu0O.js +10 -0
  140. package/ui/dist/assets/diagram-KO2AKTUF-B-kUxuHX.js +3 -0
  141. package/ui/dist/assets/diagram-LMA3HP47-C3AVVxcm.js +24 -0
  142. package/ui/dist/assets/diagram-OG6HWLK6-JHeftSsO.js +24 -0
  143. package/ui/dist/assets/erDiagram-TEJ5UH35-BSWwMysi.js +85 -0
  144. package/ui/dist/assets/flowDiagram-I6XJVG4X-D-q1cK69.js +162 -0
  145. package/ui/dist/assets/ganttDiagram-6RSMTGT7-DrYn1H_t.js +292 -0
  146. package/ui/dist/assets/gitGraphDiagram-PVQCEYII-vJByl99X.js +106 -0
  147. package/ui/dist/assets/graph-CAnANduQ.js +1 -0
  148. package/ui/dist/assets/graph-DwoitsWW.js +2 -0
  149. package/ui/dist/assets/infoDiagram-5YYISTIA-D6zhGTMj.js +2 -0
  150. package/ui/dist/assets/init-Gi6I4Gst.js +1 -0
  151. package/ui/dist/assets/ishikawaDiagram-YF4QCWOH-CY-U_l7l.js +70 -0
  152. package/ui/dist/assets/journeyDiagram-JHISSGLW-jKj4lBEJ.js +139 -0
  153. package/ui/dist/assets/kanban-definition-UN3LZRKU-PZ-5AYw2.js +89 -0
  154. package/ui/dist/assets/katex-C5jXJg4s.js +257 -0
  155. package/ui/dist/assets/layout-DGIYPm2g.js +1 -0
  156. package/ui/dist/assets/linear-COY9pyF4.js +1 -0
  157. package/ui/dist/assets/main-BBzCq-49.js +308 -0
  158. package/ui/dist/assets/mermaid.core-Bddhr0ku.js +309 -0
  159. package/ui/dist/assets/mindmap-definition-RKZ34NQL-CPY2Fdu_.js +96 -0
  160. package/ui/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  161. package/ui/dist/assets/pieDiagram-4H26LBE5-C7GJ49et.js +30 -0
  162. package/ui/dist/assets/quadrantDiagram-W4KKPZXB-DQyQN5K7.js +7 -0
  163. package/ui/dist/assets/requirementDiagram-4Y6WPE33-CDrkwz1t.js +84 -0
  164. package/ui/dist/assets/sankeyDiagram-5OEKKPKP-BrYb9Eql.js +40 -0
  165. package/ui/dist/assets/sequenceDiagram-3UESZ5HK-B8If_JZp.js +162 -0
  166. package/ui/dist/assets/stateDiagram-AJRCARHV-BbpTp9VX.js +1 -0
  167. package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-BT4PvMFS.js +1 -0
  168. package/ui/dist/assets/theme-DV7vqTnV.js +1 -0
  169. package/ui/dist/assets/theme-SpsWsRN5.css +1 -0
  170. package/ui/dist/assets/timeline-definition-PNZ67QCA-DhUg6aIV.js +120 -0
  171. package/ui/dist/assets/transform-BwXaE9hv.js +1 -0
  172. package/ui/dist/assets/vennDiagram-CIIHVFJN-DpQVNNzF.js +34 -0
  173. package/ui/dist/assets/wardley-L42UT6IY-CyaxzHGP.js +173 -0
  174. package/ui/dist/assets/wardleyDiagram-YWT4CUSO-Bm0mA7wm.js +78 -0
  175. package/ui/dist/assets/xychartDiagram-2RQKCTM6-OJbmgDx6.js +7 -0
  176. package/ui/dist/graph.html +27 -0
  177. package/ui/dist/index.html +37 -0
@@ -0,0 +1,35 @@
1
+ export function listDocs(index, input = {}) {
2
+ const root = index.getKnowledgeRoot();
3
+ if (!root) {
4
+ return [];
5
+ }
6
+ let docs = index.refresh();
7
+ if (input.type) {
8
+ docs = docs.filter((doc) => doc.type === input.type);
9
+ }
10
+ if (input.status) {
11
+ docs = docs.filter((doc) => doc.status === input.status);
12
+ }
13
+ if (input.tag) {
14
+ const tagLower = input.tag.toLowerCase();
15
+ docs = docs.filter((doc) => doc.tags.some((tag) => tag.toLowerCase() === tagLower));
16
+ }
17
+ if (input.domain) {
18
+ docs = docs.filter((doc) => doc.domain === input.domain);
19
+ }
20
+ return docs.map((doc) => ({
21
+ slug: doc.slug,
22
+ type: doc.type,
23
+ domain: doc.domain,
24
+ title: doc.title,
25
+ status: doc.status,
26
+ relativePath: doc.relativePath,
27
+ contentKind: doc.contentKind,
28
+ }));
29
+ }
30
+ export function getDocRecord(index, slug) {
31
+ if (!index.getKnowledgeRoot()) {
32
+ return null;
33
+ }
34
+ return index.getDocBySlug(slug);
35
+ }
@@ -0,0 +1,14 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { DocType, DocDomain } from '../index/types.js';
3
+ export interface SearchDocsInput {
4
+ query: string;
5
+ type?: DocType;
6
+ domain?: DocDomain;
7
+ }
8
+ export interface SearchDocsResult {
9
+ slug: string;
10
+ title: string;
11
+ snippet: string;
12
+ score: number;
13
+ }
14
+ export declare function searchDocs(index: DocIndex, input: SearchDocsInput): SearchDocsResult[];
@@ -0,0 +1,80 @@
1
+ function tokenize(query) {
2
+ return query
3
+ .trim()
4
+ .split(/\s+/)
5
+ .filter(Boolean)
6
+ .map((term) => term.toLowerCase());
7
+ }
8
+ function haystack(doc) {
9
+ return `${doc.title}\n${doc.tags.join(' ')}\n${doc.body}`.toLowerCase();
10
+ }
11
+ function scoreDoc(doc, terms) {
12
+ const titleLower = doc.title.toLowerCase();
13
+ const bodyLower = doc.body.toLowerCase();
14
+ const tagsLower = doc.tags.map((tag) => tag.toLowerCase());
15
+ for (const term of terms) {
16
+ const inTitle = titleLower.includes(term);
17
+ const inBody = bodyLower.includes(term);
18
+ const inTags = tagsLower.some((tag) => tag.includes(term));
19
+ if (!inTitle && !inBody && !inTags) {
20
+ return null;
21
+ }
22
+ }
23
+ let score = 0;
24
+ for (const term of terms) {
25
+ if (tagsLower.some((tag) => tag.includes(term))) {
26
+ score += 3;
27
+ }
28
+ if (titleLower.includes(term)) {
29
+ score += 2;
30
+ }
31
+ if (bodyLower.includes(term)) {
32
+ score += 1;
33
+ }
34
+ }
35
+ return score;
36
+ }
37
+ function makeSnippet(body, terms) {
38
+ const lowerBody = body.toLowerCase();
39
+ for (const term of terms) {
40
+ const index = lowerBody.indexOf(term);
41
+ if (index >= 0) {
42
+ const start = Math.max(0, index - 40);
43
+ const end = Math.min(body.length, index + term.length + 40);
44
+ return body.slice(start, end).replace(/\s+/g, ' ').trim();
45
+ }
46
+ }
47
+ return body.slice(0, 120).replace(/\s+/g, ' ').trim();
48
+ }
49
+ export function searchDocs(index, input) {
50
+ if (!index.getKnowledgeRoot()) {
51
+ return [];
52
+ }
53
+ const terms = tokenize(input.query);
54
+ if (terms.length === 0) {
55
+ return [];
56
+ }
57
+ let docs = index.refresh();
58
+ if (input.type) {
59
+ docs = docs.filter((doc) => doc.type === input.type);
60
+ }
61
+ if (input.domain) {
62
+ docs = docs.filter((doc) => doc.domain === input.domain);
63
+ }
64
+ const results = [];
65
+ for (const doc of docs) {
66
+ const score = scoreDoc(doc, terms);
67
+ if (score === null) {
68
+ continue;
69
+ }
70
+ results.push({
71
+ slug: doc.slug,
72
+ title: doc.title,
73
+ snippet: makeSnippet(doc.body, terms),
74
+ score,
75
+ });
76
+ }
77
+ return results
78
+ .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
79
+ .slice(0, 20);
80
+ }
@@ -0,0 +1,12 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { DocIndex } from '../index/doc-index.js';
3
+ import type { DocsWatcher } from './docs-watcher.js';
4
+ export interface ApiResponse {
5
+ status: number;
6
+ body: unknown;
7
+ }
8
+ export declare function handleApiRequest(index: DocIndex, req: IncomingMessage, pathname: string, searchParams: URLSearchParams): ApiResponse;
9
+ export declare function routeApi(index: DocIndex, req: IncomingMessage): ApiResponse | null;
10
+ /** Closes live SSE streams so `server.close()` can finish during shutdown. */
11
+ export declare function closeAllDocsEventStreams(): void;
12
+ export declare function handleDocsEvents(req: IncomingMessage, res: ServerResponse, docsWatcher: DocsWatcher | undefined): void;
@@ -0,0 +1,223 @@
1
+ import { URL } from 'node:url';
2
+ import { collectCheckReport } from '../check/collect-violations.js';
3
+ import { getPackageVersion } from '../package-version.js';
4
+ import { computeLinkHealth, getBacklinksForSlug, } from '../index/link-index.js';
5
+ import { isValidSlug } from '../index/slug.js';
6
+ import { isDocStatus, isDocType } from '../index/types.js';
7
+ import { listUnpreparedFiles } from '../prepare/prepare-docs.js';
8
+ import { buildLinkIndexForDocs, exploreGraph } from '../tools/explore-graph.js';
9
+ import { getDoc } from '../tools/get-doc.js';
10
+ import { getGlossaryTerm } from '../tools/get-glossary-term.js';
11
+ import { listDocs } from '../tools/list-docs.js';
12
+ import { searchDocs } from '../tools/search-docs.js';
13
+ import { ALL_GRAPH_SLUG, exploreGraphAll } from './graph-all.js';
14
+ import { buildDocsTree } from './fs-tree.js';
15
+ import { readCatalogMeta } from './catalog-meta.js';
16
+ import { computeKnowledgeStats } from './stats.js';
17
+ import { listPageTemplates } from './templates.js';
18
+ function jsonError(status, message) {
19
+ return { status, body: { error: message } };
20
+ }
21
+ function parseRequestUrl(req, baseUrl) {
22
+ const host = req.headers.host ?? '127.0.0.1';
23
+ return new URL(req.url ?? '/', `http://${host}${baseUrl}`);
24
+ }
25
+ export function handleApiRequest(index, req, pathname, searchParams) {
26
+ if (pathname === '/api/health') {
27
+ const docs = index.refresh();
28
+ return {
29
+ status: 200,
30
+ body: {
31
+ ok: true,
32
+ version: getPackageVersion(),
33
+ knowledgeRoot: index.getKnowledgeRoot(),
34
+ docCount: docs.length,
35
+ },
36
+ };
37
+ }
38
+ if (pathname === '/api/docs') {
39
+ const typeParam = searchParams.get('type');
40
+ const statusParam = searchParams.get('status');
41
+ const tag = searchParams.get('tag') ?? undefined;
42
+ if (typeParam && !isDocType(typeParam)) {
43
+ return jsonError(400, `invalid type: ${typeParam}`);
44
+ }
45
+ if (statusParam && !isDocStatus(statusParam)) {
46
+ return jsonError(400, `invalid status: ${statusParam}`);
47
+ }
48
+ return {
49
+ status: 200,
50
+ body: {
51
+ docs: listDocs(index, {
52
+ type: typeParam && isDocType(typeParam) ? typeParam : undefined,
53
+ status: statusParam && isDocStatus(statusParam) ? statusParam : undefined,
54
+ tag,
55
+ }),
56
+ },
57
+ };
58
+ }
59
+ if (pathname === '/api/search') {
60
+ const query = searchParams.get('q') ?? '';
61
+ if (!query.trim()) {
62
+ return jsonError(400, 'query parameter q is required');
63
+ }
64
+ const typeParam = searchParams.get('type');
65
+ if (typeParam && !isDocType(typeParam)) {
66
+ return jsonError(400, `invalid type: ${typeParam}`);
67
+ }
68
+ return {
69
+ status: 200,
70
+ body: {
71
+ results: searchDocs(index, {
72
+ query,
73
+ type: typeParam && isDocType(typeParam) ? typeParam : undefined,
74
+ }),
75
+ },
76
+ };
77
+ }
78
+ if (pathname === '/api/stats') {
79
+ return { status: 200, body: computeKnowledgeStats(index) };
80
+ }
81
+ if (pathname === '/api/check') {
82
+ const report = collectCheckReport(index);
83
+ if (!report) {
84
+ return jsonError(404, 'no docs/ directory found');
85
+ }
86
+ return { status: 200, body: report };
87
+ }
88
+ if (pathname === '/api/tree') {
89
+ const tree = buildDocsTree(index);
90
+ if (!tree) {
91
+ return jsonError(404, 'no docs/ directory found');
92
+ }
93
+ return { status: 200, body: { tree, catalogMeta: readCatalogMeta(index.getKnowledgeRoot()) } };
94
+ }
95
+ if (pathname === '/api/templates') {
96
+ return { status: 200, body: { templates: listPageTemplates() } };
97
+ }
98
+ if (pathname === '/api/link-health') {
99
+ const docs = index.refresh();
100
+ const snapshot = buildLinkIndexForDocs(index);
101
+ const health = computeLinkHealth(snapshot, docs);
102
+ return {
103
+ status: 200,
104
+ body: {
105
+ orphanCount: health.orphanSlugs.length,
106
+ orphanSlugs: health.orphanSlugs,
107
+ brokenCount: health.brokenTargets.length,
108
+ brokenTargets: health.brokenTargets,
109
+ oneWayCount: health.oneWayCount,
110
+ },
111
+ };
112
+ }
113
+ const backlinksMatch = pathname.match(/^\/api\/backlinks\/([^/]+)$/);
114
+ if (backlinksMatch) {
115
+ const slug = decodeURIComponent(backlinksMatch[1] ?? '');
116
+ if (!isValidSlug(slug)) {
117
+ return jsonError(400, `invalid slug: ${slug}`);
118
+ }
119
+ const docs = index.refresh();
120
+ const docsBySlug = new Map(docs.map((doc) => [doc.slug, doc]));
121
+ if (!docsBySlug.has(slug)) {
122
+ return jsonError(404, `unknown slug: ${slug}`);
123
+ }
124
+ const snapshot = buildLinkIndexForDocs(index);
125
+ return {
126
+ status: 200,
127
+ body: {
128
+ slug,
129
+ backlinks: getBacklinksForSlug(snapshot, slug, docsBySlug),
130
+ },
131
+ };
132
+ }
133
+ if (pathname === '/api/unprepared') {
134
+ return { status: 200, body: { files: listUnpreparedFiles(index) } };
135
+ }
136
+ const graphMatch = pathname.match(/^\/api\/graph\/([^/]+)$/);
137
+ if (graphMatch) {
138
+ const slug = decodeURIComponent(graphMatch[1] ?? '');
139
+ if (slug === ALL_GRAPH_SLUG) {
140
+ return { status: 200, body: exploreGraphAll(index) };
141
+ }
142
+ if (!isValidSlug(slug)) {
143
+ return jsonError(400, `invalid slug: ${slug}`);
144
+ }
145
+ const depthParam = searchParams.get('depth');
146
+ const depth = depthParam ? Number.parseInt(depthParam, 10) : undefined;
147
+ return {
148
+ status: 200,
149
+ body: exploreGraph(index, { slug, depth }),
150
+ };
151
+ }
152
+ const docMatch = pathname.match(/^\/api\/docs\/([^/]+)$/);
153
+ if (docMatch) {
154
+ const slug = decodeURIComponent(docMatch[1] ?? '');
155
+ if (!isValidSlug(slug)) {
156
+ return jsonError(400, `invalid slug: ${slug}`);
157
+ }
158
+ const doc = index.getDocBySlug(slug);
159
+ if (!doc) {
160
+ return { status: 404, body: { found: false } };
161
+ }
162
+ const agentShape = doc.type === 'glossary-term'
163
+ ? getGlossaryTerm(index, slug)
164
+ : getDoc(index, slug);
165
+ return {
166
+ status: 200,
167
+ body: {
168
+ found: true,
169
+ slug: doc.slug,
170
+ path: doc.path,
171
+ contentKind: doc.contentKind,
172
+ frontmatter: doc.frontmatter,
173
+ body: doc.body,
174
+ agentShape,
175
+ },
176
+ };
177
+ }
178
+ return jsonError(404, 'not found');
179
+ }
180
+ export function routeApi(index, req) {
181
+ const url = parseRequestUrl(req, '');
182
+ if (!url.pathname.startsWith('/api/')) {
183
+ return null;
184
+ }
185
+ return handleApiRequest(index, req, url.pathname, url.searchParams);
186
+ }
187
+ const docsEventClients = new Set();
188
+ /** Closes live SSE streams so `server.close()` can finish during shutdown. */
189
+ export function closeAllDocsEventStreams() {
190
+ for (const res of docsEventClients) {
191
+ if (!res.writableEnded) {
192
+ res.end();
193
+ }
194
+ }
195
+ docsEventClients.clear();
196
+ }
197
+ export function handleDocsEvents(req, res, docsWatcher) {
198
+ if ((req.method ?? 'GET') !== 'GET') {
199
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
200
+ res.end(JSON.stringify({ error: 'method not allowed' }));
201
+ return;
202
+ }
203
+ if (!docsWatcher) {
204
+ res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });
205
+ res.end(JSON.stringify({ error: 'docs watcher unavailable' }));
206
+ return;
207
+ }
208
+ res.writeHead(200, {
209
+ 'Content-Type': 'text/event-stream',
210
+ 'Cache-Control': 'no-cache',
211
+ Connection: 'keep-alive',
212
+ });
213
+ const sendRevision = (revision) => {
214
+ res.write(`data: ${JSON.stringify({ revision })}\n\n`);
215
+ };
216
+ sendRevision(docsWatcher.getRevision());
217
+ const unsubscribe = docsWatcher.subscribe(sendRevision);
218
+ docsEventClients.add(res);
219
+ req.on('close', () => {
220
+ unsubscribe();
221
+ docsEventClients.delete(res);
222
+ });
223
+ }
@@ -0,0 +1,4 @@
1
+ export type CatalogMetaMap = Record<string, string>;
2
+ export declare function readCatalogMeta(knowledgeRoot: string): CatalogMetaMap;
3
+ export declare function writeCatalogEmoji(knowledgeRoot: string, folderPath: string, emoji: string): CatalogMetaMap;
4
+ export declare function catalogEmoji(meta: CatalogMetaMap, folderPath: string): string | null;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { normalizeRelativePath } from './safe-path.js';
4
+ const META_FILE = 'catalog-meta.json';
5
+ function metaPath(knowledgeRoot) {
6
+ return path.join(knowledgeRoot, '.repo-mind', META_FILE);
7
+ }
8
+ export function readCatalogMeta(knowledgeRoot) {
9
+ const filePath = metaPath(knowledgeRoot);
10
+ if (!fs.existsSync(filePath)) {
11
+ return {};
12
+ }
13
+ try {
14
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
15
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
16
+ return {};
17
+ }
18
+ const result = {};
19
+ for (const [key, value] of Object.entries(parsed)) {
20
+ if (typeof value === 'string') {
21
+ result[normalizeRelativePath(key)] = value;
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ export function writeCatalogEmoji(knowledgeRoot, folderPath, emoji) {
31
+ const key = normalizeRelativePath(folderPath);
32
+ const meta = readCatalogMeta(knowledgeRoot);
33
+ const trimmed = emoji.trim();
34
+ if (!trimmed) {
35
+ delete meta[key];
36
+ }
37
+ else {
38
+ meta[key] = trimmed.slice(0, 8);
39
+ }
40
+ const dir = path.join(knowledgeRoot, '.repo-mind');
41
+ fs.mkdirSync(dir, { recursive: true });
42
+ fs.writeFileSync(metaPath(knowledgeRoot), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
43
+ return meta;
44
+ }
45
+ export function catalogEmoji(meta, folderPath) {
46
+ return meta[normalizeRelativePath(folderPath)] ?? null;
47
+ }
@@ -0,0 +1,49 @@
1
+ import type { DocType, DocStatus } from '../../index/types.js';
2
+ export interface DraftRow {
3
+ id: string;
4
+ slug: string;
5
+ type: DocType;
6
+ status: DocStatus;
7
+ title: string;
8
+ body: string;
9
+ tags: string[];
10
+ related: string[];
11
+ published_path: string | null;
12
+ forked_from: string | null;
13
+ target_path: string | null;
14
+ created_at: string;
15
+ updated_at: string;
16
+ }
17
+ export interface CreateDraftInput {
18
+ slug: string;
19
+ type: DocType;
20
+ title?: string;
21
+ body?: string;
22
+ tags?: string[];
23
+ related?: string[];
24
+ forked_from?: string | null;
25
+ target_path?: string | null;
26
+ }
27
+ export interface UpdateDraftInput {
28
+ slug?: string;
29
+ type?: DocType;
30
+ status?: DocStatus;
31
+ title?: string;
32
+ body?: string;
33
+ tags?: string[];
34
+ related?: string[];
35
+ }
36
+ export declare class DraftsDb {
37
+ private readonly db;
38
+ constructor(knowledgeRoot: string);
39
+ private ensureTargetPathColumn;
40
+ close(): void;
41
+ listActive(): DraftRow[];
42
+ getById(id: string): DraftRow | null;
43
+ getActiveBySlug(slug: string): DraftRow | null;
44
+ create(input: CreateDraftInput): DraftRow;
45
+ update(id: string, input: UpdateDraftInput): DraftRow;
46
+ delete(id: string): boolean;
47
+ markPublished(id: string, gitPath: string): DraftRow;
48
+ }
49
+ export declare function openDraftsDb(knowledgeRoot: string): DraftsDb;
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { isDocStatus, isDocType } from '../../index/types.js';
6
+ const SCHEMA_SQL = `
7
+ CREATE TABLE IF NOT EXISTS drafts (
8
+ id TEXT PRIMARY KEY,
9
+ slug TEXT NOT NULL,
10
+ type TEXT NOT NULL,
11
+ status TEXT NOT NULL DEFAULT 'draft',
12
+ title TEXT NOT NULL DEFAULT '',
13
+ body TEXT NOT NULL DEFAULT '',
14
+ tags_json TEXT NOT NULL DEFAULT '[]',
15
+ related_json TEXT NOT NULL DEFAULT '[]',
16
+ published_path TEXT,
17
+ forked_from TEXT,
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL
20
+ );
21
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_drafts_active_slug ON drafts(slug) WHERE published_path IS NULL;
22
+ CREATE TABLE IF NOT EXISTS publish_log (
23
+ id TEXT PRIMARY KEY,
24
+ draft_id TEXT NOT NULL,
25
+ git_path TEXT NOT NULL,
26
+ published_at TEXT NOT NULL
27
+ );
28
+ `;
29
+ function rowToDraft(row) {
30
+ return {
31
+ id: String(row.id),
32
+ slug: String(row.slug),
33
+ type: row.type,
34
+ status: row.status,
35
+ title: String(row.title),
36
+ body: String(row.body),
37
+ tags: JSON.parse(String(row.tags_json)),
38
+ related: JSON.parse(String(row.related_json)),
39
+ published_path: row.published_path ? String(row.published_path) : null,
40
+ forked_from: row.forked_from ? String(row.forked_from) : null,
41
+ target_path: row.target_path ? String(row.target_path) : null,
42
+ created_at: String(row.created_at),
43
+ updated_at: String(row.updated_at),
44
+ };
45
+ }
46
+ export class DraftsDb {
47
+ db;
48
+ constructor(knowledgeRoot) {
49
+ const dbDir = path.join(knowledgeRoot, '.repo-mind');
50
+ fs.mkdirSync(dbDir, { recursive: true });
51
+ const dbPath = path.join(dbDir, 'drafts.db');
52
+ this.db = new Database(dbPath);
53
+ this.db.pragma('journal_mode = WAL');
54
+ this.db.exec(SCHEMA_SQL);
55
+ this.ensureTargetPathColumn();
56
+ }
57
+ ensureTargetPathColumn() {
58
+ const columns = this.db.prepare(`PRAGMA table_info(drafts)`).all();
59
+ if (!columns.some((column) => column.name === 'target_path')) {
60
+ this.db.exec(`ALTER TABLE drafts ADD COLUMN target_path TEXT`);
61
+ }
62
+ }
63
+ close() {
64
+ this.db.close();
65
+ }
66
+ listActive() {
67
+ const rows = this.db
68
+ .prepare(`SELECT * FROM drafts WHERE published_path IS NULL ORDER BY updated_at DESC`)
69
+ .all();
70
+ return rows.map(rowToDraft);
71
+ }
72
+ getById(id) {
73
+ const row = this.db.prepare(`SELECT * FROM drafts WHERE id = ?`).get(id);
74
+ return row ? rowToDraft(row) : null;
75
+ }
76
+ getActiveBySlug(slug) {
77
+ const row = this.db
78
+ .prepare(`SELECT * FROM drafts WHERE slug = ? AND published_path IS NULL`)
79
+ .get(slug);
80
+ return row ? rowToDraft(row) : null;
81
+ }
82
+ create(input) {
83
+ if (!isDocType(input.type)) {
84
+ throw new Error(`invalid type: ${input.type}`);
85
+ }
86
+ const existing = this.getActiveBySlug(input.slug);
87
+ if (existing) {
88
+ throw new Error(`active draft already exists for slug: ${input.slug}`);
89
+ }
90
+ const now = new Date().toISOString();
91
+ const id = randomUUID();
92
+ const tags = JSON.stringify(input.tags ?? []);
93
+ const related = JSON.stringify(input.related ?? []);
94
+ this.db
95
+ .prepare(`INSERT INTO drafts (id, slug, type, status, title, body, tags_json, related_json, forked_from, target_path, created_at, updated_at)
96
+ VALUES (?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?)`)
97
+ .run(id, input.slug, input.type, input.title ?? input.slug, input.body ?? '', tags, related, input.forked_from ?? null, input.target_path ?? null, now, now);
98
+ const created = this.getById(id);
99
+ if (!created) {
100
+ throw new Error('failed to create draft');
101
+ }
102
+ return created;
103
+ }
104
+ update(id, input) {
105
+ const current = this.getById(id);
106
+ if (!current) {
107
+ throw new Error('draft not found');
108
+ }
109
+ if (current.published_path) {
110
+ throw new Error('cannot update published draft');
111
+ }
112
+ if (input.slug && input.slug !== current.slug) {
113
+ const collision = this.getActiveBySlug(input.slug);
114
+ if (collision && collision.id !== id) {
115
+ throw new Error(`active draft already exists for slug: ${input.slug}`);
116
+ }
117
+ }
118
+ if (input.type && !isDocType(input.type)) {
119
+ throw new Error(`invalid type: ${input.type}`);
120
+ }
121
+ if (input.status && !isDocStatus(input.status)) {
122
+ throw new Error(`invalid status: ${input.status}`);
123
+ }
124
+ const next = {
125
+ slug: input.slug ?? current.slug,
126
+ type: input.type ?? current.type,
127
+ status: input.status ?? current.status,
128
+ title: input.title ?? current.title,
129
+ body: input.body ?? current.body,
130
+ tags: input.tags ?? current.tags,
131
+ related: input.related ?? current.related,
132
+ updated_at: new Date().toISOString(),
133
+ };
134
+ this.db
135
+ .prepare(`UPDATE drafts SET slug = ?, type = ?, status = ?, title = ?, body = ?, tags_json = ?, related_json = ?, updated_at = ?
136
+ WHERE id = ?`)
137
+ .run(next.slug, next.type, next.status, next.title, next.body, JSON.stringify(next.tags), JSON.stringify(next.related), next.updated_at, id);
138
+ const updated = this.getById(id);
139
+ if (!updated) {
140
+ throw new Error('failed to update draft');
141
+ }
142
+ return updated;
143
+ }
144
+ delete(id) {
145
+ const current = this.getById(id);
146
+ if (!current) {
147
+ return false;
148
+ }
149
+ if (current.published_path) {
150
+ throw new Error('cannot delete published draft');
151
+ }
152
+ const result = this.db.prepare(`DELETE FROM drafts WHERE id = ?`).run(id);
153
+ return result.changes > 0;
154
+ }
155
+ markPublished(id, gitPath) {
156
+ const current = this.getById(id);
157
+ if (!current) {
158
+ throw new Error('draft not found');
159
+ }
160
+ if (current.published_path) {
161
+ throw new Error('draft already published');
162
+ }
163
+ const now = new Date().toISOString();
164
+ this.db
165
+ .prepare(`UPDATE drafts SET published_path = ?, updated_at = ? WHERE id = ?`)
166
+ .run(gitPath, now, id);
167
+ this.db
168
+ .prepare(`INSERT INTO publish_log (id, draft_id, git_path, published_at) VALUES (?, ?, ?, ?)`)
169
+ .run(randomUUID(), id, gitPath, now);
170
+ const published = this.getById(id);
171
+ if (!published) {
172
+ throw new Error('failed to mark published');
173
+ }
174
+ return published;
175
+ }
176
+ }
177
+ export function openDraftsDb(knowledgeRoot) {
178
+ return new DraftsDb(knowledgeRoot);
179
+ }
@@ -0,0 +1,8 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { DraftRow } from './db/drafts-db.js';
3
+ export interface DraftDiffResult {
4
+ targetPath: string | null;
5
+ isNew: boolean;
6
+ diff: string;
7
+ }
8
+ export declare function computeDraftDiff(index: DocIndex, draft: DraftRow): DraftDiffResult;