@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.
- package/README.md +110 -0
- package/dist/ab-demo/arm-baseline.d.ts +8 -0
- package/dist/ab-demo/arm-baseline.js +40 -0
- package/dist/ab-demo/arm-repomind.d.ts +7 -0
- package/dist/ab-demo/arm-repomind.js +35 -0
- package/dist/ab-demo/estimate-tokens.d.ts +3 -0
- package/dist/ab-demo/estimate-tokens.js +10 -0
- package/dist/ab-demo/load-questions.d.ts +2 -0
- package/dist/ab-demo/load-questions.js +65 -0
- package/dist/ab-demo/paths.d.ts +5 -0
- package/dist/ab-demo/paths.js +31 -0
- package/dist/ab-demo/run-ab.d.ts +7 -0
- package/dist/ab-demo/run-ab.js +128 -0
- package/dist/ab-demo/run-arms.d.ts +3 -0
- package/dist/ab-demo/run-arms.js +67 -0
- package/dist/ab-demo/session-overhead.d.ts +3 -0
- package/dist/ab-demo/session-overhead.js +68 -0
- package/dist/ab-demo/types.d.ts +65 -0
- package/dist/ab-demo/types.js +1 -0
- package/dist/ab-demo/validate-corpus.d.ts +3 -0
- package/dist/ab-demo/validate-corpus.js +38 -0
- package/dist/check/collect-violations.d.ts +11 -0
- package/dist/check/collect-violations.js +127 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +19 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +80 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +86 -0
- package/dist/commands/prepare.d.ts +7 -0
- package/dist/commands/prepare.js +61 -0
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.js +84 -0
- package/dist/commands/sync-links.d.ts +7 -0
- package/dist/commands/sync-links.js +41 -0
- package/dist/commands/ui.d.ts +5 -0
- package/dist/commands/ui.js +83 -0
- package/dist/index/asset-file.d.ts +4 -0
- package/dist/index/asset-file.js +26 -0
- package/dist/index/doc-index.d.ts +21 -0
- package/dist/index/doc-index.js +231 -0
- package/dist/index/knowledge-file.d.ts +4 -0
- package/dist/index/knowledge-file.js +17 -0
- package/dist/index/link-index.d.ts +41 -0
- package/dist/index/link-index.js +150 -0
- package/dist/index/path-inference.d.ts +9 -0
- package/dist/index/path-inference.js +33 -0
- package/dist/index/resolve-asset-href.d.ts +2 -0
- package/dist/index/resolve-asset-href.js +65 -0
- package/dist/index/resolve-md-href.d.ts +7 -0
- package/dist/index/resolve-md-href.js +116 -0
- package/dist/index/slug.d.ts +5 -0
- package/dist/index/slug.js +43 -0
- package/dist/index/types.d.ts +44 -0
- package/dist/index/types.js +71 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +155 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +15 -0
- package/dist/prepare/auto-links.d.ts +22 -0
- package/dist/prepare/auto-links.js +124 -0
- package/dist/prepare/prepare-docs.d.ts +36 -0
- package/dist/prepare/prepare-docs.js +106 -0
- package/dist/tools/explore-graph.d.ts +25 -0
- package/dist/tools/explore-graph.js +84 -0
- package/dist/tools/get-doc.d.ts +11 -0
- package/dist/tools/get-doc.js +21 -0
- package/dist/tools/get-glossary-term.d.ts +9 -0
- package/dist/tools/get-glossary-term.js +41 -0
- package/dist/tools/list-docs.d.ts +19 -0
- package/dist/tools/list-docs.js +35 -0
- package/dist/tools/search-docs.d.ts +14 -0
- package/dist/tools/search-docs.js +80 -0
- package/dist/ui/api-handlers.d.ts +12 -0
- package/dist/ui/api-handlers.js +223 -0
- package/dist/ui/catalog-meta.d.ts +4 -0
- package/dist/ui/catalog-meta.js +47 -0
- package/dist/ui/db/drafts-db.d.ts +49 -0
- package/dist/ui/db/drafts-db.js +179 -0
- package/dist/ui/diff.d.ts +8 -0
- package/dist/ui/diff.js +58 -0
- package/dist/ui/docs-watcher.d.ts +13 -0
- package/dist/ui/docs-watcher.js +59 -0
- package/dist/ui/draft-api.d.ts +7 -0
- package/dist/ui/draft-api.js +413 -0
- package/dist/ui/fs-operations.d.ts +52 -0
- package/dist/ui/fs-operations.js +304 -0
- package/dist/ui/fs-tree.d.ts +28 -0
- package/dist/ui/fs-tree.js +148 -0
- package/dist/ui/graph-all.d.ts +5 -0
- package/dist/ui/graph-all.js +50 -0
- package/dist/ui/link-cascade.d.ts +9 -0
- package/dist/ui/link-cascade.js +113 -0
- package/dist/ui/parse-multipart.d.ts +12 -0
- package/dist/ui/parse-multipart.js +64 -0
- package/dist/ui/publish.d.ts +14 -0
- package/dist/ui/publish.js +83 -0
- package/dist/ui/safe-path.d.ts +6 -0
- package/dist/ui/safe-path.js +42 -0
- package/dist/ui/serve-asset.d.ts +4 -0
- package/dist/ui/serve-asset.js +49 -0
- package/dist/ui/server.d.ts +17 -0
- package/dist/ui/server.js +237 -0
- package/dist/ui/stats.d.ts +9 -0
- package/dist/ui/stats.js +23 -0
- package/dist/ui/templates.d.ts +12 -0
- package/dist/ui/templates.js +39 -0
- package/dist/ui/upload-asset.d.ts +11 -0
- package/dist/ui/upload-asset.js +61 -0
- package/package.json +55 -0
- package/templates/adr-example.md +27 -0
- package/templates/agent-instruction-example.md +20 -0
- package/templates/combat-system-example.md +27 -0
- package/templates/feature-spec-example.md +27 -0
- package/templates/glossary-term-example.md +15 -0
- package/templates/open-question-example.md +26 -0
- package/ui/dist/assets/arc-DhC0JPue.js +1 -0
- package/ui/dist/assets/architectureDiagram-3BPJPVTR-Cun_Ijrv.js +36 -0
- package/ui/dist/assets/blockDiagram-GPEHLZMM-CgiNAArN.js +132 -0
- package/ui/dist/assets/c4Diagram-AAUBKEIU-BIwHcwcH.js +10 -0
- package/ui/dist/assets/channel-CNwAp9ic.js +1 -0
- package/ui/dist/assets/chunk-2J33WTMH-DXRgHPpp.js +1 -0
- package/ui/dist/assets/chunk-4BX2VUAB-BTb70kIb.js +1 -0
- package/ui/dist/assets/chunk-55IACEB6-BrAelyhX.js +1 -0
- package/ui/dist/assets/chunk-727SXJPM-BlYnlPdj.js +206 -0
- package/ui/dist/assets/chunk-AQP2D5EJ-DSPgdKZ8.js +231 -0
- package/ui/dist/assets/chunk-FMBD7UC4-BhH8ir2K.js +15 -0
- package/ui/dist/assets/chunk-ND2GUHAM-DCAuTSxB.js +1 -0
- package/ui/dist/assets/chunk-QZHKN3VN-DtYEkbYr.js +1 -0
- package/ui/dist/assets/classDiagram-4FO5ZUOK-DnHeGLmR.js +1 -0
- package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-DnHeGLmR.js +1 -0
- package/ui/dist/assets/cose-bilkent-S5V4N54A-CAM4jLYo.js +1 -0
- package/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/ui/dist/assets/dagre-BM42HDAG-CISbgani.js +4 -0
- package/ui/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/ui/dist/assets/diagram-2AECGRRQ-BmXargwF.js +43 -0
- package/ui/dist/assets/diagram-5GNKFQAL-COlrLu0O.js +10 -0
- package/ui/dist/assets/diagram-KO2AKTUF-B-kUxuHX.js +3 -0
- package/ui/dist/assets/diagram-LMA3HP47-C3AVVxcm.js +24 -0
- package/ui/dist/assets/diagram-OG6HWLK6-JHeftSsO.js +24 -0
- package/ui/dist/assets/erDiagram-TEJ5UH35-BSWwMysi.js +85 -0
- package/ui/dist/assets/flowDiagram-I6XJVG4X-D-q1cK69.js +162 -0
- package/ui/dist/assets/ganttDiagram-6RSMTGT7-DrYn1H_t.js +292 -0
- package/ui/dist/assets/gitGraphDiagram-PVQCEYII-vJByl99X.js +106 -0
- package/ui/dist/assets/graph-CAnANduQ.js +1 -0
- package/ui/dist/assets/graph-DwoitsWW.js +2 -0
- package/ui/dist/assets/infoDiagram-5YYISTIA-D6zhGTMj.js +2 -0
- package/ui/dist/assets/init-Gi6I4Gst.js +1 -0
- package/ui/dist/assets/ishikawaDiagram-YF4QCWOH-CY-U_l7l.js +70 -0
- package/ui/dist/assets/journeyDiagram-JHISSGLW-jKj4lBEJ.js +139 -0
- package/ui/dist/assets/kanban-definition-UN3LZRKU-PZ-5AYw2.js +89 -0
- package/ui/dist/assets/katex-C5jXJg4s.js +257 -0
- package/ui/dist/assets/layout-DGIYPm2g.js +1 -0
- package/ui/dist/assets/linear-COY9pyF4.js +1 -0
- package/ui/dist/assets/main-BBzCq-49.js +308 -0
- package/ui/dist/assets/mermaid.core-Bddhr0ku.js +309 -0
- package/ui/dist/assets/mindmap-definition-RKZ34NQL-CPY2Fdu_.js +96 -0
- package/ui/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/ui/dist/assets/pieDiagram-4H26LBE5-C7GJ49et.js +30 -0
- package/ui/dist/assets/quadrantDiagram-W4KKPZXB-DQyQN5K7.js +7 -0
- package/ui/dist/assets/requirementDiagram-4Y6WPE33-CDrkwz1t.js +84 -0
- package/ui/dist/assets/sankeyDiagram-5OEKKPKP-BrYb9Eql.js +40 -0
- package/ui/dist/assets/sequenceDiagram-3UESZ5HK-B8If_JZp.js +162 -0
- package/ui/dist/assets/stateDiagram-AJRCARHV-BbpTp9VX.js +1 -0
- package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-BT4PvMFS.js +1 -0
- package/ui/dist/assets/theme-DV7vqTnV.js +1 -0
- package/ui/dist/assets/theme-SpsWsRN5.css +1 -0
- package/ui/dist/assets/timeline-definition-PNZ67QCA-DhUg6aIV.js +120 -0
- package/ui/dist/assets/transform-BwXaE9hv.js +1 -0
- package/ui/dist/assets/vennDiagram-CIIHVFJN-DpQVNNzF.js +34 -0
- package/ui/dist/assets/wardley-L42UT6IY-CyaxzHGP.js +173 -0
- package/ui/dist/assets/wardleyDiagram-YWT4CUSO-Bm0mA7wm.js +78 -0
- package/ui/dist/assets/xychartDiagram-2RQKCTM6-OJbmgDx6.js +7 -0
- package/ui/dist/graph.html +27 -0
- package/ui/dist/index.html +37 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { getPackageVersion } from '../package-version.js';
|
|
5
|
+
import { DocIndex } from '../index/doc-index.js';
|
|
6
|
+
import { DOC_TYPES, DOC_STATUSES, DOC_DOMAINS } from '../index/types.js';
|
|
7
|
+
import { exploreGraph } from '../tools/explore-graph.js';
|
|
8
|
+
import { getDoc } from '../tools/get-doc.js';
|
|
9
|
+
import { getGlossaryTerm } from '../tools/get-glossary-term.js';
|
|
10
|
+
import { listDocs } from '../tools/list-docs.js';
|
|
11
|
+
import { searchDocs } from '../tools/search-docs.js';
|
|
12
|
+
const TOOL_NAMES = [
|
|
13
|
+
'list_docs',
|
|
14
|
+
'search_docs',
|
|
15
|
+
'get_doc',
|
|
16
|
+
'get_glossary_term',
|
|
17
|
+
'explore_graph',
|
|
18
|
+
];
|
|
19
|
+
function isToolName(name) {
|
|
20
|
+
return TOOL_NAMES.includes(name);
|
|
21
|
+
}
|
|
22
|
+
let acceptingCalls = true;
|
|
23
|
+
export async function startMcpServer() {
|
|
24
|
+
const index = new DocIndex(process.cwd());
|
|
25
|
+
const server = new Server({ name: 'repo-mind', version: getPackageVersion() }, { capabilities: { tools: {} } });
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
27
|
+
tools: [
|
|
28
|
+
{
|
|
29
|
+
name: 'list_docs',
|
|
30
|
+
description: 'List project knowledge documents with optional filters.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
type: { type: 'string', enum: [...DOC_TYPES] },
|
|
35
|
+
status: { type: 'string', enum: [...DOC_STATUSES] },
|
|
36
|
+
tag: { type: 'string' },
|
|
37
|
+
domain: { type: 'string', enum: [...DOC_DOMAINS] },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'search_docs',
|
|
43
|
+
description: 'Search project knowledge documents by query.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
query: { type: 'string' },
|
|
48
|
+
type: { type: 'string', enum: [...DOC_TYPES] },
|
|
49
|
+
domain: { type: 'string', enum: [...DOC_DOMAINS] },
|
|
50
|
+
},
|
|
51
|
+
required: ['query'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'get_doc',
|
|
56
|
+
description: 'Fetch a single document by slug.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
slug: { type: 'string' },
|
|
61
|
+
},
|
|
62
|
+
required: ['slug'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'get_glossary_term',
|
|
67
|
+
description: 'Resolve a glossary term by name.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
name: { type: 'string' },
|
|
72
|
+
},
|
|
73
|
+
required: ['name'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'explore_graph',
|
|
78
|
+
description: 'Explore related documents as a graph.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
slug: { type: 'string' },
|
|
83
|
+
depth: { type: 'number' },
|
|
84
|
+
},
|
|
85
|
+
required: ['slug'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}));
|
|
90
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
91
|
+
if (!acceptingCalls) {
|
|
92
|
+
throw new Error('Server is shutting down');
|
|
93
|
+
}
|
|
94
|
+
const args = (request.params.arguments ?? {});
|
|
95
|
+
const toolName = request.params.name;
|
|
96
|
+
if (!isToolName(toolName)) {
|
|
97
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
98
|
+
}
|
|
99
|
+
switch (toolName) {
|
|
100
|
+
case 'list_docs':
|
|
101
|
+
return toolResult(listDocs(index, {
|
|
102
|
+
type: typeof args.type === 'string' ? args.type : undefined,
|
|
103
|
+
status: typeof args.status === 'string'
|
|
104
|
+
? args.status
|
|
105
|
+
: undefined,
|
|
106
|
+
tag: typeof args.tag === 'string' ? args.tag : undefined,
|
|
107
|
+
domain: typeof args.domain === 'string'
|
|
108
|
+
? args.domain
|
|
109
|
+
: undefined,
|
|
110
|
+
}));
|
|
111
|
+
case 'search_docs':
|
|
112
|
+
return toolResult(searchDocs(index, {
|
|
113
|
+
query: typeof args.query === 'string' ? args.query : '',
|
|
114
|
+
type: typeof args.type === 'string' ? args.type : undefined,
|
|
115
|
+
domain: typeof args.domain === 'string'
|
|
116
|
+
? args.domain
|
|
117
|
+
: undefined,
|
|
118
|
+
}));
|
|
119
|
+
case 'get_doc':
|
|
120
|
+
return toolResult(getDoc(index, typeof args.slug === 'string' ? args.slug : ''));
|
|
121
|
+
case 'get_glossary_term':
|
|
122
|
+
return toolResult(getGlossaryTerm(index, typeof args.name === 'string' ? args.name : ''));
|
|
123
|
+
case 'explore_graph':
|
|
124
|
+
return toolResult(exploreGraph(index, {
|
|
125
|
+
slug: typeof args.slug === 'string' ? args.slug : '',
|
|
126
|
+
depth: typeof args.depth === 'number' ? args.depth : undefined,
|
|
127
|
+
}));
|
|
128
|
+
default: {
|
|
129
|
+
const unknownTool = toolName;
|
|
130
|
+
throw new Error(`Unknown tool: ${unknownTool}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const shutdown = async () => {
|
|
135
|
+
acceptingCalls = false;
|
|
136
|
+
await server.close();
|
|
137
|
+
process.exit(0);
|
|
138
|
+
};
|
|
139
|
+
process.on('SIGTERM', () => {
|
|
140
|
+
void shutdown();
|
|
141
|
+
});
|
|
142
|
+
process.on('SIGINT', () => {
|
|
143
|
+
void shutdown();
|
|
144
|
+
});
|
|
145
|
+
process.stdin.on('close', () => {
|
|
146
|
+
void shutdown();
|
|
147
|
+
});
|
|
148
|
+
const transport = new StdioServerTransport();
|
|
149
|
+
await server.connect(transport);
|
|
150
|
+
}
|
|
151
|
+
function toolResult(payload) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
let cachedVersion;
|
|
5
|
+
/** Package version from repo-mind `package.json` (single source for UI + MCP). */
|
|
6
|
+
export function getPackageVersion() {
|
|
7
|
+
if (cachedVersion) {
|
|
8
|
+
return cachedVersion;
|
|
9
|
+
}
|
|
10
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const packageJsonPath = path.join(moduleDir, '..', 'package.json');
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
13
|
+
cachedVersion = typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
14
|
+
return cachedVersion;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import type { DocRecord } from '../index/types.js';
|
|
3
|
+
export interface SyncLinksOptions {
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
/** Rewrite `[text](page.md)` as `[[slug]]` when the target resolves. */
|
|
6
|
+
convertMarkdownLinks?: boolean;
|
|
7
|
+
/** Merge outbound link slugs into frontmatter `related`. */
|
|
8
|
+
syncRelated?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SyncLinksFileResult {
|
|
11
|
+
relativePath: string;
|
|
12
|
+
convertedLinks: number;
|
|
13
|
+
addedRelated: string[];
|
|
14
|
+
changed: boolean;
|
|
15
|
+
skipped: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SyncLinksResult {
|
|
19
|
+
files: SyncLinksFileResult[];
|
|
20
|
+
}
|
|
21
|
+
export declare function syncDocLinks(index: DocIndex, doc: DocRecord, docs: DocRecord[], options?: SyncLinksOptions): SyncLinksFileResult;
|
|
22
|
+
export declare function syncAllDocLinks(index: DocIndex, options?: SyncLinksOptions): SyncLinksResult;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { parseWikilinkTargets, resolveWikilinkTarget } from '../index/link-index.js';
|
|
4
|
+
import { slugForMarkdownHref } from '../index/resolve-md-href.js';
|
|
5
|
+
const MARKDOWN_LINK_PATTERN = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
6
|
+
function normalizeStringArray(value) {
|
|
7
|
+
if (!Array.isArray(value)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return value.filter((item) => typeof item === 'string');
|
|
11
|
+
}
|
|
12
|
+
function buildSlugByRelative(docs) {
|
|
13
|
+
return new Map(docs.map((doc) => [doc.relativePath, doc.slug]));
|
|
14
|
+
}
|
|
15
|
+
function buildSlugLookups(docs) {
|
|
16
|
+
const slugSet = new Set(docs.map((doc) => doc.slug));
|
|
17
|
+
const titleToSlug = new Map();
|
|
18
|
+
for (const doc of docs) {
|
|
19
|
+
titleToSlug.set(doc.slug.toLowerCase(), doc.slug);
|
|
20
|
+
titleToSlug.set(doc.title.toLowerCase(), doc.slug);
|
|
21
|
+
}
|
|
22
|
+
return { slugSet, titleToSlug };
|
|
23
|
+
}
|
|
24
|
+
function collectOutboundSlugs(body, fromDoc, docs) {
|
|
25
|
+
const slugByRelative = buildSlugByRelative(docs);
|
|
26
|
+
const lookups = buildSlugLookups(docs);
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const result = [];
|
|
29
|
+
function addSlug(slug) {
|
|
30
|
+
if (!slug || slug === fromDoc.slug || seen.has(slug)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
seen.add(slug);
|
|
34
|
+
result.push(slug);
|
|
35
|
+
}
|
|
36
|
+
for (const raw of parseWikilinkTargets(body)) {
|
|
37
|
+
const resolved = resolveWikilinkTarget(raw, lookups);
|
|
38
|
+
addSlug(resolved.slug);
|
|
39
|
+
}
|
|
40
|
+
for (const match of body.matchAll(MARKDOWN_LINK_PATTERN)) {
|
|
41
|
+
const href = match[2]?.trim() ?? '';
|
|
42
|
+
addSlug(slugForMarkdownHref(fromDoc.relativePath, href, slugByRelative));
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function convertMarkdownLinksToWikilinks(body, fromRelative, slugByRelative) {
|
|
47
|
+
let count = 0;
|
|
48
|
+
const nextBody = body.replace(MARKDOWN_LINK_PATTERN, (full, text, href) => {
|
|
49
|
+
const slug = slugForMarkdownHref(fromRelative, href.trim(), slugByRelative);
|
|
50
|
+
if (!slug) {
|
|
51
|
+
return full;
|
|
52
|
+
}
|
|
53
|
+
count += 1;
|
|
54
|
+
const label = text.trim();
|
|
55
|
+
if (!label || label.toLowerCase() === slug.toLowerCase()) {
|
|
56
|
+
return `[[${slug}]]`;
|
|
57
|
+
}
|
|
58
|
+
return `[[${label}|${slug}]]`;
|
|
59
|
+
});
|
|
60
|
+
return { body: nextBody, count };
|
|
61
|
+
}
|
|
62
|
+
export function syncDocLinks(index, doc, docs, options = {}) {
|
|
63
|
+
const convertMarkdownLinks = options.convertMarkdownLinks ?? true;
|
|
64
|
+
const syncRelated = options.syncRelated ?? true;
|
|
65
|
+
if (doc.contentKind !== 'markdown') {
|
|
66
|
+
return {
|
|
67
|
+
relativePath: doc.relativePath,
|
|
68
|
+
convertedLinks: 0,
|
|
69
|
+
addedRelated: [],
|
|
70
|
+
changed: false,
|
|
71
|
+
skipped: true,
|
|
72
|
+
reason: 'not markdown',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const raw = fs.readFileSync(doc.path, 'utf8');
|
|
76
|
+
const parsed = matter(raw);
|
|
77
|
+
const data = parsed.data;
|
|
78
|
+
let body = parsed.content;
|
|
79
|
+
const currentRelated = normalizeStringArray(data.related);
|
|
80
|
+
const slugByRelative = buildSlugByRelative(docs);
|
|
81
|
+
let convertedLinks = 0;
|
|
82
|
+
if (convertMarkdownLinks) {
|
|
83
|
+
const converted = convertMarkdownLinksToWikilinks(body, doc.relativePath, slugByRelative);
|
|
84
|
+
body = converted.body;
|
|
85
|
+
convertedLinks = converted.count;
|
|
86
|
+
}
|
|
87
|
+
const outbound = collectOutboundSlugs(body, doc, docs);
|
|
88
|
+
const relatedSet = new Set(currentRelated);
|
|
89
|
+
const addedRelated = [];
|
|
90
|
+
if (syncRelated) {
|
|
91
|
+
for (const slug of outbound) {
|
|
92
|
+
if (!relatedSet.has(slug)) {
|
|
93
|
+
relatedSet.add(slug);
|
|
94
|
+
addedRelated.push(slug);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const nextRelated = syncRelated ? [...relatedSet] : currentRelated;
|
|
99
|
+
const relatedChanged = syncRelated &&
|
|
100
|
+
(nextRelated.length !== currentRelated.length ||
|
|
101
|
+
nextRelated.some((slug, index) => slug !== currentRelated[index]));
|
|
102
|
+
const bodyChanged = convertedLinks > 0;
|
|
103
|
+
const changed = bodyChanged || relatedChanged;
|
|
104
|
+
if (changed && !options.dryRun) {
|
|
105
|
+
const frontmatter = {
|
|
106
|
+
...data,
|
|
107
|
+
related: nextRelated,
|
|
108
|
+
};
|
|
109
|
+
fs.writeFileSync(doc.path, matter.stringify(body, frontmatter), 'utf8');
|
|
110
|
+
index.refresh();
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
relativePath: doc.relativePath,
|
|
114
|
+
convertedLinks,
|
|
115
|
+
addedRelated,
|
|
116
|
+
changed,
|
|
117
|
+
skipped: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function syncAllDocLinks(index, options = {}) {
|
|
121
|
+
const docs = index.refresh().filter((doc) => doc.contentKind === 'markdown');
|
|
122
|
+
const files = docs.map((doc) => syncDocLinks(index, doc, docs, options));
|
|
123
|
+
return { files };
|
|
124
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import { type DocDomain, type DocStatus, type DocType } from '../index/types.js';
|
|
3
|
+
export interface UnpreparedFile {
|
|
4
|
+
relativePath: string;
|
|
5
|
+
path: string;
|
|
6
|
+
suggestedType: DocType;
|
|
7
|
+
suggestedSlug: string;
|
|
8
|
+
suggestedTitle: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PrepareOptions {
|
|
11
|
+
type?: DocType;
|
|
12
|
+
slug?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
status?: DocStatus;
|
|
15
|
+
domain?: DocDomain;
|
|
16
|
+
}
|
|
17
|
+
export interface PrepareResult {
|
|
18
|
+
path: string;
|
|
19
|
+
relativePath: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
type: DocType;
|
|
22
|
+
domain: DocDomain;
|
|
23
|
+
}
|
|
24
|
+
export declare function listUnpreparedFiles(index: DocIndex): UnpreparedFile[];
|
|
25
|
+
export declare function prepareDocFile(index: DocIndex, relativePath: string, options?: PrepareOptions): PrepareResult;
|
|
26
|
+
export interface PrepareAllOptions extends PrepareOptions {
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface PrepareAllResult {
|
|
30
|
+
prepared: PrepareResult[];
|
|
31
|
+
skipped: Array<{
|
|
32
|
+
relativePath: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function prepareAllDocs(index: DocIndex, options?: PrepareAllOptions): PrepareAllResult;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { inferTypeFromRelative, resolveDomain } from '../index/path-inference.js';
|
|
5
|
+
import { isValidSlug, slugFromRelativePath } from '../index/slug.js';
|
|
6
|
+
import { isDocType } from '../index/types.js';
|
|
7
|
+
function titleFromSlug(slug) {
|
|
8
|
+
return slug.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
export function listUnpreparedFiles(index) {
|
|
11
|
+
return index
|
|
12
|
+
.listUnprepared()
|
|
13
|
+
.filter((doc) => doc.contentKind === 'markdown')
|
|
14
|
+
.map((doc) => ({
|
|
15
|
+
relativePath: doc.relativePath,
|
|
16
|
+
path: doc.path,
|
|
17
|
+
suggestedType: doc.type,
|
|
18
|
+
suggestedSlug: doc.slug,
|
|
19
|
+
suggestedTitle: doc.title,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
export function prepareDocFile(index, relativePath, options = {}) {
|
|
23
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
24
|
+
if (!knowledgeRoot) {
|
|
25
|
+
throw new Error('no docs/ directory found');
|
|
26
|
+
}
|
|
27
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
28
|
+
const absolutePath = path.resolve(knowledgeRoot, normalized);
|
|
29
|
+
const rootWithSep = knowledgeRoot.endsWith(path.sep)
|
|
30
|
+
? knowledgeRoot
|
|
31
|
+
: `${knowledgeRoot}${path.sep}`;
|
|
32
|
+
if (!absolutePath.startsWith(rootWithSep) || !fs.existsSync(absolutePath)) {
|
|
33
|
+
throw new Error(`file not found: ${relativePath}`);
|
|
34
|
+
}
|
|
35
|
+
const raw = fs.readFileSync(absolutePath, 'utf8');
|
|
36
|
+
const parsed = matter(raw);
|
|
37
|
+
const data = parsed.data;
|
|
38
|
+
if (typeof data.type === 'string' && isDocType(data.type)) {
|
|
39
|
+
throw new Error(`file already prepared: ${relativePath}`);
|
|
40
|
+
}
|
|
41
|
+
const inferredType = options.type ?? inferTypeFromRelative(normalized);
|
|
42
|
+
const domain = options.domain ?? resolveDomain(normalized, data.domain);
|
|
43
|
+
const slug = options.slug ??
|
|
44
|
+
(typeof data.slug === 'string' && isValidSlug(data.slug)
|
|
45
|
+
? data.slug
|
|
46
|
+
: slugFromRelativePath(normalized));
|
|
47
|
+
const title = options.title ??
|
|
48
|
+
(typeof data.title === 'string' ? data.title : titleFromSlug(slug));
|
|
49
|
+
const status = options.status ?? 'accepted';
|
|
50
|
+
const frontmatter = {
|
|
51
|
+
type: inferredType,
|
|
52
|
+
slug,
|
|
53
|
+
status,
|
|
54
|
+
domain,
|
|
55
|
+
title,
|
|
56
|
+
tags: Array.isArray(data.tags)
|
|
57
|
+
? data.tags.filter((item) => typeof item === 'string')
|
|
58
|
+
: [],
|
|
59
|
+
related: Array.isArray(data.related)
|
|
60
|
+
? data.related.filter((item) => typeof item === 'string')
|
|
61
|
+
: [],
|
|
62
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
63
|
+
};
|
|
64
|
+
const markdown = matter.stringify(parsed.content, frontmatter);
|
|
65
|
+
fs.writeFileSync(absolutePath, markdown, 'utf8');
|
|
66
|
+
index.refresh();
|
|
67
|
+
return {
|
|
68
|
+
path: absolutePath,
|
|
69
|
+
relativePath: normalized,
|
|
70
|
+
slug,
|
|
71
|
+
type: inferredType,
|
|
72
|
+
domain,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function prepareAllDocs(index, options = {}) {
|
|
76
|
+
const unprepared = listUnpreparedFiles(index);
|
|
77
|
+
const prepared = [];
|
|
78
|
+
const skipped = [];
|
|
79
|
+
for (const file of unprepared) {
|
|
80
|
+
if (options.dryRun) {
|
|
81
|
+
prepared.push({
|
|
82
|
+
path: file.path,
|
|
83
|
+
relativePath: file.relativePath,
|
|
84
|
+
slug: file.suggestedSlug,
|
|
85
|
+
type: file.suggestedType,
|
|
86
|
+
domain: resolveDomain(file.relativePath, undefined),
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
prepared.push(prepareDocFile(index, file.relativePath, {
|
|
92
|
+
type: options.type,
|
|
93
|
+
slug: options.slug,
|
|
94
|
+
title: options.title,
|
|
95
|
+
status: options.status,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
skipped.push({
|
|
100
|
+
relativePath: file.relativePath,
|
|
101
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { prepared, skipped };
|
|
106
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import { type LinkIndexSnapshot } from '../index/link-index.js';
|
|
3
|
+
import type { DocType } from '../index/types.js';
|
|
4
|
+
export interface ExploreGraphInput {
|
|
5
|
+
slug: string;
|
|
6
|
+
depth?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ExploreGraphNode {
|
|
9
|
+
slug: string;
|
|
10
|
+
type: DocType;
|
|
11
|
+
title: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ExploreGraphEdge {
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExploreGraphResult {
|
|
18
|
+
nodes: ExploreGraphNode[];
|
|
19
|
+
edges: ExploreGraphEdge[];
|
|
20
|
+
maxDepthReached: number;
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
broken_links: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function exploreGraph(index: DocIndex, input: ExploreGraphInput): ExploreGraphResult;
|
|
25
|
+
export declare function buildLinkIndexForDocs(index: DocIndex): LinkIndexSnapshot;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isValidSlug } from '../index/slug.js';
|
|
2
|
+
import { buildLinkIndex, getOutboundSlugs, } from '../index/link-index.js';
|
|
3
|
+
import { buildDocsTree, collectParentOfEdges } from '../ui/fs-tree.js';
|
|
4
|
+
function buildSnapshot(index) {
|
|
5
|
+
const docs = index.refresh();
|
|
6
|
+
const tree = buildDocsTree(index);
|
|
7
|
+
return buildLinkIndex(docs, collectParentOfEdges(tree));
|
|
8
|
+
}
|
|
9
|
+
export function exploreGraph(index, input) {
|
|
10
|
+
const empty = {
|
|
11
|
+
nodes: [],
|
|
12
|
+
edges: [],
|
|
13
|
+
maxDepthReached: 0,
|
|
14
|
+
truncated: false,
|
|
15
|
+
broken_links: [],
|
|
16
|
+
};
|
|
17
|
+
if (!index.getKnowledgeRoot()) {
|
|
18
|
+
return empty;
|
|
19
|
+
}
|
|
20
|
+
if (!isValidSlug(input.slug)) {
|
|
21
|
+
return empty;
|
|
22
|
+
}
|
|
23
|
+
const rootDoc = index.getDocBySlug(input.slug);
|
|
24
|
+
if (!rootDoc) {
|
|
25
|
+
return empty;
|
|
26
|
+
}
|
|
27
|
+
let maxDepth = input.depth ?? 3;
|
|
28
|
+
if (maxDepth <= 0) {
|
|
29
|
+
maxDepth = 1;
|
|
30
|
+
}
|
|
31
|
+
const linkIndex = buildSnapshot(index);
|
|
32
|
+
const nodes = new Map();
|
|
33
|
+
const edges = [];
|
|
34
|
+
const brokenLinks = new Set(linkIndex.brokenTargets);
|
|
35
|
+
const visited = new Set();
|
|
36
|
+
let maxDepthReached = 0;
|
|
37
|
+
let truncated = false;
|
|
38
|
+
const queue = [{ slug: rootDoc.slug, depth: 0 }];
|
|
39
|
+
while (queue.length > 0) {
|
|
40
|
+
const current = queue.shift();
|
|
41
|
+
if (!current || visited.has(current.slug)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
visited.add(current.slug);
|
|
45
|
+
const doc = index.getDocBySlug(current.slug);
|
|
46
|
+
if (!doc) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
nodes.set(doc.slug, {
|
|
50
|
+
slug: doc.slug,
|
|
51
|
+
type: doc.type,
|
|
52
|
+
title: doc.title,
|
|
53
|
+
});
|
|
54
|
+
maxDepthReached = Math.max(maxDepthReached, current.depth);
|
|
55
|
+
const outbound = getOutboundSlugs(linkIndex, doc.slug);
|
|
56
|
+
if (current.depth + 1 >= maxDepth) {
|
|
57
|
+
if (outbound.length > 0) {
|
|
58
|
+
truncated = true;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const targetSlug of outbound) {
|
|
63
|
+
edges.push({ from: doc.slug, to: targetSlug });
|
|
64
|
+
const targetDoc = index.getDocBySlug(targetSlug);
|
|
65
|
+
if (!targetDoc) {
|
|
66
|
+
brokenLinks.add(targetSlug);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!visited.has(targetSlug)) {
|
|
70
|
+
queue.push({ slug: targetSlug, depth: current.depth + 1 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
nodes: [...nodes.values()],
|
|
76
|
+
edges,
|
|
77
|
+
maxDepthReached,
|
|
78
|
+
truncated,
|
|
79
|
+
broken_links: [...brokenLinks],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function buildLinkIndexForDocs(index) {
|
|
83
|
+
return buildSnapshot(index);
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import type { DocFrontmatter } from '../index/types.js';
|
|
3
|
+
export interface GetDocResult {
|
|
4
|
+
found: boolean;
|
|
5
|
+
slug?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
contentKind?: 'markdown' | 'yaml' | 'json';
|
|
8
|
+
frontmatter?: DocFrontmatter;
|
|
9
|
+
body?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function getDoc(index: DocIndex, slug: string): GetDocResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isValidSlug } from '../index/slug.js';
|
|
2
|
+
export function getDoc(index, slug) {
|
|
3
|
+
if (!index.getKnowledgeRoot()) {
|
|
4
|
+
return { found: false };
|
|
5
|
+
}
|
|
6
|
+
if (!isValidSlug(slug)) {
|
|
7
|
+
return { found: false };
|
|
8
|
+
}
|
|
9
|
+
const doc = index.getDocBySlug(slug);
|
|
10
|
+
if (!doc) {
|
|
11
|
+
return { found: false };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
found: true,
|
|
15
|
+
slug: doc.slug,
|
|
16
|
+
path: doc.path,
|
|
17
|
+
contentKind: doc.contentKind,
|
|
18
|
+
frontmatter: doc.frontmatter,
|
|
19
|
+
body: doc.body,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
export interface GetGlossaryTermResult {
|
|
3
|
+
found: boolean;
|
|
4
|
+
slug?: string;
|
|
5
|
+
definition?: string;
|
|
6
|
+
related?: string[];
|
|
7
|
+
suggestions?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function getGlossaryTerm(index: DocIndex, name: string): GetGlossaryTermResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { firstParagraph } from '../index/doc-index.js';
|
|
2
|
+
import { searchDocs } from './search-docs.js';
|
|
3
|
+
function substringMatch(name, slug, title) {
|
|
4
|
+
const needle = name.toLowerCase();
|
|
5
|
+
return slug.toLowerCase().includes(needle) || title.toLowerCase().includes(needle);
|
|
6
|
+
}
|
|
7
|
+
export function getGlossaryTerm(index, name) {
|
|
8
|
+
if (!index.getKnowledgeRoot()) {
|
|
9
|
+
return { found: false };
|
|
10
|
+
}
|
|
11
|
+
const trimmed = name.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return { found: false, suggestions: [] };
|
|
14
|
+
}
|
|
15
|
+
const glossaryDocs = index.getDocsByType('glossary-term');
|
|
16
|
+
const exact = glossaryDocs.find((doc) => doc.slug.toLowerCase() === trimmed.toLowerCase());
|
|
17
|
+
if (exact) {
|
|
18
|
+
return {
|
|
19
|
+
found: true,
|
|
20
|
+
slug: exact.slug,
|
|
21
|
+
definition: firstParagraph(exact.body),
|
|
22
|
+
related: exact.related,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const substringHits = glossaryDocs.filter((doc) => substringMatch(trimmed, doc.slug, doc.title));
|
|
26
|
+
if (substringHits.length === 1) {
|
|
27
|
+
const hit = substringHits[0];
|
|
28
|
+
return {
|
|
29
|
+
found: true,
|
|
30
|
+
slug: hit.slug,
|
|
31
|
+
definition: firstParagraph(hit.body),
|
|
32
|
+
related: hit.related,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const ranked = searchDocs(index, { query: trimmed, type: 'glossary-term' });
|
|
36
|
+
const suggestions = ranked.slice(0, 3).map((result) => result.slug);
|
|
37
|
+
return {
|
|
38
|
+
found: false,
|
|
39
|
+
suggestions,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import type { DocRecord, DocStatus, DocType, DocDomain } from '../index/types.js';
|
|
3
|
+
export interface ListDocsInput {
|
|
4
|
+
type?: DocType;
|
|
5
|
+
status?: DocStatus;
|
|
6
|
+
tag?: string;
|
|
7
|
+
domain?: DocDomain;
|
|
8
|
+
}
|
|
9
|
+
export interface ListDocsItem {
|
|
10
|
+
slug: string;
|
|
11
|
+
type: DocType;
|
|
12
|
+
domain: DocDomain;
|
|
13
|
+
title: string;
|
|
14
|
+
status: DocStatus;
|
|
15
|
+
relativePath: string;
|
|
16
|
+
contentKind: DocRecord['contentKind'];
|
|
17
|
+
}
|
|
18
|
+
export declare function listDocs(index: DocIndex, input?: ListDocsInput): ListDocsItem[];
|
|
19
|
+
export declare function getDocRecord(index: DocIndex, slug: string): DocRecord | null;
|