@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,64 @@
1
+ function extractBoundary(contentType) {
2
+ const match = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType);
3
+ if (!match) {
4
+ return null;
5
+ }
6
+ return match[1] ?? match[2] ?? null;
7
+ }
8
+ function parseContentDisposition(header) {
9
+ const result = {};
10
+ const nameMatch = /name="([^"]+)"/i.exec(header);
11
+ if (nameMatch) {
12
+ result.name = nameMatch[1];
13
+ }
14
+ const fileMatch = /filename="([^"]+)"/i.exec(header);
15
+ if (fileMatch) {
16
+ result.filename = fileMatch[1];
17
+ }
18
+ return result;
19
+ }
20
+ /** Minimal multipart/form-data parser for single-file uploads. */
21
+ export function parseMultipartFormData(body, contentType) {
22
+ const boundary = extractBoundary(contentType);
23
+ if (!boundary) {
24
+ throw new Error('missing multipart boundary');
25
+ }
26
+ const delimiter = `--${boundary}`;
27
+ const text = body.toString('latin1');
28
+ const parts = text.split(delimiter).slice(1, -1);
29
+ const fields = new Map();
30
+ const files = new Map();
31
+ for (const rawPart of parts) {
32
+ const part = rawPart.replace(/^\r\n/, '').replace(/\r\n$/, '');
33
+ const headerEnd = part.indexOf('\r\n\r\n');
34
+ if (headerEnd === -1) {
35
+ continue;
36
+ }
37
+ const headerBlock = part.slice(0, headerEnd);
38
+ const bodyStart = headerEnd + 4;
39
+ const bodyEnd = part.length - (part.endsWith('\r\n') ? 2 : 0);
40
+ const partBodyLatin1 = part.slice(bodyStart, bodyEnd);
41
+ const partBody = Buffer.from(partBodyLatin1, 'latin1');
42
+ const disposition = headerBlock
43
+ .split('\r\n')
44
+ .find((line) => line.toLowerCase().startsWith('content-disposition:'));
45
+ if (!disposition) {
46
+ continue;
47
+ }
48
+ const { name, filename } = parseContentDisposition(disposition);
49
+ if (!name) {
50
+ continue;
51
+ }
52
+ if (filename) {
53
+ const mimeLine = headerBlock
54
+ .split('\r\n')
55
+ .find((line) => line.toLowerCase().startsWith('content-type:'));
56
+ const mimeType = mimeLine ? mimeLine.split(':')[1]?.trim() ?? 'application/octet-stream' : 'application/octet-stream';
57
+ files.set(name, { fieldName: name, fileName: filename, mimeType, data: partBody });
58
+ }
59
+ else {
60
+ fields.set(name, partBody.toString('utf8'));
61
+ }
62
+ }
63
+ return { fields, files };
64
+ }
@@ -0,0 +1,14 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { DraftRow } from './db/drafts-db.js';
3
+ export interface PublishResult {
4
+ path: string;
5
+ slug: string;
6
+ }
7
+ export interface PublishError {
8
+ code: 'invalid_slug' | 'invalid_type' | 'invalid_status' | 'path_conflict' | 'broken_related';
9
+ message: string;
10
+ }
11
+ export declare function resolvePublishTargetPath(index: DocIndex, draft: DraftRow): string | null;
12
+ export declare function validateDraftForPublish(index: DocIndex, draft: DraftRow): PublishError | null;
13
+ export declare function buildMarkdownFromDraft(draft: DraftRow): string;
14
+ export declare function publishDraft(index: DocIndex, draft: DraftRow): PublishResult;
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { isValidSlug, resolveDocPath } from '../index/slug.js';
5
+ import { TYPE_TO_DIR, isDocStatus, isDocType } from '../index/types.js';
6
+ import { resolveRelativeMdPath } from './safe-path.js';
7
+ export function resolvePublishTargetPath(index, draft) {
8
+ const knowledgeRoot = index.getKnowledgeRoot();
9
+ if (!knowledgeRoot) {
10
+ return null;
11
+ }
12
+ if (draft.target_path) {
13
+ return resolveRelativeMdPath(knowledgeRoot, draft.target_path);
14
+ }
15
+ if (draft.forked_from) {
16
+ const doc = index.getDocBySlug(draft.forked_from);
17
+ if (doc) {
18
+ return doc.path;
19
+ }
20
+ }
21
+ const typeDir = TYPE_TO_DIR[draft.type];
22
+ return resolveDocPath(knowledgeRoot, typeDir, draft.slug);
23
+ }
24
+ export function validateDraftForPublish(index, draft) {
25
+ if (!isValidSlug(draft.slug)) {
26
+ return { code: 'invalid_slug', message: `invalid slug: ${draft.slug}` };
27
+ }
28
+ if (!isDocType(draft.type)) {
29
+ return { code: 'invalid_type', message: `invalid type: ${draft.type}` };
30
+ }
31
+ if (!isDocStatus(draft.status)) {
32
+ return { code: 'invalid_status', message: `invalid status: ${draft.status}` };
33
+ }
34
+ const knowledgeRoot = index.getKnowledgeRoot();
35
+ if (!knowledgeRoot) {
36
+ return { code: 'path_conflict', message: 'no knowledge root' };
37
+ }
38
+ const targetPath = resolvePublishTargetPath(index, draft);
39
+ if (!targetPath) {
40
+ return { code: 'invalid_slug', message: `cannot resolve path for slug: ${draft.slug}` };
41
+ }
42
+ const slugSet = new Set(index.refresh().map((doc) => doc.slug));
43
+ for (const related of draft.related) {
44
+ if (!slugSet.has(related) && related !== draft.slug) {
45
+ return {
46
+ code: 'broken_related',
47
+ message: `broken related slug: ${related}`,
48
+ };
49
+ }
50
+ }
51
+ if (fs.existsSync(targetPath) && !draft.forked_from) {
52
+ return {
53
+ code: 'path_conflict',
54
+ message: `file already exists: ${targetPath}`,
55
+ };
56
+ }
57
+ return null;
58
+ }
59
+ export function buildMarkdownFromDraft(draft) {
60
+ const frontmatter = {
61
+ type: draft.type,
62
+ slug: draft.slug,
63
+ status: draft.status,
64
+ title: draft.title,
65
+ tags: draft.tags,
66
+ related: draft.related,
67
+ updated: new Date().toISOString().slice(0, 10),
68
+ };
69
+ return matter.stringify(draft.body, frontmatter);
70
+ }
71
+ export function publishDraft(index, draft) {
72
+ const validation = validateDraftForPublish(index, draft);
73
+ if (validation) {
74
+ throw new Error(validation.message);
75
+ }
76
+ const knowledgeRoot = index.getKnowledgeRoot();
77
+ const targetPath = resolvePublishTargetPath(index, draft);
78
+ const markdown = buildMarkdownFromDraft(draft);
79
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
80
+ fs.writeFileSync(targetPath, markdown, 'utf8');
81
+ index.refresh();
82
+ return { path: targetPath, slug: draft.slug };
83
+ }
@@ -0,0 +1,6 @@
1
+ export declare function isValidFsName(name: string): boolean;
2
+ export declare function normalizeRelativePath(relative: string): string;
3
+ export declare function resolveUnderKnowledgeRoot(knowledgeRoot: string, relative: string): string | null;
4
+ export declare function resolveRelativeMdPath(knowledgeRoot: string, relative: string): string | null;
5
+ export declare function parentRelativePath(relative: string): string;
6
+ export declare function joinRelativePath(parent: string, name: string): string;
@@ -0,0 +1,42 @@
1
+ import path from 'node:path';
2
+ const FS_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
3
+ export function isValidFsName(name) {
4
+ return FS_NAME_PATTERN.test(name) && name !== '.repo-mind' && name !== '.worktrees';
5
+ }
6
+ export function normalizeRelativePath(relative) {
7
+ return relative.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
8
+ }
9
+ export function resolveUnderKnowledgeRoot(knowledgeRoot, relative) {
10
+ const normalized = normalizeRelativePath(relative);
11
+ const candidate = path.resolve(knowledgeRoot, normalized);
12
+ const rootWithSep = knowledgeRoot.endsWith(path.sep)
13
+ ? knowledgeRoot
14
+ : `${knowledgeRoot}${path.sep}`;
15
+ if (!candidate.startsWith(rootWithSep) && candidate !== knowledgeRoot) {
16
+ return null;
17
+ }
18
+ return candidate;
19
+ }
20
+ export function resolveRelativeMdPath(knowledgeRoot, relative) {
21
+ const normalized = normalizeRelativePath(relative);
22
+ if (!normalized || normalized.endsWith('.md')) {
23
+ const base = resolveUnderKnowledgeRoot(knowledgeRoot, normalized);
24
+ if (!base) {
25
+ return null;
26
+ }
27
+ return base.endsWith('.md') ? base : `${base}.md`;
28
+ }
29
+ return resolveUnderKnowledgeRoot(knowledgeRoot, `${normalized}.md`);
30
+ }
31
+ export function parentRelativePath(relative) {
32
+ const normalized = normalizeRelativePath(relative);
33
+ const idx = normalized.lastIndexOf('/');
34
+ if (idx === -1) {
35
+ return '';
36
+ }
37
+ return normalized.slice(0, idx);
38
+ }
39
+ export function joinRelativePath(parent, name) {
40
+ const base = normalizeRelativePath(parent);
41
+ return base ? `${base}/${name}` : name;
42
+ }
@@ -0,0 +1,4 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ export declare function decodeAssetRelativePath(encodedPath: string): string;
3
+ export declare function serveKnowledgeAsset(res: ServerResponse, knowledgeRoot: string, encodedRelative: string): void;
4
+ export declare function assetExists(knowledgeRoot: string, relative: string): boolean;
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import { isImageFileName, mimeTypeForImagePath } from '../index/asset-file.js';
3
+ import { resolveUnderKnowledgeRoot } from './safe-path.js';
4
+ function sendJson(res, status, body) {
5
+ const payload = JSON.stringify(body);
6
+ res.writeHead(status, {
7
+ 'Content-Type': 'application/json; charset=utf-8',
8
+ 'Content-Length': Buffer.byteLength(payload),
9
+ });
10
+ res.end(payload);
11
+ }
12
+ export function decodeAssetRelativePath(encodedPath) {
13
+ return encodedPath
14
+ .replace(/^\/+/, '')
15
+ .split('/')
16
+ .map((segment) => decodeURIComponent(segment))
17
+ .join('/');
18
+ }
19
+ export function serveKnowledgeAsset(res, knowledgeRoot, encodedRelative) {
20
+ const relative = decodeAssetRelativePath(encodedRelative);
21
+ if (!relative || !isImageFileName(relative)) {
22
+ sendJson(res, 400, { error: 'invalid asset path' });
23
+ return;
24
+ }
25
+ const absolute = resolveUnderKnowledgeRoot(knowledgeRoot, relative);
26
+ if (!absolute || !fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
27
+ sendJson(res, 404, { error: 'asset not found' });
28
+ return;
29
+ }
30
+ const contentType = mimeTypeForImagePath(relative);
31
+ if (!contentType) {
32
+ sendJson(res, 403, { error: 'unsupported asset type' });
33
+ return;
34
+ }
35
+ const data = fs.readFileSync(absolute);
36
+ res.writeHead(200, {
37
+ 'Content-Type': contentType,
38
+ 'Content-Length': data.length,
39
+ 'Cache-Control': 'no-cache',
40
+ });
41
+ res.end(data);
42
+ }
43
+ export function assetExists(knowledgeRoot, relative) {
44
+ if (!isImageFileName(relative)) {
45
+ return false;
46
+ }
47
+ const absolute = resolveUnderKnowledgeRoot(knowledgeRoot, relative);
48
+ return Boolean(absolute && fs.existsSync(absolute) && fs.statSync(absolute).isFile());
49
+ }
@@ -0,0 +1,17 @@
1
+ import http from 'node:http';
2
+ import type { DocIndex } from '../index/doc-index.js';
3
+ import type { DocsWatcher } from './docs-watcher.js';
4
+ import type { DraftsDb } from './db/drafts-db.js';
5
+ export interface UiServerOptions {
6
+ host?: string;
7
+ port: number;
8
+ index: DocIndex;
9
+ staticDir: string;
10
+ draftsDb?: DraftsDb;
11
+ docsWatcher?: DocsWatcher;
12
+ }
13
+ export declare function createUiServer(options: UiServerOptions): http.Server;
14
+ export declare function resolveUiStaticDir(): string;
15
+ export declare function startUiServer(options: UiServerOptions): Promise<http.Server>;
16
+ /** Destroys open HTTP connections (including SSE) so shutdown can complete. */
17
+ export declare function destroyUiServerConnections(server: http.Server): void;
@@ -0,0 +1,237 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { routeApi, handleDocsEvents } from './api-handlers.js';
6
+ import { handleDraftApi } from './draft-api.js';
7
+ import { serveKnowledgeAsset } from './serve-asset.js';
8
+ import { handleAssetUpload, MAX_ASSET_UPLOAD_BYTES } from './upload-asset.js';
9
+ const MIME_TYPES = {
10
+ '.html': 'text/html; charset=utf-8',
11
+ '.js': 'text/javascript; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.svg': 'image/svg+xml',
14
+ '.json': 'application/json; charset=utf-8',
15
+ '.ico': 'image/x-icon',
16
+ '.png': 'image/png',
17
+ '.woff2': 'font/woff2',
18
+ };
19
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
20
+ function readBodyBuffer(req, maxBytes) {
21
+ return new Promise((resolve, reject) => {
22
+ const chunks = [];
23
+ let size = 0;
24
+ req.on('data', (chunk) => {
25
+ size += chunk.length;
26
+ if (size > maxBytes) {
27
+ reject(new Error('request body too large'));
28
+ req.destroy();
29
+ return;
30
+ }
31
+ chunks.push(chunk);
32
+ });
33
+ req.on('end', () => resolve(Buffer.concat(chunks)));
34
+ req.on('error', reject);
35
+ });
36
+ }
37
+ function resolveStaticPath(staticDir, requestPath) {
38
+ const normalized = path.normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, '');
39
+ const filePath = path.join(staticDir, normalized);
40
+ const resolvedStatic = path.resolve(staticDir);
41
+ const resolvedFile = path.resolve(filePath);
42
+ if (!resolvedFile.startsWith(resolvedStatic)) {
43
+ return null;
44
+ }
45
+ return resolvedFile;
46
+ }
47
+ function sendJson(res, status, body) {
48
+ const payload = JSON.stringify(body);
49
+ res.writeHead(status, {
50
+ 'Content-Type': 'application/json; charset=utf-8',
51
+ 'Content-Length': Buffer.byteLength(payload),
52
+ });
53
+ res.end(payload);
54
+ }
55
+ function readBody(req) {
56
+ return new Promise((resolve, reject) => {
57
+ const chunks = [];
58
+ let size = 0;
59
+ req.on('data', (chunk) => {
60
+ size += chunk.length;
61
+ if (size > MAX_BODY_BYTES) {
62
+ reject(new Error('request body too large'));
63
+ req.destroy();
64
+ return;
65
+ }
66
+ chunks.push(chunk);
67
+ });
68
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
69
+ req.on('error', reject);
70
+ });
71
+ }
72
+ function serveStatic(res, staticDir, requestPath) {
73
+ const relative = requestPath === '/' ? 'index.html' : requestPath.replace(/^\//, '');
74
+ let filePath = resolveStaticPath(staticDir, relative);
75
+ if (filePath && fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
76
+ filePath = path.join(filePath, 'index.html');
77
+ }
78
+ if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
79
+ const spaFallback = resolveStaticPath(staticDir, 'index.html');
80
+ if (!spaFallback || !fs.existsSync(spaFallback)) {
81
+ return false;
82
+ }
83
+ filePath = spaFallback;
84
+ }
85
+ const ext = path.extname(filePath);
86
+ const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
87
+ const data = fs.readFileSync(filePath);
88
+ res.writeHead(200, {
89
+ 'Content-Type': contentType,
90
+ 'Content-Length': data.length,
91
+ });
92
+ res.end(data);
93
+ return true;
94
+ }
95
+ export function createUiServer(options) {
96
+ const host = options.host ?? '127.0.0.1';
97
+ const { staticDir, index, draftsDb, docsWatcher } = options;
98
+ return http.createServer((req, res) => {
99
+ void (async () => {
100
+ const method = req.method ?? 'GET';
101
+ const urlPath = new URL(req.url ?? '/', `http://${host}`).pathname;
102
+ if (urlPath.startsWith('/api/')) {
103
+ if (urlPath === '/api/assets/upload' && method === 'POST') {
104
+ const knowledgeRoot = index.getKnowledgeRoot();
105
+ if (!knowledgeRoot) {
106
+ sendJson(res, 404, { error: 'no docs/ directory found' });
107
+ return;
108
+ }
109
+ const contentType = req.headers['content-type'] ?? '';
110
+ if (!contentType.toLowerCase().includes('multipart/form-data')) {
111
+ sendJson(res, 400, { error: 'multipart/form-data required' });
112
+ return;
113
+ }
114
+ let bodyBuffer;
115
+ try {
116
+ bodyBuffer = await readBodyBuffer(req, MAX_ASSET_UPLOAD_BYTES);
117
+ }
118
+ catch (error) {
119
+ const message = error instanceof Error ? error.message : 'bad request';
120
+ sendJson(res, 400, { error: message });
121
+ return;
122
+ }
123
+ const uploadResponse = handleAssetUpload(knowledgeRoot, bodyBuffer, contentType);
124
+ sendJson(res, uploadResponse.status, uploadResponse.body);
125
+ return;
126
+ }
127
+ if (urlPath.startsWith('/api/assets/') && method === 'GET') {
128
+ const knowledgeRoot = index.getKnowledgeRoot();
129
+ if (!knowledgeRoot) {
130
+ sendJson(res, 404, { error: 'no docs/ directory found' });
131
+ return;
132
+ }
133
+ serveKnowledgeAsset(res, knowledgeRoot, urlPath.slice('/api/assets/'.length));
134
+ return;
135
+ }
136
+ if (urlPath === '/api/events' && method === 'GET') {
137
+ handleDocsEvents(req, res, docsWatcher);
138
+ return;
139
+ }
140
+ let bodyRaw = '';
141
+ if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
142
+ try {
143
+ bodyRaw = await readBody(req);
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : 'bad request';
147
+ sendJson(res, 400, { error: message });
148
+ return;
149
+ }
150
+ }
151
+ const draftResponse = handleDraftApi(index, draftsDb, method, urlPath, bodyRaw);
152
+ if (draftResponse) {
153
+ sendJson(res, draftResponse.status, draftResponse.body);
154
+ return;
155
+ }
156
+ if (urlPath.startsWith('/api/drafts')) {
157
+ sendJson(res, 503, { error: 'drafts database unavailable' });
158
+ return;
159
+ }
160
+ if (method !== 'GET' && method !== 'HEAD') {
161
+ sendJson(res, 405, { error: 'method not allowed' });
162
+ return;
163
+ }
164
+ const apiResponse = routeApi(index, req);
165
+ if (apiResponse) {
166
+ if (method === 'HEAD') {
167
+ res.writeHead(apiResponse.status);
168
+ res.end();
169
+ return;
170
+ }
171
+ sendJson(res, apiResponse.status, apiResponse.body);
172
+ return;
173
+ }
174
+ sendJson(res, 404, { error: 'not found' });
175
+ return;
176
+ }
177
+ if (method !== 'GET' && method !== 'HEAD') {
178
+ sendJson(res, 405, { error: 'method not allowed' });
179
+ return;
180
+ }
181
+ if (method === 'HEAD') {
182
+ res.writeHead(200);
183
+ res.end();
184
+ return;
185
+ }
186
+ if (!serveStatic(res, staticDir, urlPath)) {
187
+ sendJson(res, 503, {
188
+ error: 'UI assets not found — run `npm run build:ui` in the repo-mind package',
189
+ });
190
+ }
191
+ })().catch((error) => {
192
+ const message = error instanceof Error ? error.message : 'internal error';
193
+ sendJson(res, 500, { error: message });
194
+ });
195
+ });
196
+ }
197
+ export function resolveUiStaticDir() {
198
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
199
+ const packageRoot = path.join(moduleDir, '..', '..');
200
+ return path.join(packageRoot, 'ui', 'dist');
201
+ }
202
+ export function startUiServer(options) {
203
+ const host = options.host ?? '127.0.0.1';
204
+ const server = createUiServer(options);
205
+ attachActiveConnectionTracking(server);
206
+ return new Promise((resolve, reject) => {
207
+ server.once('error', reject);
208
+ server.listen(options.port, host, () => {
209
+ server.removeListener('error', reject);
210
+ resolve(server);
211
+ });
212
+ });
213
+ }
214
+ const activeSockets = new WeakMap();
215
+ function attachActiveConnectionTracking(server) {
216
+ const sockets = new Set();
217
+ activeSockets.set(server, sockets);
218
+ server.on('connection', (socket) => {
219
+ sockets.add(socket);
220
+ socket.on('close', () => {
221
+ sockets.delete(socket);
222
+ });
223
+ });
224
+ }
225
+ /** Destroys open HTTP connections (including SSE) so shutdown can complete. */
226
+ export function destroyUiServerConnections(server) {
227
+ const sockets = activeSockets.get(server);
228
+ if (sockets) {
229
+ for (const socket of sockets) {
230
+ socket.destroy();
231
+ }
232
+ sockets.clear();
233
+ }
234
+ if (typeof server.closeAllConnections === 'function') {
235
+ server.closeAllConnections();
236
+ }
237
+ }
@@ -0,0 +1,9 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import { type DocStatus, type DocType } from '../index/types.js';
3
+ export interface KnowledgeStats {
4
+ byType: Record<DocType, number>;
5
+ byStatus: Record<DocStatus, number>;
6
+ brokenRelatedCount: number;
7
+ totalDocs: number;
8
+ }
9
+ export declare function computeKnowledgeStats(index: DocIndex): KnowledgeStats;
@@ -0,0 +1,23 @@
1
+ import { DOC_STATUSES, DOC_TYPES } from '../index/types.js';
2
+ export function computeKnowledgeStats(index) {
3
+ const byType = Object.fromEntries(DOC_TYPES.map((t) => [t, 0]));
4
+ const byStatus = Object.fromEntries(DOC_STATUSES.map((s) => [s, 0]));
5
+ const docs = index.refresh();
6
+ const slugSet = new Set(docs.map((doc) => doc.slug));
7
+ let brokenRelatedCount = 0;
8
+ for (const doc of docs) {
9
+ byType[doc.type] += 1;
10
+ byStatus[doc.status] += 1;
11
+ for (const related of doc.related) {
12
+ if (!slugSet.has(related)) {
13
+ brokenRelatedCount += 1;
14
+ }
15
+ }
16
+ }
17
+ return {
18
+ byType,
19
+ byStatus,
20
+ brokenRelatedCount,
21
+ totalDocs: docs.length,
22
+ };
23
+ }
@@ -0,0 +1,12 @@
1
+ import type { DocType } from '../index/types.js';
2
+ export interface TemplateInfo {
3
+ id: string;
4
+ label: string;
5
+ type: DocType;
6
+ filename: string;
7
+ }
8
+ export declare function listPageTemplates(): TemplateInfo[];
9
+ export declare function readPageTemplate(templateId: string): {
10
+ body: string;
11
+ title?: string;
12
+ };
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { fileURLToPath } from 'node:url';
5
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
6
+ const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
7
+ const TEMPLATE_LABELS = {
8
+ 'adr-example': { label: 'ADR', type: 'adr' },
9
+ 'feature-spec-example': { label: 'Feature spec', type: 'feature-spec' },
10
+ 'glossary-term-example': { label: 'Glossary term', type: 'glossary-term' },
11
+ 'open-question-example': { label: 'Open question', type: 'open-question' },
12
+ 'agent-instruction-example': { label: 'Agent instruction', type: 'agent-instruction' },
13
+ 'combat-system-example': { label: 'Feature spec (example)', type: 'feature-spec' },
14
+ };
15
+ export function listPageTemplates() {
16
+ if (!fs.existsSync(TEMPLATES_DIR)) {
17
+ return [];
18
+ }
19
+ const templates = [];
20
+ for (const filename of fs.readdirSync(TEMPLATES_DIR)) {
21
+ if (!filename.endsWith('.md')) {
22
+ continue;
23
+ }
24
+ const id = filename.replace(/\.md$/, '');
25
+ const meta = TEMPLATE_LABELS[id] ?? { label: id, type: 'wiki-page' };
26
+ templates.push({ id, label: meta.label, type: meta.type, filename });
27
+ }
28
+ return templates.sort((a, b) => a.label.localeCompare(b.label));
29
+ }
30
+ export function readPageTemplate(templateId) {
31
+ const safeId = templateId.replace(/[^a-zA-Z0-9_-]/g, '');
32
+ const filePath = path.join(TEMPLATES_DIR, `${safeId}.md`);
33
+ if (!fs.existsSync(filePath)) {
34
+ throw new Error(`unknown template: ${templateId}`);
35
+ }
36
+ const parsed = matter(fs.readFileSync(filePath, 'utf8'));
37
+ const title = typeof parsed.data.title === 'string' ? parsed.data.title : undefined;
38
+ return { body: parsed.content.trimStart(), title };
39
+ }
@@ -0,0 +1,11 @@
1
+ export declare const MAX_ASSET_UPLOAD_BYTES: number;
2
+ export interface AssetUploadResult {
3
+ relativePath: string;
4
+ url: string;
5
+ }
6
+ export declare function handleAssetUpload(knowledgeRoot: string, body: Buffer, contentType: string): {
7
+ status: number;
8
+ body: AssetUploadResult | {
9
+ error: string;
10
+ };
11
+ };