@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,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const NPM_PACKAGE_NAME = '@justyork/repo-mind';
|
|
5
|
+
const MCP_SERVER_NAME = 'repo-mind';
|
|
6
|
+
const CLAUDE_SNIPPET = '\n<!-- repo-mind -->\nProject knowledge lives in `docs/`. Use repo-mind MCP (`search_docs`, `get_doc`, `get_glossary_term`) — the same files humans edit in `repo-mind ui`.\n<!-- /repo-mind -->\n';
|
|
7
|
+
export function runSetup(options = {}) {
|
|
8
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
9
|
+
const setupCursor = options.cursor ?? !options.claude;
|
|
10
|
+
const setupClaude = options.claude ?? !options.cursor;
|
|
11
|
+
const actions = [];
|
|
12
|
+
if (setupCursor) {
|
|
13
|
+
const cursorPath = resolveCursorConfigPath(cwd);
|
|
14
|
+
mergeMcpConfig(cursorPath, options.force ?? false);
|
|
15
|
+
actions.push(`Cursor MCP config: ${cursorPath}`);
|
|
16
|
+
}
|
|
17
|
+
if (setupClaude) {
|
|
18
|
+
const claudePath = path.join(os.homedir(), '.claude.json');
|
|
19
|
+
mergeClaudeConfig(claudePath, options.force ?? false);
|
|
20
|
+
actions.push(`Claude Code MCP config: ${claudePath}`);
|
|
21
|
+
}
|
|
22
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
23
|
+
appendClaudeSnippet(claudeMdPath);
|
|
24
|
+
actions.push(`CLAUDE.md snippet: ${claudeMdPath}`);
|
|
25
|
+
for (const action of actions) {
|
|
26
|
+
console.log(action);
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
function resolveCursorConfigPath(cwd) {
|
|
31
|
+
const projectConfig = path.join(cwd, '.cursor', 'mcp.json');
|
|
32
|
+
if (fs.existsSync(path.dirname(projectConfig))) {
|
|
33
|
+
return projectConfig;
|
|
34
|
+
}
|
|
35
|
+
return path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
36
|
+
}
|
|
37
|
+
function mergeMcpConfig(configPath, force) {
|
|
38
|
+
const config = readJson(configPath);
|
|
39
|
+
config.mcpServers ??= {};
|
|
40
|
+
if (config.mcpServers[MCP_SERVER_NAME] && !force) {
|
|
41
|
+
console.warn(`warning: ${MCP_SERVER_NAME} already configured in ${configPath} (use --force to replace)`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
config.mcpServers[MCP_SERVER_NAME] = {
|
|
45
|
+
command: 'npx',
|
|
46
|
+
args: ['-y', NPM_PACKAGE_NAME, 'mcp'],
|
|
47
|
+
};
|
|
48
|
+
writeJson(configPath, config);
|
|
49
|
+
}
|
|
50
|
+
function mergeClaudeConfig(configPath, force) {
|
|
51
|
+
const config = readJson(configPath);
|
|
52
|
+
config.mcpServers ??= {};
|
|
53
|
+
if (config.mcpServers[MCP_SERVER_NAME] && !force) {
|
|
54
|
+
console.warn(`warning: ${MCP_SERVER_NAME} already configured in ${configPath} (use --force to replace)`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
config.mcpServers[MCP_SERVER_NAME] = {
|
|
58
|
+
command: 'npx',
|
|
59
|
+
args: ['-y', NPM_PACKAGE_NAME, 'mcp'],
|
|
60
|
+
};
|
|
61
|
+
writeJson(configPath, config);
|
|
62
|
+
}
|
|
63
|
+
function appendClaudeSnippet(claudeMdPath) {
|
|
64
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
65
|
+
const existing = fs.readFileSync(claudeMdPath, 'utf8');
|
|
66
|
+
if (existing.includes('<!-- repo-mind -->')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
fs.appendFileSync(claudeMdPath, CLAUDE_SNIPPET, 'utf8');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(claudeMdPath, `# CLAUDE.md${CLAUDE_SNIPPET}`, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
function readJson(filePath) {
|
|
75
|
+
if (!fs.existsSync(filePath)) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
79
|
+
}
|
|
80
|
+
function writeJson(filePath, value) {
|
|
81
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
82
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
export { CLAUDE_SNIPPET, MCP_SERVER_NAME, NPM_PACKAGE_NAME };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { DocIndex } from '../index/doc-index.js';
|
|
3
|
+
import { syncAllDocLinks } from '../prepare/auto-links.js';
|
|
4
|
+
function printSyncLinksResult(result, dryRun) {
|
|
5
|
+
const verb = dryRun ? 'Would update' : 'Updated';
|
|
6
|
+
let changedCount = 0;
|
|
7
|
+
for (const file of result.files) {
|
|
8
|
+
if (file.skipped) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (!file.changed) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
changedCount += 1;
|
|
15
|
+
const parts = [];
|
|
16
|
+
if (file.convertedLinks > 0) {
|
|
17
|
+
parts.push(`${file.convertedLinks} markdown link(s) → wikilink`);
|
|
18
|
+
}
|
|
19
|
+
if (file.addedRelated.length > 0) {
|
|
20
|
+
parts.push(`related +${file.addedRelated.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
console.log(`${verb}: ${file.relativePath}${parts.length > 0 ? ` (${parts.join('; ')})` : ''}`);
|
|
23
|
+
}
|
|
24
|
+
const unchanged = result.files.filter((file) => !file.skipped && !file.changed).length;
|
|
25
|
+
console.log(`${verb} ${changedCount} file(s), ${unchanged} unchanged`);
|
|
26
|
+
}
|
|
27
|
+
export function runSyncLinks(options = {}) {
|
|
28
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
29
|
+
const index = new DocIndex(cwd);
|
|
30
|
+
if (!index.getKnowledgeRoot()) {
|
|
31
|
+
console.error('no docs/ found — run `repo-mind init` or create a docs/ directory');
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const result = syncAllDocLinks(index, {
|
|
35
|
+
dryRun: options.dryRun,
|
|
36
|
+
convertMarkdownLinks: options.convertBody ?? true,
|
|
37
|
+
syncRelated: options.syncRelated ?? true,
|
|
38
|
+
});
|
|
39
|
+
printSyncLinksResult(result, options.dryRun === true);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DocIndex } from '../index/doc-index.js';
|
|
4
|
+
import { closeAllDocsEventStreams } from '../ui/api-handlers.js';
|
|
5
|
+
import { DocsWatcher } from '../ui/docs-watcher.js';
|
|
6
|
+
import { openDraftsDb } from '../ui/db/drafts-db.js';
|
|
7
|
+
import { destroyUiServerConnections, resolveUiStaticDir, startUiServer } from '../ui/server.js';
|
|
8
|
+
export async function runUi(options = {}) {
|
|
9
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
10
|
+
const port = options.port ?? 3847;
|
|
11
|
+
const index = new DocIndex(cwd);
|
|
12
|
+
if (!index.getKnowledgeRoot()) {
|
|
13
|
+
console.error('no docs/ found — run `repo-mind init` or create a docs/ directory');
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const staticDir = resolveUiStaticDir();
|
|
17
|
+
const indexHtml = path.join(staticDir, 'index.html');
|
|
18
|
+
if (!fs.existsSync(indexHtml)) {
|
|
19
|
+
console.warn('warning: ui/dist not found — run `npm run build:ui` in the repo-mind package');
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const draftsDb = openDraftsDb(index.getKnowledgeRoot());
|
|
23
|
+
const docsWatcher = new DocsWatcher(index);
|
|
24
|
+
docsWatcher.start(index.getKnowledgeRoot());
|
|
25
|
+
const server = await startUiServer({
|
|
26
|
+
host: '127.0.0.1',
|
|
27
|
+
port,
|
|
28
|
+
index,
|
|
29
|
+
staticDir,
|
|
30
|
+
draftsDb,
|
|
31
|
+
docsWatcher,
|
|
32
|
+
});
|
|
33
|
+
const docCount = index.refresh().length;
|
|
34
|
+
const draftCount = draftsDb.listActive().length;
|
|
35
|
+
console.log(`RepoMind UI at http://127.0.0.1:${port} (${docCount} docs, ${draftCount} drafts)`);
|
|
36
|
+
console.log('Press Ctrl+C to stop');
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
let shuttingDown = false;
|
|
39
|
+
const finishShutdown = () => {
|
|
40
|
+
closeAllDocsEventStreams();
|
|
41
|
+
destroyUiServerConnections(server);
|
|
42
|
+
try {
|
|
43
|
+
draftsDb.close();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore close errors during shutdown
|
|
47
|
+
}
|
|
48
|
+
server.close(() => {
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const shutdown = () => {
|
|
53
|
+
if (shuttingDown) {
|
|
54
|
+
console.log('\nForce exit.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
shuttingDown = true;
|
|
59
|
+
console.log('\nShutting down...');
|
|
60
|
+
const forceTimer = setTimeout(() => {
|
|
61
|
+
finishShutdown();
|
|
62
|
+
}, 2_000);
|
|
63
|
+
forceTimer.unref();
|
|
64
|
+
void docsWatcher.stop().finally(() => {
|
|
65
|
+
clearTimeout(forceTimer);
|
|
66
|
+
finishShutdown();
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
process.on('SIGINT', shutdown);
|
|
70
|
+
process.on('SIGTERM', shutdown);
|
|
71
|
+
});
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
if (message.includes('EADDRINUSE')) {
|
|
77
|
+
console.error(`port ${port} is in use — try --port <other>`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
console.error(message);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const IMAGE_EXTENSIONS: readonly [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
|
|
2
|
+
export declare function isImageFileName(name: string): boolean;
|
|
3
|
+
export declare function mimeTypeForImagePath(relativePath: string): string | null;
|
|
4
|
+
export declare function assetApiPath(relativePath: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
|
|
2
|
+
const IMAGE_MIME_TYPES = {
|
|
3
|
+
'.png': 'image/png',
|
|
4
|
+
'.jpg': 'image/jpeg',
|
|
5
|
+
'.jpeg': 'image/jpeg',
|
|
6
|
+
'.gif': 'image/gif',
|
|
7
|
+
'.webp': 'image/webp',
|
|
8
|
+
'.svg': 'image/svg+xml',
|
|
9
|
+
};
|
|
10
|
+
export function isImageFileName(name) {
|
|
11
|
+
const lower = name.toLowerCase();
|
|
12
|
+
return IMAGE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
13
|
+
}
|
|
14
|
+
export function mimeTypeForImagePath(relativePath) {
|
|
15
|
+
const lower = relativePath.toLowerCase();
|
|
16
|
+
for (const ext of IMAGE_EXTENSIONS) {
|
|
17
|
+
if (lower.endsWith(ext)) {
|
|
18
|
+
return IMAGE_MIME_TYPES[ext] ?? null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function assetApiPath(relativePath) {
|
|
24
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
25
|
+
return `/api/assets/${normalized.split('/').map(encodeURIComponent).join('/')}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DocRecord, type DocType } from './types.js';
|
|
2
|
+
/** Primary project knowledge directory — single source of truth for humans and agents. */
|
|
3
|
+
export declare const KNOWLEDGE_DIR = "docs";
|
|
4
|
+
export declare function discoverKnowledgeRoot(startDir: string): string | null;
|
|
5
|
+
export declare function listKnowledgeFiles(knowledgeRoot: string): string[];
|
|
6
|
+
export declare function listMarkdownFiles(knowledgeRoot: string): string[];
|
|
7
|
+
export declare class DocIndex {
|
|
8
|
+
private readonly startDir;
|
|
9
|
+
private knowledgeRoot;
|
|
10
|
+
private readonly cache;
|
|
11
|
+
constructor(startDir?: string);
|
|
12
|
+
getKnowledgeRoot(): string | null;
|
|
13
|
+
rediscover(): void;
|
|
14
|
+
refresh(): DocRecord[];
|
|
15
|
+
getDocs(): DocRecord[];
|
|
16
|
+
getDocBySlug(slug: string): DocRecord | null;
|
|
17
|
+
getDocByRelativePath(relativePath: string): DocRecord | null;
|
|
18
|
+
getDocsByType(type: DocType): DocRecord[];
|
|
19
|
+
listUnprepared(): DocRecord[];
|
|
20
|
+
}
|
|
21
|
+
export declare function firstParagraph(body: string): string;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { contentKindFromRelativePath } from './knowledge-file.js';
|
|
6
|
+
import { inferTypeFromRelative, resolveDomain } from './path-inference.js';
|
|
7
|
+
import { isValidSlug, slugFromRelativePath } from './slug.js';
|
|
8
|
+
import { isDocStatus, isDocType, } from './types.js';
|
|
9
|
+
/** Primary project knowledge directory — single source of truth for humans and agents. */
|
|
10
|
+
export const KNOWLEDGE_DIR = 'docs';
|
|
11
|
+
const GLOB_IGNORE = ['**/.repo-mind/**', '**/.worktrees/**'];
|
|
12
|
+
export function discoverKnowledgeRoot(startDir) {
|
|
13
|
+
let current = path.resolve(startDir);
|
|
14
|
+
while (true) {
|
|
15
|
+
const candidate = path.join(current, KNOWLEDGE_DIR);
|
|
16
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(current);
|
|
20
|
+
if (parent === current) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function normalizeStringArray(value) {
|
|
27
|
+
if (!Array.isArray(value)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return value.filter((item) => typeof item === 'string');
|
|
31
|
+
}
|
|
32
|
+
function inferSlug(relative, data, inferredType) {
|
|
33
|
+
if (typeof data.slug === 'string' && isValidSlug(data.slug)) {
|
|
34
|
+
return data.slug;
|
|
35
|
+
}
|
|
36
|
+
const basename = path.basename(relative).replace(/\.(md|ya?ml|json)$/i, '');
|
|
37
|
+
if (inferredType !== 'wiki-page' && isValidSlug(basename)) {
|
|
38
|
+
return basename;
|
|
39
|
+
}
|
|
40
|
+
return slugFromRelativePath(relative);
|
|
41
|
+
}
|
|
42
|
+
function titleFromBasename(basename) {
|
|
43
|
+
return basename
|
|
44
|
+
.replace(/[-_]/g, ' ')
|
|
45
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
46
|
+
}
|
|
47
|
+
function buildFrontmatter(relativeNorm, data, type, slug, status, title) {
|
|
48
|
+
return {
|
|
49
|
+
type,
|
|
50
|
+
slug,
|
|
51
|
+
status,
|
|
52
|
+
domain: resolveDomain(relativeNorm, data.domain),
|
|
53
|
+
title,
|
|
54
|
+
tags: normalizeStringArray(data.tags),
|
|
55
|
+
related: normalizeStringArray(data.related),
|
|
56
|
+
owner: typeof data.owner === 'string' ? data.owner : undefined,
|
|
57
|
+
updated: typeof data.updated === 'string' ? data.updated : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function parseStructuredFile(filePath, knowledgeRoot, contentKind) {
|
|
61
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
62
|
+
const relative = path.relative(knowledgeRoot, filePath).replace(/\\/g, '/');
|
|
63
|
+
const inferredType = inferTypeFromRelative(relative);
|
|
64
|
+
const slug = slugFromRelativePath(relative);
|
|
65
|
+
const basename = path.basename(relative).replace(/\.(ya?ml|json)$/i, '');
|
|
66
|
+
const title = titleFromBasename(basename);
|
|
67
|
+
const domain = resolveDomain(relative, undefined);
|
|
68
|
+
const frontmatter = {
|
|
69
|
+
type: inferredType,
|
|
70
|
+
slug,
|
|
71
|
+
status: 'accepted',
|
|
72
|
+
domain,
|
|
73
|
+
title,
|
|
74
|
+
tags: [],
|
|
75
|
+
related: [],
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
path: filePath,
|
|
79
|
+
relativePath: relative,
|
|
80
|
+
slug,
|
|
81
|
+
type: inferredType,
|
|
82
|
+
domain,
|
|
83
|
+
status: 'accepted',
|
|
84
|
+
title,
|
|
85
|
+
tags: [],
|
|
86
|
+
related: [],
|
|
87
|
+
body: raw,
|
|
88
|
+
frontmatter,
|
|
89
|
+
prepared: false,
|
|
90
|
+
contentKind,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parseKnowledgeFile(filePath, knowledgeRoot) {
|
|
94
|
+
const relative = path.relative(knowledgeRoot, filePath).replace(/\\/g, '/');
|
|
95
|
+
const contentKind = contentKindFromRelativePath(relative);
|
|
96
|
+
if (contentKind === 'markdown') {
|
|
97
|
+
return parseDoc(filePath, knowledgeRoot);
|
|
98
|
+
}
|
|
99
|
+
if (contentKind === 'yaml' || contentKind === 'json') {
|
|
100
|
+
return parseStructuredFile(filePath, knowledgeRoot, contentKind);
|
|
101
|
+
}
|
|
102
|
+
const _exhaustive = contentKind;
|
|
103
|
+
return _exhaustive;
|
|
104
|
+
}
|
|
105
|
+
function parseDoc(filePath, knowledgeRoot) {
|
|
106
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
const parsed = matter(raw);
|
|
108
|
+
const data = parsed.data;
|
|
109
|
+
const relative = path.relative(knowledgeRoot, filePath);
|
|
110
|
+
const relativeNorm = relative.replace(/\\/g, '/');
|
|
111
|
+
const inferredType = inferTypeFromRelative(relativeNorm);
|
|
112
|
+
const type = isDocType(data.type) ? data.type : inferredType;
|
|
113
|
+
const slug = inferSlug(relativeNorm, data, type);
|
|
114
|
+
const status = isDocStatus(data.status) ? data.status : 'accepted';
|
|
115
|
+
const title = typeof data.title === 'string'
|
|
116
|
+
? data.title
|
|
117
|
+
: slug.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
118
|
+
const prepared = typeof data.type === 'string' && isDocType(data.type);
|
|
119
|
+
const frontmatter = buildFrontmatter(relativeNorm, data, type, slug, status, title);
|
|
120
|
+
return {
|
|
121
|
+
path: filePath,
|
|
122
|
+
relativePath: relativeNorm,
|
|
123
|
+
slug,
|
|
124
|
+
type,
|
|
125
|
+
domain: frontmatter.domain ?? resolveDomain(relativeNorm, undefined),
|
|
126
|
+
status,
|
|
127
|
+
title,
|
|
128
|
+
tags: frontmatter.tags ?? [],
|
|
129
|
+
related: frontmatter.related ?? [],
|
|
130
|
+
body: parsed.content.trim(),
|
|
131
|
+
frontmatter,
|
|
132
|
+
prepared,
|
|
133
|
+
contentKind: 'markdown',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function listKnowledgeFiles(knowledgeRoot) {
|
|
137
|
+
const pattern = path.join(knowledgeRoot, '**/*.{md,yml,yaml,json}').replace(/\\/g, '/');
|
|
138
|
+
return fg.sync(pattern, {
|
|
139
|
+
absolute: true,
|
|
140
|
+
onlyFiles: true,
|
|
141
|
+
ignore: GLOB_IGNORE,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export function listMarkdownFiles(knowledgeRoot) {
|
|
145
|
+
const pattern = path.join(knowledgeRoot, '**/*.md').replace(/\\/g, '/');
|
|
146
|
+
return fg.sync(pattern, {
|
|
147
|
+
absolute: true,
|
|
148
|
+
onlyFiles: true,
|
|
149
|
+
ignore: GLOB_IGNORE,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export class DocIndex {
|
|
153
|
+
startDir;
|
|
154
|
+
knowledgeRoot;
|
|
155
|
+
cache = new Map();
|
|
156
|
+
constructor(startDir = process.cwd()) {
|
|
157
|
+
this.startDir = path.resolve(startDir);
|
|
158
|
+
this.knowledgeRoot = discoverKnowledgeRoot(this.startDir);
|
|
159
|
+
}
|
|
160
|
+
getKnowledgeRoot() {
|
|
161
|
+
return this.knowledgeRoot;
|
|
162
|
+
}
|
|
163
|
+
rediscover() {
|
|
164
|
+
this.knowledgeRoot = discoverKnowledgeRoot(this.startDir);
|
|
165
|
+
this.cache.clear();
|
|
166
|
+
}
|
|
167
|
+
refresh() {
|
|
168
|
+
if (!this.knowledgeRoot) {
|
|
169
|
+
this.cache.clear();
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const files = listKnowledgeFiles(this.knowledgeRoot);
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
for (const filePath of files) {
|
|
175
|
+
seen.add(filePath);
|
|
176
|
+
const stat = fs.statSync(filePath);
|
|
177
|
+
const cached = this.cache.get(filePath);
|
|
178
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const record = parseKnowledgeFile(filePath, this.knowledgeRoot);
|
|
182
|
+
if (record) {
|
|
183
|
+
this.cache.set(filePath, { mtimeMs: stat.mtimeMs, record });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.cache.delete(filePath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const filePath of [...this.cache.keys()]) {
|
|
190
|
+
if (!seen.has(filePath)) {
|
|
191
|
+
this.cache.delete(filePath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return this.getDocs();
|
|
195
|
+
}
|
|
196
|
+
getDocs() {
|
|
197
|
+
return [...this.cache.values()].map((entry) => entry.record);
|
|
198
|
+
}
|
|
199
|
+
getDocBySlug(slug) {
|
|
200
|
+
this.refresh();
|
|
201
|
+
const matches = this.getDocs().filter((doc) => doc.slug === slug);
|
|
202
|
+
if (matches.length !== 1) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return matches[0] ?? null;
|
|
206
|
+
}
|
|
207
|
+
getDocByRelativePath(relativePath) {
|
|
208
|
+
this.refresh();
|
|
209
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
210
|
+
const matches = this.getDocs().filter((doc) => doc.relativePath === normalized);
|
|
211
|
+
if (matches.length !== 1) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return matches[0] ?? null;
|
|
215
|
+
}
|
|
216
|
+
getDocsByType(type) {
|
|
217
|
+
this.refresh();
|
|
218
|
+
return this.getDocs().filter((doc) => doc.type === type);
|
|
219
|
+
}
|
|
220
|
+
listUnprepared() {
|
|
221
|
+
this.refresh();
|
|
222
|
+
return this.getDocs().filter((doc) => !doc.prepared);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export function firstParagraph(body) {
|
|
226
|
+
const paragraphs = body
|
|
227
|
+
.split(/\n\s*\n/)
|
|
228
|
+
.map((paragraph) => paragraph.trim())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
return paragraphs[0] ?? '';
|
|
231
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ContentKind = 'markdown' | 'yaml' | 'json';
|
|
2
|
+
export declare function isKnowledgeFileName(name: string): boolean;
|
|
3
|
+
export declare function contentKindFromRelativePath(relativePath: string): ContentKind;
|
|
4
|
+
export declare function stripKnowledgeExtension(relativePath: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const KNOWLEDGE_FILE_PATTERN = /\.(md|ya?ml|json)$/i;
|
|
2
|
+
export function isKnowledgeFileName(name) {
|
|
3
|
+
return KNOWLEDGE_FILE_PATTERN.test(name);
|
|
4
|
+
}
|
|
5
|
+
export function contentKindFromRelativePath(relativePath) {
|
|
6
|
+
const lower = relativePath.toLowerCase();
|
|
7
|
+
if (lower.endsWith('.json')) {
|
|
8
|
+
return 'json';
|
|
9
|
+
}
|
|
10
|
+
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
|
|
11
|
+
return 'yaml';
|
|
12
|
+
}
|
|
13
|
+
return 'markdown';
|
|
14
|
+
}
|
|
15
|
+
export function stripKnowledgeExtension(relativePath) {
|
|
16
|
+
return relativePath.replace(/\\/g, '/').replace(/\.(md|ya?ml|json)$/i, '');
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DocRecord } from './types.js';
|
|
2
|
+
export type LinkKind = 'related' | 'wikilink' | 'parent_of';
|
|
3
|
+
export interface LinkEdge {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
kind: LinkKind;
|
|
7
|
+
/** Unresolved wikilink target when `to` is not a known slug. */
|
|
8
|
+
rawTarget?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BacklinkEntry {
|
|
11
|
+
from: string;
|
|
12
|
+
kind: LinkKind;
|
|
13
|
+
}
|
|
14
|
+
export interface LinkIndexSnapshot {
|
|
15
|
+
edges: LinkEdge[];
|
|
16
|
+
backlinks: Map<string, BacklinkEntry[]>;
|
|
17
|
+
brokenTargets: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseWikilinkTargets(body: string): string[];
|
|
20
|
+
interface SlugLookups {
|
|
21
|
+
slugSet: Set<string>;
|
|
22
|
+
titleToSlug: Map<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveWikilinkTarget(raw: string, lookups: SlugLookups): {
|
|
25
|
+
slug: string | null;
|
|
26
|
+
broken: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function buildLinkIndex(docs: DocRecord[], extraEdges?: LinkEdge[]): LinkIndexSnapshot;
|
|
29
|
+
export declare function getOutboundSlugs(snapshot: LinkIndexSnapshot, fromSlug: string): string[];
|
|
30
|
+
export interface LinkHealthSummary {
|
|
31
|
+
orphanSlugs: string[];
|
|
32
|
+
brokenTargets: string[];
|
|
33
|
+
oneWayCount: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function computeLinkHealth(snapshot: LinkIndexSnapshot, docs: DocRecord[]): LinkHealthSummary;
|
|
36
|
+
export declare function getBacklinksForSlug(snapshot: LinkIndexSnapshot, slug: string, docsBySlug: Map<string, DocRecord>): Array<{
|
|
37
|
+
slug: string;
|
|
38
|
+
title: string;
|
|
39
|
+
kind: BacklinkEntry['kind'];
|
|
40
|
+
}>;
|
|
41
|
+
export {};
|