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 +9 -0
- package/dist/bin/cntx-ui.js +1 -1
- package/dist/lib/api-router.js +19 -0
- package/dist/lib/artifact-manager.js +147 -0
- package/dist/lib/bundle-manager.js +4 -0
- package/dist/lib/mcp-server.js +42 -0
- package/dist/lib/semantic-splitter.js +9 -1
- package/dist/lib/simple-vector-store.js +3 -2
- package/dist/server.js +32 -8
- package/package.json +1 -1
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
|
package/dist/bin/cntx-ui.js
CHANGED
|
@@ -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
|
|
35
|
+
await startServer({ withMcp: true, skipFileWatcher: true });
|
|
36
36
|
break;
|
|
37
37
|
case 'bundle':
|
|
38
38
|
const bundleName = args[1] || 'master';
|
package/dist/lib/api-router.js
CHANGED
|
@@ -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) {
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|