@codragraph/cli 2.1.1 → 2.1.4

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 (101) hide show
  1. package/README.md +12 -9
  2. package/dist/cli/ai-context.js +1 -1
  3. package/dist/cli/analyze.js +19 -2
  4. package/dist/cli/index.js +2 -1
  5. package/dist/cli/serve.d.ts +1 -0
  6. package/dist/cli/serve.js +3 -1
  7. package/dist/cli/setup.js +36 -19
  8. package/dist/cli/status.d.ts +13 -0
  9. package/dist/cli/status.js +99 -0
  10. package/dist/config/ignore-service.js +2 -0
  11. package/dist/core/graphstore/cgdb-row-source.js +3 -2
  12. package/dist/core/group/bridge-db.js +42 -10
  13. package/dist/core/run-analyze.d.ts +20 -0
  14. package/dist/core/run-analyze.js +201 -0
  15. package/dist/core/search/hybrid-search.js +11 -3
  16. package/dist/mcp/resources.js +2 -2
  17. package/dist/server/api.d.ts +14 -2
  18. package/dist/server/api.js +90 -7
  19. package/dist/server/mcp-http.d.ts +22 -0
  20. package/dist/server/mcp-http.js +21 -2
  21. package/dist/server/web-dashboard.d.ts +28 -0
  22. package/dist/server/web-dashboard.js +61 -0
  23. package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
  24. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
  25. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
  26. package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
  27. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
  28. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
  29. package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
  30. package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
  31. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
  32. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
  33. package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
  34. package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
  35. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
  36. package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
  37. package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
  38. package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
  39. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
  40. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
  41. package/dist/web/assets/context-builder-22jU3V56.js +16 -0
  42. package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
  43. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
  44. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
  45. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
  46. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
  47. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
  48. package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
  49. package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
  50. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
  51. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
  52. package/dist/web/assets/index-BgeqpYgd.js +1415 -0
  53. package/dist/web/assets/index-CT0GtFLZ.css +1 -0
  54. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
  55. package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
  56. package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
  57. package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
  58. package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
  59. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
  60. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
  61. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
  62. package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
  63. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
  64. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
  65. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
  66. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
  67. package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
  68. package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
  69. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
  70. package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
  71. package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
  72. package/dist/web/codragraph-logo-512.png +0 -0
  73. package/dist/web/codragraph-logo.png +0 -0
  74. package/dist/web/favicon.png +0 -0
  75. package/dist/web/index.html +36 -0
  76. package/hooks/claude/codragraph-hook.cjs +24 -9
  77. package/hooks/claude/pre-tool-use.sh +6 -1
  78. package/package.json +3 -1
  79. package/scripts/build.js +62 -4
  80. package/scripts/patch-tree-sitter-swift.cjs +0 -1
  81. package/skills/codragraph-cli.md +1 -1
  82. package/vendor/leiden/index.cjs +272 -285
  83. package/vendor/leiden/utils.cjs +264 -274
  84. package/dist/_shared/lbug/schema-constants.d.ts +0 -16
  85. package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
  86. package/dist/_shared/lbug/schema-constants.js +0 -67
  87. package/dist/_shared/lbug/schema-constants.js.map +0 -1
  88. package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
  89. package/dist/core/graphstore/lbug-row-source.js +0 -141
  90. package/dist/core/lbug/content-read.d.ts +0 -46
  91. package/dist/core/lbug/content-read.js +0 -64
  92. package/dist/core/lbug/csv-generator.d.ts +0 -29
  93. package/dist/core/lbug/csv-generator.js +0 -492
  94. package/dist/core/lbug/lbug-adapter.d.ts +0 -176
  95. package/dist/core/lbug/lbug-adapter.js +0 -1320
  96. package/dist/core/lbug/pool-adapter.d.ts +0 -93
  97. package/dist/core/lbug/pool-adapter.js +0 -550
  98. package/dist/core/lbug/schema.d.ts +0 -62
  99. package/dist/core/lbug/schema.js +0 -502
  100. package/dist/mcp/core/lbug-adapter.d.ts +0 -5
  101. package/dist/mcp/core/lbug-adapter.js +0 -5
@@ -10,18 +10,52 @@
10
10
  */
11
11
  import path from 'path';
12
12
  import fs from 'fs/promises';
13
+ import { execFileSync } from 'node:child_process';
13
14
  import * as fsSync from 'node:fs';
14
15
  import * as v8 from 'node:v8';
16
+ import { getLanguageFromFilename } from '../_shared/index.js';
15
17
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
16
18
  import { initCgdb, loadGraphToCgdb, getCgdbStats, executeQuery, executeWithReusedStatement, closeCgdb, loadCachedEmbeddings, } from './cgdb/cgdb-adapter.js';
17
19
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, INDEX_SCHEMA_VERSION, } from '../storage/repo-manager.js';
18
20
  import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName } from '../storage/git.js';
21
+ import { shouldIgnorePath } from '../config/ignore-service.js';
19
22
  import { recordAnalysisSnapshot } from './graphstore/index.js';
20
23
  import { generateAIContextFiles } from '../cli/ai-context.js';
21
24
  import { EMBEDDING_TABLE_NAME } from './cgdb/schema.js';
22
25
  import { STALE_HASH_SENTINEL } from './cgdb/schema.js';
23
26
  /** Threshold: auto-skip embeddings for repos with more nodes than this */
24
27
  const EMBEDDING_NODE_LIMIT = 50_000;
28
+ const GENERATED_AGENT_CONTEXT_PATHS = new Set(['agents.md', 'claude.md']);
29
+ const GENERATED_AGENT_CONTEXT_PREFIXES = [
30
+ '.claude/skills/generated/',
31
+ '.cursor/rules/codragraph-generated/',
32
+ ];
33
+ const IGNORE_CONTROL_FILES = new Set(['.gitignore', '.codragraphignore']);
34
+ const GRAPH_CONFIG_BASENAMES = new Set([
35
+ 'package.json',
36
+ 'tsconfig.json',
37
+ 'jsconfig.json',
38
+ 'go.mod',
39
+ 'cargo.toml',
40
+ 'pyproject.toml',
41
+ 'requirements.txt',
42
+ 'composer.json',
43
+ 'gemfile',
44
+ 'pom.xml',
45
+ 'build.gradle',
46
+ 'build.gradle.kts',
47
+ 'settings.gradle',
48
+ 'settings.gradle.kts',
49
+ 'pubspec.yaml',
50
+ 'pubspec.yml',
51
+ 'mix.exs',
52
+ 'rebar.config',
53
+ 'cmakelists.txt',
54
+ 'makefile',
55
+ 'dockerfile',
56
+ ]);
57
+ const GRAPH_CONFIG_PATTERNS = [/^tsconfig\..+\.json$/i, /^jsconfig\..+\.json$/i];
58
+ const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
25
59
  export const PHASE_LABELS = {
26
60
  extracting: 'Scanning files',
27
61
  structure: 'Building structure',
@@ -38,6 +72,119 @@ export const PHASE_LABELS = {
38
72
  embeddings: 'Generating embeddings',
39
73
  done: 'Done',
40
74
  };
75
+ const normalizeGitPath = (filePath) => filePath.replace(/\\/g, '/');
76
+ export const parseGitNameStatus = (raw) => {
77
+ const tokens = raw.split('\0').filter(Boolean);
78
+ const changes = [];
79
+ for (let i = 0; i < tokens.length;) {
80
+ const status = tokens[i++] ?? '';
81
+ const code = status[0]?.toUpperCase();
82
+ if (code === 'R' || code === 'C') {
83
+ const previousPath = tokens[i++];
84
+ const nextPath = tokens[i++];
85
+ if (previousPath && nextPath) {
86
+ changes.push({
87
+ status,
88
+ path: normalizeGitPath(nextPath),
89
+ previousPath: normalizeGitPath(previousPath),
90
+ });
91
+ }
92
+ continue;
93
+ }
94
+ const changedPath = tokens[i++];
95
+ if (status && changedPath) {
96
+ changes.push({ status, path: normalizeGitPath(changedPath) });
97
+ }
98
+ }
99
+ return changes;
100
+ };
101
+ export const listChangedPathsBetweenCommits = (repoPath, fromRef, toRef) => {
102
+ if (!fromRef || !toRef || fromRef === toRef)
103
+ return [];
104
+ try {
105
+ const stdout = execFileSync('git', ['diff', '--name-status', '-z', `${fromRef}..${toRef}`], {
106
+ cwd: repoPath,
107
+ encoding: 'utf8',
108
+ maxBuffer: 20 * 1024 * 1024,
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ });
111
+ return parseGitNameStatus(stdout);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ };
117
+ export const isGeneratedAgentContextPath = (filePath) => {
118
+ const normalized = normalizeGitPath(filePath).toLowerCase();
119
+ const basename = path.posix.basename(normalized);
120
+ return (GENERATED_AGENT_CONTEXT_PATHS.has(basename) ||
121
+ GENERATED_AGENT_CONTEXT_PREFIXES.some((prefix) => normalized.startsWith(prefix)));
122
+ };
123
+ export const isGraphContentPath = (filePath) => {
124
+ const normalized = normalizeGitPath(filePath);
125
+ const basename = path.posix.basename(normalized);
126
+ const lowerBasename = basename.toLowerCase();
127
+ if (isGeneratedAgentContextPath(normalized))
128
+ return false;
129
+ if (IGNORE_CONTROL_FILES.has(lowerBasename))
130
+ return true;
131
+ if (shouldIgnorePath(normalized))
132
+ return false;
133
+ if (getLanguageFromFilename(normalized) !== null)
134
+ return true;
135
+ const ext = path.posix.extname(lowerBasename);
136
+ if (MARKDOWN_EXTENSIONS.has(ext))
137
+ return true;
138
+ if (GRAPH_CONFIG_BASENAMES.has(lowerBasename))
139
+ return true;
140
+ return GRAPH_CONFIG_PATTERNS.some((pattern) => pattern.test(basename));
141
+ };
142
+ export const changedPathAffectsGraph = (change) => {
143
+ const statusCode = change.status[0]?.toUpperCase();
144
+ const paths = [change.path, change.previousPath].filter((p) => Boolean(p));
145
+ if (paths.some(isGraphContentPath))
146
+ return true;
147
+ // Add/delete/rename/copy can change File/Folder structure even when content
148
+ // is not parsed. Ignored or generated-agent paths are outside the index.
149
+ if (statusCode === 'A' || statusCode === 'D' || statusCode === 'R' || statusCode === 'C') {
150
+ return paths.some((p) => !isGeneratedAgentContextPath(p) && !shouldIgnorePath(p));
151
+ }
152
+ // Modified non-code/non-doc files keep the same path and are not read by the
153
+ // graph pipeline, so the existing graph can be reused.
154
+ if (statusCode === 'M' || statusCode === 'T')
155
+ return false;
156
+ // Unknown git status: rebuild rather than risk stale graph state.
157
+ return true;
158
+ };
159
+ export const getGraphRelevantChangedPaths = (changes) => changes.filter(changedPathAffectsGraph);
160
+ export const getAnalyzeConfigRebuildReason = (existingMeta, options) => {
161
+ const existingCompress = existingMeta.compress ?? 'none';
162
+ if (options.compress && options.compress !== existingCompress) {
163
+ return `requested compression changed from ${existingCompress} to ${options.compress}`;
164
+ }
165
+ if (options.embeddings && (existingMeta.stats?.embeddings ?? 0) === 0) {
166
+ return 'embeddings were requested but the existing index has no vectors';
167
+ }
168
+ return null;
169
+ };
170
+ const formatChangeForLog = (change) => change.previousPath ? `${change.previousPath} -> ${change.path}` : change.path;
171
+ const buildReusedMeta = (existingMeta, repoPath, currentCommit) => ({
172
+ ...existingMeta,
173
+ repoPath,
174
+ lastCommit: currentCommit,
175
+ indexedAt: new Date().toISOString(),
176
+ schemaVersion: INDEX_SCHEMA_VERSION,
177
+ remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : existingMeta.remoteUrl,
178
+ });
179
+ const pathExists = async (targetPath) => {
180
+ try {
181
+ await fs.stat(targetPath);
182
+ return true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ };
41
188
  // ---------------------------------------------------------------------------
42
189
  // Main orchestrator
43
190
  // ---------------------------------------------------------------------------
@@ -135,9 +282,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
135
282
  // end-to-end yet, so the supported migration path is re-analyze via a fresh
136
283
  // CREATE NODE TABLE.
137
284
  const schemaUpToDate = !!existingMeta && (existingMeta.schemaVersion ?? 0) >= INDEX_SCHEMA_VERSION;
285
+ const existingCgdbPresent = existingMeta ? await pathExists(cgdbPath) : false;
286
+ const storageRebuildReason = existingMeta && schemaUpToDate && !existingCgdbPresent
287
+ ? 'graph database files are missing'
288
+ : null;
289
+ const configRebuildReason = storageRebuildReason ??
290
+ (existingMeta && schemaUpToDate && !options.force
291
+ ? getAnalyzeConfigRebuildReason(existingMeta, options)
292
+ : null);
138
293
  if (existingMeta &&
139
294
  schemaUpToDate &&
140
295
  !options.force &&
296
+ !configRebuildReason &&
141
297
  existingMeta.lastCommit === currentCommit) {
142
298
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
143
299
  if (currentCommit !== '') {
@@ -149,6 +305,51 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
149
305
  };
150
306
  }
151
307
  }
308
+ if (existingMeta && schemaUpToDate && !options.force && configRebuildReason) {
309
+ log(`Re-analyzing: ${configRebuildReason}.`);
310
+ }
311
+ if (existingMeta &&
312
+ schemaUpToDate &&
313
+ !options.force &&
314
+ !configRebuildReason &&
315
+ currentCommit !== '' &&
316
+ existingMeta.lastCommit !== currentCommit) {
317
+ const changedPaths = listChangedPathsBetweenCommits(repoPath, existingMeta.lastCommit, currentCommit);
318
+ if (changedPaths) {
319
+ const graphRelevantChanges = getGraphRelevantChangedPaths(changedPaths);
320
+ if (graphRelevantChanges.length === 0) {
321
+ const reusedMeta = buildReusedMeta(existingMeta, repoPath, currentCommit);
322
+ await saveMeta(storagePath, reusedMeta);
323
+ const projectName = await registerRepo(repoPath, reusedMeta, {
324
+ name: options.registryName,
325
+ allowDuplicateName: options.allowDuplicateName,
326
+ });
327
+ if (hasGitDir(repoPath)) {
328
+ await addToGitignore(repoPath);
329
+ }
330
+ const reuseReason = `Smart analyze reused the existing graph; ${changedPaths.length} changed ` +
331
+ `file(s) did not affect indexed code, docs, config, or file structure.`;
332
+ log(reuseReason);
333
+ progress('done', 100, 'Existing graph reused');
334
+ return {
335
+ repoName: projectName,
336
+ repoPath,
337
+ stats: reusedMeta.stats ?? {},
338
+ alreadyUpToDate: true,
339
+ reusedExistingIndex: true,
340
+ reuseReason,
341
+ };
342
+ }
343
+ const preview = graphRelevantChanges.slice(0, 5).map(formatChangeForLog).join(', ');
344
+ const suffix = graphRelevantChanges.length > 5 ? ', ...' : '';
345
+ log(`Smart analyze: ${graphRelevantChanges.length} indexed change(s) require rebuild` +
346
+ (preview ? ` (${preview}${suffix})` : '') +
347
+ '.');
348
+ }
349
+ else {
350
+ log('Smart analyze: could not inspect git diff; rebuilding.');
351
+ }
352
+ }
152
353
  if (existingMeta && !schemaUpToDate) {
153
354
  log(`Index schema version ${existingMeta.schemaVersion ?? '<missing>'} is older than ` +
154
355
  `${INDEX_SCHEMA_VERSION} (FeatureCluster context-pack schema). ` +
@@ -111,8 +111,16 @@ export const formatHybridResults = (results) => {
111
111
  * The semanticSearch function is injected to keep this module environment-agnostic.
112
112
  */
113
113
  export const hybridSearch = async (query, limit, executeQuery, semanticSearch) => {
114
- // Use LadybugDB FTS for always-fresh BM25 results
115
- const bm25Results = await searchFTSFromCgdb(query, limit);
116
- const semanticResults = await semanticSearch(executeQuery, query, limit);
114
+ const bm25Promise = searchFTSFromCgdb(query, limit);
115
+ // Start semantic work immediately, but gate its DB calls behind BM25.
116
+ // semanticSearch performs embedding before it calls executeQuery, so this
117
+ // overlaps CPU/model work with BM25 while avoiding concurrent queries on the
118
+ // singleton LadybugDB connection used by CLI/HTTP paths.
119
+ const executeAfterBm25 = async (cypher) => {
120
+ await bm25Promise;
121
+ return executeQuery(cypher);
122
+ };
123
+ const semanticPromise = semanticSearch(executeAfterBm25, query, limit);
124
+ const [bm25Results, semanticResults] = await Promise.all([bm25Promise, semanticPromise]);
117
125
  return mergeWithRRF(bm25Results, semanticResults, limit);
118
126
  };
@@ -685,8 +685,8 @@ async function getSetupResource(backend) {
685
685
  '',
686
686
  '## Cross-platform commands',
687
687
  '',
688
- '- Use `npx @codragraph/cli ...` or `codragraph ...` in Windows PowerShell, macOS bash/zsh, and Linux shells.',
689
- '- Prefer `npm --prefix <package> <script>` from repo root for package checks instead of shell-specific `cd dir && ...` chains.',
688
+ '- Use `npx @codragraph/cli ...`, `bunx @codragraph/cli ...`, or `codragraph ...` in Windows PowerShell, macOS bash/zsh, and Linux shells.',
689
+ '- Prefer `npm --prefix <package> <script>` or `bun run --filter <workspace> <script>` from repo root for package checks instead of shell-specific `cd dir && ...` chains.',
690
690
  ];
691
691
  sections.push(lines.join('\n'));
692
692
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import express from 'express';
11
11
  import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
12
+ import { type WebDashboardMode } from './web-dashboard.js';
12
13
  /**
13
14
  * Determine whether an HTTP Origin header value is allowed by CORS policy.
14
15
  *
@@ -20,13 +21,16 @@ import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
20
21
  * 10.0.0.0/8 → 10.x.x.x
21
22
  * 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
22
23
  * 192.168.0.0/16 → 192.168.x.x
23
- * - https://codragraph.vercel.app — the deployed CodraGraph web UI
24
+ * - Hosted CodraGraph web UI — defaults to https://codragraph.vercel.app
24
25
  *
25
26
  * @param origin - The value of the HTTP `Origin` request header, or `undefined`
26
27
  * when the header is absent (non-browser request).
27
28
  * @returns `true` if the origin is allowed, `false` otherwise.
28
29
  */
29
30
  export declare const isAllowedOrigin: (origin: string | undefined) => boolean;
31
+ export interface CreateServerOptions {
32
+ web?: WebDashboardMode;
33
+ }
30
34
  type GraphStreamRecord = {
31
35
  type: 'node';
32
36
  data: GraphNode;
@@ -41,7 +45,15 @@ export declare class ClientDisconnectedError extends Error {
41
45
  constructor();
42
46
  }
43
47
  export declare const isIgnorableGraphQueryError: (err: unknown) => boolean;
48
+ export interface GraphStoreErrorResponse {
49
+ error: string;
50
+ code: 'GRAPHSTORE_CORRUPT';
51
+ operation: string;
52
+ recovery: string[];
53
+ }
54
+ export declare const isGraphStoreCorruptionError: (err: unknown) => boolean;
55
+ export declare const getGraphStoreErrorResponse: (err: unknown, operation: string) => GraphStoreErrorResponse | null;
44
56
  export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
45
57
  export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
46
- export declare const createServer: (port: number, host?: string) => Promise<void>;
58
+ export declare const createServer: (port: number, host?: string, options?: CreateServerOptions) => Promise<void>;
47
59
  export {};
@@ -22,7 +22,8 @@ import { hybridSearch } from '../core/search/hybrid-search.js';
22
22
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
23
23
  // at server startup — crashes on unsupported Node ABI versions (#89)
24
24
  import { LocalBackend } from '../mcp/local/local-backend.js';
25
- import { mountMCPEndpoints } from './mcp-http.js';
25
+ import { getMcpHttpRouteGuidance, mountMCPEndpoints } from './mcp-http.js';
26
+ import { HOSTED_WEB_APP_URL, getWebDashboardInfo, mountWebDashboard, } from './web-dashboard.js';
26
27
  import { fork } from 'child_process';
27
28
  import { fileURLToPath, pathToFileURL } from 'url';
28
29
  import { JobManager } from './analyze-job.js';
@@ -40,7 +41,7 @@ const pkg = _require('../../package.json');
40
41
  * 10.0.0.0/8 → 10.x.x.x
41
42
  * 172.16.0.0/12 → 172.16.x.x – 172.31.x.x
42
43
  * 192.168.0.0/16 → 192.168.x.x
43
- * - https://codragraph.vercel.app — the deployed CodraGraph web UI
44
+ * - Hosted CodraGraph web UI — defaults to https://codragraph.vercel.app
44
45
  *
45
46
  * @param origin - The value of the HTTP `Origin` request header, or `undefined`
46
47
  * when the header is absent (non-browser request).
@@ -57,7 +58,7 @@ export const isAllowedOrigin = (origin) => {
57
58
  origin === 'http://127.0.0.1' ||
58
59
  origin.startsWith('http://[::1]:') ||
59
60
  origin === 'http://[::1]' ||
60
- origin === 'https://codragraph.vercel.app') {
61
+ origin === HOSTED_WEB_APP_URL) {
61
62
  return true;
62
63
  }
63
64
  // RFC 1918 private network ranges — allow any port on these hosts.
@@ -104,6 +105,30 @@ export const isIgnorableGraphQueryError = (err) => {
104
105
  message.includes('not found') ||
105
106
  message.includes('No table named'));
106
107
  };
108
+ export const isGraphStoreCorruptionError = (err) => {
109
+ const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
110
+ return (message.includes('wal checksum') ||
111
+ (message.includes('wal') && message.includes('corrupt')) ||
112
+ (message.includes('checksum') && message.includes('corrupt')) ||
113
+ message.includes('database disk image is malformed') ||
114
+ message.includes('database image is malformed'));
115
+ };
116
+ export const getGraphStoreErrorResponse = (err, operation) => {
117
+ if (!isGraphStoreCorruptionError(err))
118
+ return null;
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ return {
121
+ error: message || 'Graph store is corrupted',
122
+ code: 'GRAPHSTORE_CORRUPT',
123
+ operation,
124
+ recovery: [
125
+ 'Stop overlapping codragraph serve, mcp, analyze, and embedding jobs for this repo.',
126
+ 'Retry: npx @codragraph/cli analyze --force',
127
+ 'If this repo had embeddings, preserve them with: npx @codragraph/cli analyze --force --embeddings',
128
+ 'Only after approval, use: npx @codragraph/cli clean --force',
129
+ ],
130
+ };
131
+ };
107
132
  const ensureStreamIsWritable = (res, signal) => {
108
133
  if (signal?.aborted || res.destroyed || res.writableEnded) {
109
134
  throw new ClientDisconnectedError();
@@ -355,6 +380,8 @@ const mountSSEProgress = (app, routePath, jm) => {
355
380
  });
356
381
  };
357
382
  const statusFromError = (err) => {
383
+ if (isGraphStoreCorruptionError(err))
384
+ return 503;
358
385
  const msg = String(err?.message ?? '');
359
386
  if (msg.includes('No indexed repositories') || msg.includes('not found'))
360
387
  return 404;
@@ -371,9 +398,14 @@ const requestedRepo = (req) => {
371
398
  }
372
399
  return undefined;
373
400
  };
374
- export const createServer = async (port, host = '127.0.0.1') => {
401
+ export const createServer = async (port, host = '127.0.0.1', options = {}) => {
375
402
  const app = express();
376
403
  app.disable('x-powered-by');
404
+ let webDashboard = {
405
+ mode: options.web ?? 'local',
406
+ served: false,
407
+ hostedUrl: HOSTED_WEB_APP_URL,
408
+ };
377
409
  // CORS: allow localhost, private/LAN networks, and the deployed site.
378
410
  // Non-browser requests (curl, server-to-server) have no origin and are allowed.
379
411
  // Disallowed origins get the response without Access-Control-Allow-Origin,
@@ -520,7 +552,15 @@ export const createServer = async (port, host = '127.0.0.1') => {
520
552
  else {
521
553
  launchContext = 'global';
522
554
  }
523
- res.json({ version: pkg.version, launchContext, nodeVersion: process.version });
555
+ const displayHost = host === '::' || host === '0.0.0.0' ? 'localhost' : host;
556
+ const apiBaseUrl = `http://${displayHost}:${port}`;
557
+ res.json({
558
+ version: pkg.version,
559
+ launchContext,
560
+ nodeVersion: process.version,
561
+ mcp: getMcpHttpRouteGuidance(),
562
+ web: getWebDashboardInfo(webDashboard, apiBaseUrl),
563
+ });
524
564
  });
525
565
  // List all registered repos
526
566
  app.get('/api/repos', async (_req, res) => {
@@ -995,10 +1035,13 @@ export const createServer = async (port, host = '127.0.0.1') => {
995
1035
  if (err instanceof ClientDisconnectedError) {
996
1036
  return;
997
1037
  }
1038
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.graph');
998
1039
  const message = err.message || 'Failed to build graph';
999
1040
  if (res.headersSent) {
1000
1041
  try {
1001
- res.write(JSON.stringify({ type: 'error', error: message }) + '\n');
1042
+ res.write(JSON.stringify(graphStoreError
1043
+ ? { type: 'error', ...graphStoreError }
1044
+ : { type: 'error', error: message }) + '\n');
1002
1045
  }
1003
1046
  catch {
1004
1047
  // Best-effort only after streaming has started.
@@ -1006,6 +1049,10 @@ export const createServer = async (port, host = '127.0.0.1') => {
1006
1049
  res.end();
1007
1050
  return;
1008
1051
  }
1052
+ if (graphStoreError) {
1053
+ res.status(503).json(graphStoreError);
1054
+ return;
1055
+ }
1009
1056
  res.status(500).json({ error: message });
1010
1057
  }
1011
1058
  });
@@ -1031,6 +1078,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1031
1078
  res.json({ result });
1032
1079
  }
1033
1080
  catch (err) {
1081
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.query');
1082
+ if (graphStoreError) {
1083
+ res.status(503).json(graphStoreError);
1084
+ return;
1085
+ }
1034
1086
  res.status(500).json({ error: err.message || 'Query failed' });
1035
1087
  }
1036
1088
  });
@@ -1050,6 +1102,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1050
1102
  res.json(result);
1051
1103
  }
1052
1104
  catch (err) {
1105
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.context');
1106
+ if (graphStoreError) {
1107
+ res.status(503).json(graphStoreError);
1108
+ return;
1109
+ }
1053
1110
  res.status(statusFromError(err)).json({ error: err.message || 'Context query failed' });
1054
1111
  }
1055
1112
  });
@@ -1070,6 +1127,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1070
1127
  res.json(result);
1071
1128
  }
1072
1129
  catch (err) {
1130
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.impact');
1131
+ if (graphStoreError) {
1132
+ res.status(503).json(graphStoreError);
1133
+ return;
1134
+ }
1073
1135
  res.status(statusFromError(err)).json({ error: err.message || 'Impact query failed' });
1074
1136
  }
1075
1137
  });
@@ -1197,6 +1259,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1197
1259
  res.json({ results });
1198
1260
  }
1199
1261
  catch (err) {
1262
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.search');
1263
+ if (graphStoreError) {
1264
+ res.status(503).json(graphStoreError);
1265
+ return;
1266
+ }
1200
1267
  res.status(500).json({ error: err.message || 'Search failed' });
1201
1268
  }
1202
1269
  });
@@ -1813,6 +1880,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1813
1880
  embedJobManager.cancelJob(req.params.jobId, 'Cancelled by user');
1814
1881
  res.json({ id: job.id, status: 'failed', error: 'Cancelled by user' });
1815
1882
  });
1883
+ webDashboard = mountWebDashboard(app, { mode: options.web ?? 'local' });
1816
1884
  // Global error handler — catch anything the route handlers miss
1817
1885
  app.use((err, _req, res, _next) => {
1818
1886
  console.error('Unhandled error:', err);
@@ -1823,7 +1891,22 @@ export const createServer = async (port, host = '127.0.0.1') => {
1823
1891
  await new Promise((resolve, reject) => {
1824
1892
  const server = app.listen(port, host, () => {
1825
1893
  const displayHost = host === '::' || host === '0.0.0.0' ? 'localhost' : host;
1826
- console.log(`CodraGraph server running on http://${displayHost}:${port}`);
1894
+ const localUrl = `http://${displayHost}:${port}`;
1895
+ console.log(`CodraGraph server running on ${localUrl}`);
1896
+ if (webDashboard.served) {
1897
+ console.log(`Web dashboard: ${localUrl}`);
1898
+ }
1899
+ else if (webDashboard.mode === 'hosted') {
1900
+ console.log(`Hosted dashboard: ${webDashboard.hostedUrl}`);
1901
+ console.log(`Connect it to local API: ${localUrl}`);
1902
+ }
1903
+ else if (webDashboard.mode === 'off') {
1904
+ console.log('Web dashboard disabled (--web off).');
1905
+ }
1906
+ else {
1907
+ console.warn(`Web dashboard not bundled: ${webDashboard.reason}`);
1908
+ console.warn(`Hosted dashboard: ${webDashboard.hostedUrl}`);
1909
+ }
1827
1910
  resolve();
1828
1911
  });
1829
1912
  server.on('error', (err) => reject(err));
@@ -10,4 +10,26 @@
10
10
  */
11
11
  import type { Express } from 'express';
12
12
  import type { LocalBackend } from '../mcp/local/local-backend.js';
13
+ export declare const MCP_HTTP_ENDPOINT = "/api/mcp";
14
+ export declare const UNSUPPORTED_MCP_REST_EXAMPLE = "/api/mcp/tools/list";
15
+ export interface McpHttpRouteGuidance {
16
+ endpoint: string;
17
+ transport: 'streamable-http';
18
+ note: string;
19
+ unsupportedRestExample: string;
20
+ powershellHealthCheck: string;
21
+ clientInstruction: string;
22
+ }
23
+ export declare const getMcpHttpRouteGuidance: () => McpHttpRouteGuidance;
24
+ export declare const getUnsupportedMcpRestRouteResponse: (path: string) => {
25
+ endpoint: string;
26
+ transport: "streamable-http";
27
+ note: string;
28
+ unsupportedRestExample: string;
29
+ powershellHealthCheck: string;
30
+ clientInstruction: string;
31
+ error: string;
32
+ code: string;
33
+ unsupportedRoute: string;
34
+ };
13
35
  export declare function mountMCPEndpoints(app: Express, backend: LocalBackend): () => Promise<void>;
@@ -11,6 +11,22 @@
11
11
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
12
  import { createMCPServer } from '../mcp/server.js';
13
13
  import { randomUUID } from 'crypto';
14
+ export const MCP_HTTP_ENDPOINT = '/api/mcp';
15
+ export const UNSUPPORTED_MCP_REST_EXAMPLE = '/api/mcp/tools/list';
16
+ export const getMcpHttpRouteGuidance = () => ({
17
+ endpoint: MCP_HTTP_ENDPOINT,
18
+ transport: 'streamable-http',
19
+ note: 'HTTP MCP is a protocol endpoint, not a REST tools namespace.',
20
+ unsupportedRestExample: UNSUPPORTED_MCP_REST_EXAMPLE,
21
+ powershellHealthCheck: "Invoke-RestMethod -Uri 'http://127.0.0.1:4747/api/info' -TimeoutSec 10",
22
+ clientInstruction: 'Point an MCP client at http://127.0.0.1:4747/api/mcp using StreamableHTTP.',
23
+ });
24
+ export const getUnsupportedMcpRestRouteResponse = (path) => ({
25
+ error: 'Unsupported MCP HTTP REST route',
26
+ code: 'MCP_HTTP_REST_ROUTE_UNSUPPORTED',
27
+ unsupportedRoute: path,
28
+ ...getMcpHttpRouteGuidance(),
29
+ });
14
30
  /** Idle sessions are evicted after 30 minutes */
15
31
  const SESSION_TTL_MS = 30 * 60 * 1000;
16
32
  /** Cleanup sweep runs every 5 minutes */
@@ -72,7 +88,7 @@ export function mountMCPEndpoints(app, backend) {
72
88
  });
73
89
  }
74
90
  };
75
- app.all('/api/mcp', (req, res) => {
91
+ app.all(MCP_HTTP_ENDPOINT, (req, res) => {
76
92
  void handleMcpRequest(req, res).catch((err) => {
77
93
  console.error('MCP HTTP request failed:', err);
78
94
  if (res.headersSent)
@@ -84,6 +100,9 @@ export function mountMCPEndpoints(app, backend) {
84
100
  });
85
101
  });
86
102
  });
103
+ app.all(/^\/api\/mcp\/.+/, (req, res) => {
104
+ res.status(200).json(getUnsupportedMcpRestRouteResponse(req.path));
105
+ });
87
106
  const cleanup = async () => {
88
107
  clearInterval(cleanupTimer);
89
108
  const closers = [...sessions.values()].map(async (session) => {
@@ -95,6 +114,6 @@ export function mountMCPEndpoints(app, backend) {
95
114
  sessions.clear();
96
115
  await Promise.allSettled(closers);
97
116
  };
98
- console.log('MCP HTTP endpoints mounted at /api/mcp');
117
+ console.log(`MCP HTTP endpoint mounted at ${MCP_HTTP_ENDPOINT}`);
99
118
  return cleanup;
100
119
  }
@@ -0,0 +1,28 @@
1
+ import { type Express } from 'express';
2
+ export declare const HOSTED_WEB_APP_URL: string;
3
+ export type WebDashboardMode = 'local' | 'hosted' | 'off';
4
+ export interface WebDashboardMountOptions {
5
+ mode?: WebDashboardMode;
6
+ webAppPath?: string;
7
+ }
8
+ export interface WebDashboardMount {
9
+ mode: WebDashboardMode;
10
+ served: boolean;
11
+ hostedUrl: string;
12
+ localPath?: string;
13
+ reason?: string;
14
+ }
15
+ export interface WebDashboardInfo {
16
+ mode: WebDashboardMode;
17
+ served: boolean;
18
+ localUrl: string | null;
19
+ hostedUrl: string;
20
+ apiBaseUrl: string;
21
+ reason?: string;
22
+ }
23
+ export declare const normalizeWebDashboardMode: (raw: string | undefined) => WebDashboardMode;
24
+ export declare const getBundledWebAppCandidates: () => string[];
25
+ export declare const hasWebDashboardIndex: (candidate: string) => boolean;
26
+ export declare const resolveBundledWebAppPath: (candidates?: readonly string[]) => string | null;
27
+ export declare const mountWebDashboard: (app: Express, options?: WebDashboardMountOptions) => WebDashboardMount;
28
+ export declare const getWebDashboardInfo: (mount: WebDashboardMount, apiBaseUrl: string) => WebDashboardInfo;