@codragraph/cli 2.1.0 → 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 (156) hide show
  1. package/README.md +62 -21
  2. package/dist/_shared/cgdb/schema-constants.d.ts +2 -2
  3. package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -1
  4. package/dist/_shared/cgdb/schema-constants.js +3 -0
  5. package/dist/_shared/cgdb/schema-constants.js.map +1 -1
  6. package/dist/_shared/feature-clusters.d.ts +99 -0
  7. package/dist/_shared/feature-clusters.d.ts.map +1 -0
  8. package/dist/_shared/feature-clusters.js +2 -0
  9. package/dist/_shared/feature-clusters.js.map +1 -0
  10. package/dist/_shared/graph/types.d.ts +16 -2
  11. package/dist/_shared/graph/types.d.ts.map +1 -1
  12. package/dist/_shared/index.d.ts +1 -0
  13. package/dist/_shared/index.d.ts.map +1 -1
  14. package/dist/_shared/index.js.map +1 -1
  15. package/dist/_shared/pipeline.d.ts +1 -1
  16. package/dist/_shared/pipeline.d.ts.map +1 -1
  17. package/dist/cli/ai-context.js +4 -0
  18. package/dist/cli/analyze.js +46 -26
  19. package/dist/cli/index.js +39 -1
  20. package/dist/cli/serve.d.ts +1 -0
  21. package/dist/cli/serve.js +3 -1
  22. package/dist/cli/setup.js +42 -21
  23. package/dist/cli/status.d.ts +13 -0
  24. package/dist/cli/status.js +99 -0
  25. package/dist/cli/tool.d.ts +25 -0
  26. package/dist/cli/tool.js +74 -0
  27. package/dist/config/ignore-service.js +2 -0
  28. package/dist/config/supported-languages.d.ts +3 -3
  29. package/dist/config/supported-languages.js +3 -3
  30. package/dist/core/cgdb/cgdb-adapter.js +19 -3
  31. package/dist/core/cgdb/csv-generator.js +33 -2
  32. package/dist/core/cgdb/schema.d.ts +2 -1
  33. package/dist/core/cgdb/schema.js +55 -0
  34. package/dist/core/embeddings/embedder.js +4 -2
  35. package/dist/core/graphstore/cgdb-row-source.js +3 -2
  36. package/dist/core/graphstore/index.d.ts +1 -1
  37. package/dist/core/graphstore/index.js +1 -1
  38. package/dist/core/group/bridge-db.js +42 -10
  39. package/dist/core/group/service.d.ts +16 -0
  40. package/dist/core/group/service.js +360 -0
  41. package/dist/core/ingestion/emit-references.d.ts +1 -1
  42. package/dist/core/ingestion/emit-references.js +1 -1
  43. package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
  44. package/dist/core/ingestion/feature-cluster-processor.js +626 -0
  45. package/dist/core/ingestion/finalize-orchestrator.js +1 -1
  46. package/dist/core/ingestion/model/registration-table.js +1 -0
  47. package/dist/core/ingestion/model/resolve.d.ts +2 -2
  48. package/dist/core/ingestion/model/resolve.js +3 -3
  49. package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
  50. package/dist/core/ingestion/model/semantic-model.js +1 -1
  51. package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
  52. package/dist/core/ingestion/model/symbol-table.js +1 -1
  53. package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
  54. package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
  55. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  56. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  57. package/dist/core/ingestion/pipeline.d.ts +4 -0
  58. package/dist/core/ingestion/pipeline.js +9 -5
  59. package/dist/core/run-analyze.d.ts +21 -0
  60. package/dist/core/run-analyze.js +213 -6
  61. package/dist/core/search/hybrid-search.js +11 -3
  62. package/dist/mcp/core/embedder.js +5 -2
  63. package/dist/mcp/local/local-backend.d.ts +12 -0
  64. package/dist/mcp/local/local-backend.js +381 -3
  65. package/dist/mcp/resources.js +139 -0
  66. package/dist/mcp/tools.js +174 -2
  67. package/dist/server/api.d.ts +14 -2
  68. package/dist/server/api.js +206 -7
  69. package/dist/server/mcp-http.d.ts +22 -0
  70. package/dist/server/mcp-http.js +21 -2
  71. package/dist/server/web-dashboard.d.ts +28 -0
  72. package/dist/server/web-dashboard.js +61 -0
  73. package/dist/storage/repo-manager.d.ts +6 -1
  74. package/dist/storage/repo-manager.js +5 -1
  75. package/dist/types/pipeline.d.ts +2 -0
  76. package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
  77. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
  78. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
  79. package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
  80. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
  81. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
  82. package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
  83. package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
  84. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
  85. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
  86. package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
  87. package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
  88. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
  89. package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
  90. package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
  91. package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
  92. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
  93. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
  94. package/dist/web/assets/context-builder-22jU3V56.js +16 -0
  95. package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
  96. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
  97. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
  98. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
  99. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
  100. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
  101. package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
  102. package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
  103. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
  104. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
  105. package/dist/web/assets/index-BgeqpYgd.js +1415 -0
  106. package/dist/web/assets/index-CT0GtFLZ.css +1 -0
  107. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
  108. package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
  109. package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
  110. package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
  111. package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
  112. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
  113. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
  114. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
  115. package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
  116. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
  117. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
  118. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
  119. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
  120. package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
  121. package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
  122. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
  123. package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
  124. package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
  125. package/dist/web/codragraph-logo-512.png +0 -0
  126. package/dist/web/codragraph-logo.png +0 -0
  127. package/dist/web/favicon.png +0 -0
  128. package/dist/web/index.html +36 -0
  129. package/hooks/claude/codragraph-hook.cjs +24 -9
  130. package/hooks/claude/pre-tool-use.sh +6 -1
  131. package/package.json +15 -4
  132. package/scripts/build.js +75 -16
  133. package/scripts/patch-tree-sitter-swift.cjs +0 -1
  134. package/skills/codragraph-cli.md +17 -1
  135. package/skills/codragraph-guide.md +6 -2
  136. package/skills/codragraph-onboarding.md +2 -2
  137. package/vendor/leiden/index.cjs +272 -285
  138. package/vendor/leiden/utils.cjs +264 -274
  139. package/dist/_shared/lbug/schema-constants.d.ts +0 -16
  140. package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
  141. package/dist/_shared/lbug/schema-constants.js +0 -67
  142. package/dist/_shared/lbug/schema-constants.js.map +0 -1
  143. package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
  144. package/dist/core/graphstore/lbug-row-source.js +0 -141
  145. package/dist/core/lbug/content-read.d.ts +0 -46
  146. package/dist/core/lbug/content-read.js +0 -64
  147. package/dist/core/lbug/csv-generator.d.ts +0 -29
  148. package/dist/core/lbug/csv-generator.js +0 -492
  149. package/dist/core/lbug/lbug-adapter.d.ts +0 -176
  150. package/dist/core/lbug/lbug-adapter.js +0 -1320
  151. package/dist/core/lbug/pool-adapter.d.ts +0 -93
  152. package/dist/core/lbug/pool-adapter.js +0 -550
  153. package/dist/core/lbug/schema.d.ts +0 -62
  154. package/dist/core/lbug/schema.js +0 -502
  155. package/dist/mcp/core/lbug-adapter.d.ts +0 -5
  156. package/dist/mcp/core/lbug-adapter.js +0 -5
package/dist/mcp/tools.js CHANGED
@@ -84,6 +84,177 @@ SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). W
84
84
  required: ['query'],
85
85
  },
86
86
  },
87
+ {
88
+ name: 'feature_clusters',
89
+ description: `List human-facing feature clusters such as Settings, AI, Auth, Billing, MCP, or Ingestion.
90
+
91
+ WHEN TO USE: First step for targeted implementation/refactoring when you need the functional area map before loading files. This is the product/domain layer above algorithmic Community nodes.
92
+ AFTER THIS: Use feature_context(name) for members with file paths and line ranges, then context() or impact() on specific symbols.`,
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ query: {
97
+ type: 'string',
98
+ description: 'Optional cluster owner search term (e.g. "settings", "AI", "billing").',
99
+ },
100
+ repo: {
101
+ type: 'string',
102
+ description: 'Repository name or path. Omit if only one repo is indexed.',
103
+ },
104
+ limit: {
105
+ type: 'number',
106
+ description: 'Max feature clusters to return (default: 100)',
107
+ default: 100,
108
+ minimum: 1,
109
+ maximum: 500,
110
+ },
111
+ },
112
+ required: [],
113
+ },
114
+ },
115
+ {
116
+ name: 'feature_context',
117
+ description: `Get a complete context pack for one FeatureCluster.
118
+
119
+ Returns the cluster metadata, member symbols/files with line ranges, outgoing/incoming feature dependencies, and related execution processes.
120
+
121
+ WHEN TO USE: Before editing a feature area like Settings or AI. This narrows exploration to the exact files and symbols in that feature cluster.`,
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ name: {
126
+ type: 'string',
127
+ description: 'Feature cluster name, slug, or id (e.g. "Settings", "settings").',
128
+ },
129
+ repo: {
130
+ type: 'string',
131
+ description: 'Repository name or path. Omit if only one repo is indexed.',
132
+ },
133
+ limit: {
134
+ type: 'number',
135
+ description: 'Max members to return (default: 100)',
136
+ default: 100,
137
+ minimum: 1,
138
+ maximum: 500,
139
+ },
140
+ },
141
+ required: ['name'],
142
+ },
143
+ },
144
+ {
145
+ name: 'cluster_query',
146
+ description: `Cluster-first alias for feature_clusters.
147
+
148
+ WHEN TO USE: Ask which product/domain cluster owns an area like Settings, AI, Auth, or Billing before loading files.`,
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ query: {
153
+ type: 'string',
154
+ description: 'Optional cluster owner search term (e.g. "settings", "AI", "billing").',
155
+ },
156
+ repo: {
157
+ type: 'string',
158
+ description: 'Repository name or path. Omit if only one repo is indexed.',
159
+ },
160
+ limit: {
161
+ type: 'number',
162
+ description: 'Max feature clusters to return (default: 100)',
163
+ default: 100,
164
+ minimum: 1,
165
+ maximum: 500,
166
+ },
167
+ },
168
+ required: [],
169
+ },
170
+ },
171
+ {
172
+ name: 'cluster_context',
173
+ description: `Cluster-first alias for feature_context.
174
+
175
+ Returns a FeatureCluster context pack with members, entry points, routes, tools, tests, docs, dependencies, and safe edit surface.`,
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ name: {
180
+ type: 'string',
181
+ description: 'Feature cluster name, slug, or id (e.g. "Settings", "settings").',
182
+ },
183
+ repo: {
184
+ type: 'string',
185
+ description: 'Repository name or path. Omit if only one repo is indexed.',
186
+ },
187
+ limit: {
188
+ type: 'number',
189
+ description: 'Max members to return (default: 100)',
190
+ default: 100,
191
+ minimum: 1,
192
+ maximum: 500,
193
+ },
194
+ },
195
+ required: ['name'],
196
+ },
197
+ },
198
+ {
199
+ name: 'context_pack',
200
+ description: `Get the compact agent context pack for a FeatureCluster.
201
+
202
+ WHEN TO USE: Before a refactor or implementation task where the agent should avoid re-exploring the full repo.`,
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ name: {
207
+ type: 'string',
208
+ description: 'Feature cluster name, slug, or id (e.g. "AI", "ai").',
209
+ },
210
+ repo: {
211
+ type: 'string',
212
+ description: 'Repository name or path. Omit if only one repo is indexed.',
213
+ },
214
+ limit: {
215
+ type: 'number',
216
+ description: 'Max members to return (default: 100)',
217
+ default: 100,
218
+ minimum: 1,
219
+ maximum: 500,
220
+ },
221
+ },
222
+ required: ['name'],
223
+ },
224
+ },
225
+ {
226
+ name: 'cluster_impact',
227
+ description: `Assess feature-level blast radius for a FeatureCluster.
228
+
229
+ Returns upstream/downstream cluster dependencies plus the same context pack and safe edit surface used for targeted edits.`,
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ name: {
234
+ type: 'string',
235
+ description: 'Feature cluster name, slug, or id.',
236
+ },
237
+ direction: {
238
+ type: 'string',
239
+ enum: ['upstream', 'downstream', 'both'],
240
+ description: 'Dependency direction to inspect.',
241
+ default: 'upstream',
242
+ },
243
+ repo: {
244
+ type: 'string',
245
+ description: 'Repository name or path. Omit if only one repo is indexed.',
246
+ },
247
+ limit: {
248
+ type: 'number',
249
+ description: 'Max members to include in the context pack (default: 100)',
250
+ default: 100,
251
+ minimum: 1,
252
+ maximum: 500,
253
+ },
254
+ },
255
+ required: ['name'],
256
+ },
257
+ },
87
258
  {
88
259
  name: 'cypher',
89
260
  description: `Execute Cypher query against the code knowledge graph.
@@ -92,10 +263,10 @@ WHEN TO USE: Complex structural queries that search/explore can't answer. READ c
92
263
  AFTER THIS: Use context() on result symbols for deeper context.
93
264
 
94
265
  SCHEMA:
95
- - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool
266
+ - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, FeatureCluster, Route, Tool
96
267
  - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
97
268
  - All edges via single CodeRelation table with 'type' property
98
- - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, METHOD_IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF
269
+ - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, METHOD_IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF, WRAPS, QUERIES, FEATURE_MEMBER_OF, FEATURE_DEPENDS_ON
99
270
  - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
100
271
 
101
272
  EXAMPLES:
@@ -129,6 +300,7 @@ TIPS:
129
300
  - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
130
301
  - Community = auto-detected functional area (Leiden algorithm). Properties: heuristicLabel, cohesion, symbolCount, keywords, description, enrichedBy
131
302
  - Process = execution flow trace from entry point to terminal. Properties: heuristicLabel, processType, stepCount, communities, entryPointId, terminalId
303
+ - FeatureCluster = product/domain area for targeted context packs. Properties: name, slug, featureKind, summary, repo, service, memberCount, entryPointIds, routes, tools, testCoverageHints, lastIndexedCommit, confidence, signals
132
304
  - Use heuristicLabel (not label) for human-readable community/process names`,
133
305
  inputSchema: {
134
306
  type: 'object',
@@ -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();
@@ -202,6 +227,9 @@ const getNodeQuery = (table, includeContent) => {
202
227
  if (table === 'Process') {
203
228
  return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
204
229
  }
230
+ if (table === 'FeatureCluster') {
231
+ return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.slug AS slug, n.featureKind AS featureKind, n.summary AS summary, n.description AS description, n.repo AS repo, n.service AS service, n.signals AS signals, n.memberCount AS memberCount, n.entryPointIds AS entryPointIds, n.routes AS routes, n.tools AS tools, n.testCoverageHints AS testCoverageHints, n.lastIndexedCommit AS lastIndexedCommit, n.confidence AS confidence, n.source AS source`;
232
+ }
205
233
  if (table === 'Route') {
206
234
  return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware`;
207
235
  }
@@ -233,6 +261,20 @@ const mapGraphNodeRow = (table, row, includeContent) => ({
233
261
  communities: row.communities,
234
262
  entryPointId: row.entryPointId,
235
263
  terminalId: row.terminalId,
264
+ slug: row.slug,
265
+ featureKind: row.featureKind,
266
+ summary: row.summary,
267
+ repo: row.repo,
268
+ service: row.service,
269
+ signals: row.signals,
270
+ memberCount: row.memberCount,
271
+ entryPointIds: row.entryPointIds,
272
+ routes: row.routes,
273
+ tools: row.tools,
274
+ testCoverageHints: row.testCoverageHints,
275
+ lastIndexedCommit: row.lastIndexedCommit,
276
+ confidence: row.confidence,
277
+ source: row.source,
236
278
  },
237
279
  });
238
280
  const mapGraphRelationshipRow = (row) => ({
@@ -338,6 +380,8 @@ const mountSSEProgress = (app, routePath, jm) => {
338
380
  });
339
381
  };
340
382
  const statusFromError = (err) => {
383
+ if (isGraphStoreCorruptionError(err))
384
+ return 503;
341
385
  const msg = String(err?.message ?? '');
342
386
  if (msg.includes('No indexed repositories') || msg.includes('not found'))
343
387
  return 404;
@@ -354,9 +398,14 @@ const requestedRepo = (req) => {
354
398
  }
355
399
  return undefined;
356
400
  };
357
- export const createServer = async (port, host = '127.0.0.1') => {
401
+ export const createServer = async (port, host = '127.0.0.1', options = {}) => {
358
402
  const app = express();
359
403
  app.disable('x-powered-by');
404
+ let webDashboard = {
405
+ mode: options.web ?? 'local',
406
+ served: false,
407
+ hostedUrl: HOSTED_WEB_APP_URL,
408
+ };
360
409
  // CORS: allow localhost, private/LAN networks, and the deployed site.
361
410
  // Non-browser requests (curl, server-to-server) have no origin and are allowed.
362
411
  // Disallowed origins get the response without Access-Control-Allow-Origin,
@@ -503,7 +552,15 @@ export const createServer = async (port, host = '127.0.0.1') => {
503
552
  else {
504
553
  launchContext = 'global';
505
554
  }
506
- 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
+ });
507
564
  });
508
565
  // List all registered repos
509
566
  app.get('/api/repos', async (_req, res) => {
@@ -978,10 +1035,13 @@ export const createServer = async (port, host = '127.0.0.1') => {
978
1035
  if (err instanceof ClientDisconnectedError) {
979
1036
  return;
980
1037
  }
1038
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.graph');
981
1039
  const message = err.message || 'Failed to build graph';
982
1040
  if (res.headersSent) {
983
1041
  try {
984
- 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');
985
1045
  }
986
1046
  catch {
987
1047
  // Best-effort only after streaming has started.
@@ -989,6 +1049,10 @@ export const createServer = async (port, host = '127.0.0.1') => {
989
1049
  res.end();
990
1050
  return;
991
1051
  }
1052
+ if (graphStoreError) {
1053
+ res.status(503).json(graphStoreError);
1054
+ return;
1055
+ }
992
1056
  res.status(500).json({ error: message });
993
1057
  }
994
1058
  });
@@ -1014,9 +1078,63 @@ export const createServer = async (port, host = '127.0.0.1') => {
1014
1078
  res.json({ result });
1015
1079
  }
1016
1080
  catch (err) {
1081
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.query');
1082
+ if (graphStoreError) {
1083
+ res.status(503).json(graphStoreError);
1084
+ return;
1085
+ }
1017
1086
  res.status(500).json({ error: err.message || 'Query failed' });
1018
1087
  }
1019
1088
  });
1089
+ // Symbol context through the same LocalBackend path as MCP `context`
1090
+ app.post('/api/context', async (req, res) => {
1091
+ try {
1092
+ const name = String(req.body?.name ?? '').trim();
1093
+ if (!name) {
1094
+ res.status(400).json({ error: 'Missing "name" in request body' });
1095
+ return;
1096
+ }
1097
+ const result = await backend.callTool('context', {
1098
+ ...req.body,
1099
+ name,
1100
+ repo: requestedRepo(req),
1101
+ });
1102
+ res.json(result);
1103
+ }
1104
+ catch (err) {
1105
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.context');
1106
+ if (graphStoreError) {
1107
+ res.status(503).json(graphStoreError);
1108
+ return;
1109
+ }
1110
+ res.status(statusFromError(err)).json({ error: err.message || 'Context query failed' });
1111
+ }
1112
+ });
1113
+ // Symbol impact through the same LocalBackend path as MCP `impact`
1114
+ app.post('/api/impact', async (req, res) => {
1115
+ try {
1116
+ const target = String(req.body?.target ?? '').trim();
1117
+ if (!target) {
1118
+ res.status(400).json({ error: 'Missing "target" in request body' });
1119
+ return;
1120
+ }
1121
+ const result = await backend.callTool('impact', {
1122
+ ...req.body,
1123
+ target,
1124
+ direction: req.body?.direction ?? 'upstream',
1125
+ repo: requestedRepo(req),
1126
+ });
1127
+ res.json(result);
1128
+ }
1129
+ catch (err) {
1130
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.impact');
1131
+ if (graphStoreError) {
1132
+ res.status(503).json(graphStoreError);
1133
+ return;
1134
+ }
1135
+ res.status(statusFromError(err)).json({ error: err.message || 'Impact query failed' });
1136
+ }
1137
+ });
1020
1138
  // Search (supports mode: 'hybrid' | 'semantic' | 'bm25', and optional enrichment)
1021
1139
  app.post('/api/search', async (req, res) => {
1022
1140
  try {
@@ -1141,6 +1259,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1141
1259
  res.json({ results });
1142
1260
  }
1143
1261
  catch (err) {
1262
+ const graphStoreError = getGraphStoreErrorResponse(err, 'api.search');
1263
+ if (graphStoreError) {
1264
+ res.status(503).json(graphStoreError);
1265
+ return;
1266
+ }
1144
1267
  res.status(500).json({ error: err.message || 'Search failed' });
1145
1268
  }
1146
1269
  });
@@ -1326,6 +1449,66 @@ export const createServer = async (port, host = '127.0.0.1') => {
1326
1449
  .json({ error: err.message || 'Failed to query cluster detail' });
1327
1450
  }
1328
1451
  });
1452
+ // List all feature clusters
1453
+ app.get('/api/feature-clusters', async (req, res) => {
1454
+ try {
1455
+ const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
1456
+ const query = String(req.query.query ?? '');
1457
+ const result = await backend.queryFeatureClusters(requestedRepo(req), limit, query);
1458
+ res.json(result);
1459
+ }
1460
+ catch (err) {
1461
+ res
1462
+ .status(statusFromError(err))
1463
+ .json({ error: err.message || 'Failed to query feature clusters' });
1464
+ }
1465
+ });
1466
+ // Feature cluster detail
1467
+ app.get('/api/feature-cluster', async (req, res) => {
1468
+ try {
1469
+ const name = String(req.query.name ?? '').trim();
1470
+ if (!name) {
1471
+ res.status(400).json({ error: 'Missing "name" query parameter' });
1472
+ return;
1473
+ }
1474
+ const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
1475
+ const result = await backend.queryFeatureContext(name, requestedRepo(req), limit);
1476
+ if (result?.error) {
1477
+ res.status(404).json({ error: result.error });
1478
+ return;
1479
+ }
1480
+ res.json(result);
1481
+ }
1482
+ catch (err) {
1483
+ res
1484
+ .status(statusFromError(err))
1485
+ .json({ error: err.message || 'Failed to query feature cluster detail' });
1486
+ }
1487
+ });
1488
+ // Feature cluster impact/context pack
1489
+ app.get(['/api/feature-impact', '/api/cluster-impact'], async (req, res) => {
1490
+ try {
1491
+ const name = String(req.query.name ?? '').trim();
1492
+ if (!name) {
1493
+ res.status(400).json({ error: 'Missing "name" query parameter' });
1494
+ return;
1495
+ }
1496
+ const limit = req.query.limit ? Number.parseInt(String(req.query.limit), 10) : undefined;
1497
+ const directionText = String(req.query.direction ?? 'upstream');
1498
+ const direction = directionText === 'downstream' || directionText === 'both' ? directionText : 'upstream';
1499
+ const result = await backend.queryFeatureImpact(name, requestedRepo(req), direction, limit);
1500
+ if (result?.error) {
1501
+ res.status(404).json({ error: result.error });
1502
+ return;
1503
+ }
1504
+ res.json(result);
1505
+ }
1506
+ catch (err) {
1507
+ res
1508
+ .status(statusFromError(err))
1509
+ .json({ error: err.message || 'Failed to query feature cluster impact' });
1510
+ }
1511
+ });
1329
1512
  // ── Analyze API ──────────────────────────────────────────────────────
1330
1513
  // POST /api/analyze — start a new analysis job
1331
1514
  app.post('/api/analyze', async (req, res) => {
@@ -1697,6 +1880,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1697
1880
  embedJobManager.cancelJob(req.params.jobId, 'Cancelled by user');
1698
1881
  res.json({ id: job.id, status: 'failed', error: 'Cancelled by user' });
1699
1882
  });
1883
+ webDashboard = mountWebDashboard(app, { mode: options.web ?? 'local' });
1700
1884
  // Global error handler — catch anything the route handlers miss
1701
1885
  app.use((err, _req, res, _next) => {
1702
1886
  console.error('Unhandled error:', err);
@@ -1707,7 +1891,22 @@ export const createServer = async (port, host = '127.0.0.1') => {
1707
1891
  await new Promise((resolve, reject) => {
1708
1892
  const server = app.listen(port, host, () => {
1709
1893
  const displayHost = host === '::' || host === '0.0.0.0' ? 'localhost' : host;
1710
- 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
+ }
1711
1910
  resolve();
1712
1911
  });
1713
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;