cntx-ui 3.1.2 → 3.1.3

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 CHANGED
@@ -40,6 +40,10 @@ Agents interact through MCP tools or the HTTP API:
40
40
  | `agent/query` | Semantic search — "where is auth handled?" |
41
41
  | `agent/investigate` | Find integration points for a new feature |
42
42
  | `agent/organize` | Audit and optimize bundle structure |
43
+ | `artifacts/list` | List normalized project artifacts (OpenAPI + Navigation) |
44
+ | `artifacts/get_openapi` | Return OpenAPI artifact summary and payload |
45
+ | `artifacts/get_navigation` | Return Navigation artifact summary and payload |
46
+ | `artifacts/summarize` | Compact cross-artifact summary for agents |
43
47
  | `list_bundles` | List all bundles with metadata |
44
48
  | `get_bundle` | Get full bundle content as XML |
45
49
  | `get_semantic_chunks` | Get all analyzed code chunks |
@@ -47,6 +51,11 @@ Agents interact through MCP tools or the HTTP API:
47
51
 
48
52
  Full tool reference with parameters is generated in `.cntx/AGENT.md` and `.cntx/TOOLS.md`.
49
53
 
54
+ Artifact HTTP endpoints:
55
+ - `GET /api/artifacts`
56
+ - `GET /api/artifacts/openapi`
57
+ - `GET /api/artifacts/navigation`
58
+
50
59
  ## How it works
51
60
 
52
61
  1. **tree-sitter** parses source files into AST, extracts functions/types/interfaces
@@ -32,7 +32,7 @@ async function main() {
32
32
  await initConfig();
33
33
  break;
34
34
  case 'mcp':
35
- await startServer({ withMcp: true, skipFileWatcher: true, skipBundleGeneration: true });
35
+ await startServer({ withMcp: true, skipFileWatcher: true });
36
36
  break;
37
37
  case 'bundle':
38
38
  const bundleName = args[1] || 'master';
@@ -106,6 +106,16 @@ export default class APIRouter {
106
106
  if (pathname === '/api/mcp-status' && method === 'GET') {
107
107
  return await this.handleGetMcpStatus(req, res);
108
108
  }
109
+ // === Artifact Endpoints ===
110
+ if (pathname === '/api/artifacts' && method === 'GET') {
111
+ return await this.handleGetArtifacts(req, res);
112
+ }
113
+ if (pathname === '/api/artifacts/openapi' && method === 'GET') {
114
+ return await this.handleGetArtifact(req, res, 'openapi');
115
+ }
116
+ if (pathname === '/api/artifacts/navigation' && method === 'GET') {
117
+ return await this.handleGetArtifact(req, res, 'navigation');
118
+ }
109
119
  // === Rule Management ===
110
120
  if (pathname === '/api/cursor-rules' && method === 'GET') {
111
121
  return await this.handleGetCursorRules(req, res);
@@ -350,6 +360,15 @@ export default class APIRouter {
350
360
  message: isRunning ? 'MCP server is running' : 'MCP server integration available'
351
361
  });
352
362
  }
363
+ async handleGetArtifacts(req, res) {
364
+ const artifacts = this.cntxServer.artifactManager.refresh();
365
+ this.sendResponse(res, 200, { artifacts });
366
+ }
367
+ async handleGetArtifact(req, res, type) {
368
+ this.cntxServer.artifactManager.refresh();
369
+ const payload = this.cntxServer.artifactManager.getPayload(type);
370
+ this.sendResponse(res, 200, payload);
371
+ }
353
372
  async handleGetCursorRules(req, res) {
354
373
  const filePath = join(this.configManager.CWD, '.cursorrules');
355
374
  if (existsSync(filePath)) {
@@ -0,0 +1,147 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ export default class ArtifactManager {
4
+ cwd;
5
+ records;
6
+ constructor(cwd) {
7
+ this.cwd = cwd;
8
+ this.records = new Map();
9
+ }
10
+ refresh() {
11
+ const openapi = this.resolveOpenApiArtifact();
12
+ const navigation = this.resolveNavigationArtifact();
13
+ this.records.set('openapi', openapi);
14
+ this.records.set('navigation', navigation);
15
+ return this.list();
16
+ }
17
+ list() {
18
+ if (this.records.size === 0) {
19
+ this.refresh();
20
+ }
21
+ return Array.from(this.records.values());
22
+ }
23
+ get(type) {
24
+ if (!this.records.has(type)) {
25
+ this.refresh();
26
+ }
27
+ return this.records.get(type);
28
+ }
29
+ getPayload(type) {
30
+ const record = this.get(type);
31
+ if (!record) {
32
+ return {
33
+ type,
34
+ filePath: '',
35
+ format: 'json',
36
+ exists: false,
37
+ summary: {}
38
+ };
39
+ }
40
+ if (!record.exists) {
41
+ return { ...record };
42
+ }
43
+ try {
44
+ const absolutePath = join(this.cwd, record.filePath);
45
+ const raw = readFileSync(absolutePath, 'utf8');
46
+ if (record.format === 'json') {
47
+ return { ...record, parsed: JSON.parse(raw), raw };
48
+ }
49
+ return { ...record, raw };
50
+ }
51
+ catch {
52
+ return { ...record };
53
+ }
54
+ }
55
+ resolveOpenApiArtifact() {
56
+ const candidates = [
57
+ { path: 'openapi.json', format: 'json' },
58
+ { path: 'openapi.yaml', format: 'yaml' },
59
+ { path: 'openapi.yml', format: 'yaml' }
60
+ ];
61
+ for (const candidate of candidates) {
62
+ const absolutePath = join(this.cwd, candidate.path);
63
+ if (!existsSync(absolutePath))
64
+ continue;
65
+ const summary = this.summarizeOpenApi(absolutePath, candidate.format);
66
+ return {
67
+ type: 'openapi',
68
+ filePath: candidate.path,
69
+ format: candidate.format,
70
+ exists: true,
71
+ summary
72
+ };
73
+ }
74
+ return {
75
+ type: 'openapi',
76
+ filePath: 'openapi.json',
77
+ format: 'json',
78
+ exists: false,
79
+ summary: {}
80
+ };
81
+ }
82
+ resolveNavigationArtifact() {
83
+ const candidates = [
84
+ { path: 'navigation.manifest.json', format: 'json' },
85
+ { path: 'navigation.json', format: 'json' }
86
+ ];
87
+ for (const candidate of candidates) {
88
+ const absolutePath = join(this.cwd, candidate.path);
89
+ if (!existsSync(absolutePath))
90
+ continue;
91
+ const summary = this.summarizeNavigation(absolutePath);
92
+ return {
93
+ type: 'navigation',
94
+ filePath: candidate.path,
95
+ format: candidate.format,
96
+ exists: true,
97
+ summary
98
+ };
99
+ }
100
+ return {
101
+ type: 'navigation',
102
+ filePath: 'navigation.manifest.json',
103
+ format: 'json',
104
+ exists: false,
105
+ summary: {}
106
+ };
107
+ }
108
+ summarizeOpenApi(absolutePath, format) {
109
+ try {
110
+ const raw = readFileSync(absolutePath, 'utf8');
111
+ if (format === 'json') {
112
+ const parsed = JSON.parse(raw);
113
+ const endpointCount = parsed.paths ? Object.keys(parsed.paths).length : 0;
114
+ return {
115
+ title: parsed.info?.title,
116
+ version: parsed.info?.version,
117
+ endpointCount
118
+ };
119
+ }
120
+ const titleMatch = raw.match(/^\s*title:\s*["']?(.+?)["']?\s*$/m);
121
+ const versionMatch = raw.match(/^\s*version:\s*["']?(.+?)["']?\s*$/m);
122
+ return {
123
+ title: titleMatch?.[1],
124
+ version: versionMatch?.[1]
125
+ };
126
+ }
127
+ catch {
128
+ return {};
129
+ }
130
+ }
131
+ summarizeNavigation(absolutePath) {
132
+ try {
133
+ const parsed = JSON.parse(readFileSync(absolutePath, 'utf8'));
134
+ return {
135
+ title: parsed.project?.name || parsed.name,
136
+ version: parsed.version ? String(parsed.version) : undefined,
137
+ routeCount: Array.isArray(parsed.routes) ? parsed.routes.length : 0,
138
+ stateCount: Array.isArray(parsed.states) ? parsed.states.length : 0,
139
+ flowCount: Array.isArray(parsed.flows) ? parsed.flows.length : 0,
140
+ viewportCount: Array.isArray(parsed.viewports) ? parsed.viewports.length : 0
141
+ };
142
+ }
143
+ catch {
144
+ return {};
145
+ }
146
+ }
147
+ }
@@ -170,6 +170,10 @@ export default class BundleManager {
170
170
  try {
171
171
  const fullPath = this.fileSystemManager.fullPath(relativePath);
172
172
  const content = readFileSync(fullPath, 'utf8');
173
+ // Skip files over 100KB in bundle XML to prevent string length crashes
174
+ if (content.length > 100_000) {
175
+ return ` <file path="${this.escapeXml(relativePath)}" skipped="too-large" size="${content.length}" />\n`;
176
+ }
173
177
  const chunks = this.db.getChunksByFile(relativePath);
174
178
  let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
175
179
  if (chunks.length > 0) {
@@ -92,6 +92,28 @@ export class MCPServer {
92
92
  }
93
93
  // Legacy tools mapping
94
94
  switch (name) {
95
+ case 'artifacts/list': {
96
+ const artifacts = this.cntxServer.artifactManager.refresh();
97
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify({ artifacts }, null, 2) }] });
98
+ }
99
+ case 'artifacts/get_openapi': {
100
+ this.cntxServer.artifactManager.refresh();
101
+ const payload = this.cntxServer.artifactManager.getPayload('openapi');
102
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
103
+ }
104
+ case 'artifacts/get_navigation': {
105
+ this.cntxServer.artifactManager.refresh();
106
+ const payload = this.cntxServer.artifactManager.getPayload('navigation');
107
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] });
108
+ }
109
+ case 'artifacts/summarize': {
110
+ const artifacts = this.cntxServer.artifactManager.refresh();
111
+ const summary = {
112
+ openapi: artifacts.find((a) => a.type === 'openapi')?.summary ?? {},
113
+ navigation: artifacts.find((a) => a.type === 'navigation')?.summary ?? {}
114
+ };
115
+ return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] });
116
+ }
95
117
  case 'list_bundles':
96
118
  const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
97
119
  return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(bundles, null, 2) }] });
@@ -144,6 +166,26 @@ export class MCPServer {
144
166
  description: 'Investigation Mode: Suggest integration points.',
145
167
  inputSchema: { type: 'object', properties: { feature: { type: 'string' } }, required: ['feature'] }
146
168
  },
169
+ {
170
+ name: 'artifacts/list',
171
+ description: 'List normalized project artifacts (OpenAPI and Navigation manifests).',
172
+ inputSchema: { type: 'object', properties: {} }
173
+ },
174
+ {
175
+ name: 'artifacts/get_openapi',
176
+ description: 'Get OpenAPI artifact payload (summary + parsed content when JSON).',
177
+ inputSchema: { type: 'object', properties: {} }
178
+ },
179
+ {
180
+ name: 'artifacts/get_navigation',
181
+ description: 'Get Navigation artifact payload (summary + parsed manifest).',
182
+ inputSchema: { type: 'object', properties: {} }
183
+ },
184
+ {
185
+ name: 'artifacts/summarize',
186
+ description: 'Get compact summaries for OpenAPI and Navigation artifacts.',
187
+ inputSchema: { type: 'object', properties: {} }
188
+ },
147
189
  {
148
190
  name: 'list_bundles',
149
191
  description: 'List all project bundles.',
@@ -106,6 +106,11 @@ export default class SemanticSplitter {
106
106
  if (!existsSync(fullPath))
107
107
  return [];
108
108
  const content = readFileSync(fullPath, 'utf8');
109
+ // Skip files larger than 200KB — tree-sitter and embeddings can't handle them well
110
+ if (content.length > 200_000) {
111
+ console.warn(`Skipping ${relativePath}: file too large (${Math.round(content.length / 1024)}KB)`);
112
+ return [];
113
+ }
109
114
  const parser = this.getParser(relativePath);
110
115
  const tree = parser.parse(content);
111
116
  const root = tree.rootNode;
@@ -394,7 +399,10 @@ export default class SemanticSplitter {
394
399
  if (!node)
395
400
  continue;
396
401
  if (node.type === 'table' || node.type === 'table_array_element' || node.type === 'pair') {
397
- const keyNode = node.childForFieldName('name') || node.childForFieldName('key') || node.namedChild(0);
402
+ // Legacy parser uses namedChild instead of childForFieldName
403
+ const keyNode = node.childForFieldName?.('name')
404
+ || node.childForFieldName?.('key')
405
+ || node.namedChild(0);
398
406
  const name = keyNode ? content.slice(keyNode.startIndex, keyNode.endIndex) : node.type;
399
407
  const structure = this.mapStructureNode(name, node, content, filePath);
400
408
  if (structure.code.length >= this.options.minStructureSize) {
@@ -37,8 +37,9 @@ export default class SimpleVectorStore {
37
37
  const existing = this.db.getEmbedding(chunkId);
38
38
  if (existing)
39
39
  return existing;
40
- // Generate new embedding
41
- const textToEmbed = `${chunk.name} ${chunk.purpose} ${chunk.code}`;
40
+ // Generate new embedding — truncate to 8KB to stay within model limits
41
+ const rawText = `${chunk.name} ${chunk.purpose} ${chunk.code}`;
42
+ const textToEmbed = rawText.length > 8192 ? rawText.slice(0, 8192) : rawText;
42
43
  const embedding = await this.generateEmbedding(textToEmbed);
43
44
  // Save to SQLite
44
45
  this.db.saveEmbedding(chunkId, embedding, this.modelName);
package/dist/server.js CHANGED
@@ -13,6 +13,7 @@ import FileSystemManager from './lib/file-system-manager.js';
13
13
  import BundleManager from './lib/bundle-manager.js';
14
14
  import APIRouter from './lib/api-router.js';
15
15
  import WebSocketManager from './lib/websocket-manager.js';
16
+ import ArtifactManager from './lib/artifact-manager.js';
16
17
  // Import existing lib modules
17
18
  import SemanticSplitter from './lib/semantic-splitter.js';
18
19
  import SimpleVectorStore from './lib/simple-vector-store.js';
@@ -47,6 +48,7 @@ export class CntxServer {
47
48
  bundleManager;
48
49
  webSocketManager;
49
50
  apiRouter;
51
+ artifactManager;
50
52
  semanticSplitter;
51
53
  vectorStore;
52
54
  agentRuntime;
@@ -71,6 +73,7 @@ export class CntxServer {
71
73
  this.fileSystemManager = new FileSystemManager(cwd, { verbose: this.verbose });
72
74
  this.bundleManager = new BundleManager(this.configManager, this.fileSystemManager, this.verbose);
73
75
  this.webSocketManager = new WebSocketManager(this.bundleManager, this.configManager, { verbose: this.verbose });
76
+ this.artifactManager = new ArtifactManager(cwd);
74
77
  // AI Components
75
78
  this.semanticSplitter = new SemanticSplitter({
76
79
  maxChunkSize: 2000,
@@ -112,6 +115,7 @@ export class CntxServer {
112
115
  }
113
116
  // Load reasoning/manifest
114
117
  await this.agentRuntime.generateAgentManifest();
118
+ this.artifactManager.refresh();
115
119
  }
116
120
  startWatching() {
117
121
  this.fileSystemManager.startWatching((eventType, filename) => {
@@ -152,14 +156,30 @@ export class CntxServer {
152
156
  async enhanceSemanticChunksIfNeeded(analysis) {
153
157
  if (!analysis || !analysis.chunks)
154
158
  return;
155
- if (!this.vectorStoreInitialized) {
156
- await this.vectorStore.init();
157
- this.vectorStoreInitialized = true;
158
- }
159
- for (const chunk of analysis.chunks) {
160
- if (!this.databaseManager.getEmbedding(chunk.id)) {
161
- await this.vectorStore.upsertChunk(chunk);
159
+ try {
160
+ if (!this.vectorStoreInitialized) {
161
+ await this.vectorStore.init();
162
+ this.vectorStoreInitialized = true;
163
+ }
164
+ let embedded = 0;
165
+ let skipped = 0;
166
+ for (const chunk of analysis.chunks) {
167
+ if (!this.databaseManager.getEmbedding(chunk.id)) {
168
+ try {
169
+ await this.vectorStore.upsertChunk(chunk);
170
+ embedded++;
171
+ }
172
+ catch (e) {
173
+ skipped++;
174
+ }
175
+ }
162
176
  }
177
+ if (skipped > 0) {
178
+ console.warn(`⚠️ Embedded ${embedded} chunks, skipped ${skipped} (too large or invalid)`);
179
+ }
180
+ }
181
+ catch (e) {
182
+ console.error(`⚠️ Vector embedding failed: ${e.message}. Semantic search will be unavailable.`);
163
183
  }
164
184
  }
165
185
  async refreshSemanticAnalysis() {
@@ -193,7 +213,11 @@ export class CntxServer {
193
213
  });
194
214
  }
195
215
  handleStaticFile(req, res, url) {
196
- const webDir = join(__dirname, 'web/dist');
216
+ let webDir = join(__dirname, 'web', 'dist');
217
+ if (!existsSync(webDir)) {
218
+ // Fallback for dist/ context — web/dist is at package root
219
+ webDir = join(__dirname, '..', 'web', 'dist');
220
+ }
197
221
  let filePath = join(webDir, url.pathname === '/' ? 'index.html' : url.pathname);
198
222
  if (!existsSync(filePath)) {
199
223
  filePath = join(webDir, 'index.html');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cntx-ui",
3
3
  "type": "module",
4
- "version": "3.1.2",
4
+ "version": "3.1.3",
5
5
  "description": "Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.",
6
6
  "keywords": [
7
7
  "repository-intelligence",