cntx-ui 3.1.0 → 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/agent-runtime.js +10 -2
- 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 +238 -6
- package/dist/lib/simple-vector-store.js +3 -2
- package/dist/server.js +78 -50
- package/package.json +8 -1
- package/templates/agent-instructions.md +7 -21
- package/web/dist/assets/{index-D2RTcdqV.js → index-8QXMnXVq.js} +3 -3
- package/web/dist/index.html +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';
|
|
@@ -39,7 +39,9 @@ export class AgentRuntime {
|
|
|
39
39
|
let toolsReference = '';
|
|
40
40
|
if (this.cntxServer.mcpServer) {
|
|
41
41
|
const tools = this.cntxServer.mcpServer.getToolDefinitions();
|
|
42
|
-
toolsReference = tools
|
|
42
|
+
toolsReference = tools
|
|
43
|
+
.filter(t => !t.name?.includes('activities'))
|
|
44
|
+
.map(t => {
|
|
43
45
|
let params = [];
|
|
44
46
|
if (t.inputSchema?.properties) {
|
|
45
47
|
params = Object.entries(t.inputSchema.properties).map(([name, prop]) => {
|
|
@@ -50,6 +52,12 @@ export class AgentRuntime {
|
|
|
50
52
|
return `### \`${t.name}\`\n${t.description}\n${params.length > 0 ? '**Parameters:**\n- ' + params.join('\n- ') : '*No parameters required*'}\n`;
|
|
51
53
|
}).join('\n');
|
|
52
54
|
}
|
|
55
|
+
// Find TOOLS.md template
|
|
56
|
+
let toolsMdPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../templates/TOOLS.md');
|
|
57
|
+
if (!fs.existsSync(toolsMdPath)) {
|
|
58
|
+
// Fallback for dist/lib/ context
|
|
59
|
+
toolsMdPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../templates/TOOLS.md');
|
|
60
|
+
}
|
|
53
61
|
const manifest = `# 🤖 Agent Handshake: ${overview.projectPath.split('/').pop()}
|
|
54
62
|
|
|
55
63
|
## Project Overview
|
|
@@ -70,7 +78,7 @@ ${toolsReference || '*(MCP Server not yet initialized, tools will appear here)*'
|
|
|
70
78
|
## 🛠 Complete Tool & API Reference
|
|
71
79
|
Refer to the dynamic reference below for full parameter schemas and HTTP fallback endpoints.
|
|
72
80
|
|
|
73
|
-
${fs.
|
|
81
|
+
${fs.existsSync(toolsMdPath) ? fs.readFileSync(toolsMdPath, 'utf8') : '*(Tools documentation missing)*'}
|
|
74
82
|
|
|
75
83
|
## Working Memory
|
|
76
84
|
This agent is **stateful**. All interactions in this directory are logged to a persistent SQLite database (\`.cntx/bundles.db\`), allowing for context retention across sessions.
|
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.',
|
|
@@ -9,6 +9,13 @@ import Parser from 'tree-sitter';
|
|
|
9
9
|
import JavaScript from 'tree-sitter-javascript';
|
|
10
10
|
import TypeScript from 'tree-sitter-typescript';
|
|
11
11
|
import Rust from 'tree-sitter-rust';
|
|
12
|
+
import Json from 'tree-sitter-json';
|
|
13
|
+
import Css from 'tree-sitter-css';
|
|
14
|
+
import Html from 'tree-sitter-html';
|
|
15
|
+
import Sql from 'tree-sitter-sql';
|
|
16
|
+
import Markdown from 'tree-sitter-markdown';
|
|
17
|
+
import Toml from 'tree-sitter-toml';
|
|
18
|
+
import LegacyParser from 'tree-sitter-legacy';
|
|
12
19
|
import HeuristicsManager from './heuristics-manager.js';
|
|
13
20
|
export default class SemanticSplitter {
|
|
14
21
|
options;
|
|
@@ -20,6 +27,7 @@ export default class SemanticSplitter {
|
|
|
20
27
|
maxChunkSize: 3000, // Max chars per chunk
|
|
21
28
|
includeContext: true, // Include imports/types needed
|
|
22
29
|
minFunctionSize: 40, // Skip tiny functions
|
|
30
|
+
minStructureSize: 20, // Skip tiny structures
|
|
23
31
|
...options
|
|
24
32
|
};
|
|
25
33
|
// Initialize tree-sitter parsers
|
|
@@ -27,17 +35,37 @@ export default class SemanticSplitter {
|
|
|
27
35
|
javascript: new Parser(),
|
|
28
36
|
typescript: new Parser(),
|
|
29
37
|
tsx: new Parser(),
|
|
30
|
-
rust: new Parser()
|
|
38
|
+
rust: new Parser(),
|
|
39
|
+
json: new Parser(),
|
|
40
|
+
css: new Parser(),
|
|
41
|
+
html: new Parser(),
|
|
42
|
+
sql: new LegacyParser(),
|
|
43
|
+
markdown: new LegacyParser(),
|
|
44
|
+
toml: new LegacyParser()
|
|
31
45
|
};
|
|
32
46
|
this.parsers.javascript.setLanguage(JavaScript);
|
|
33
47
|
this.parsers.typescript.setLanguage(TypeScript.typescript);
|
|
34
48
|
this.parsers.tsx.setLanguage(TypeScript.tsx);
|
|
35
49
|
this.parsers.rust.setLanguage(Rust);
|
|
50
|
+
this.parsers.json.setLanguage(Json);
|
|
51
|
+
this.parsers.css.setLanguage(Css);
|
|
52
|
+
this.parsers.html.setLanguage(Html);
|
|
53
|
+
this.parsers.sql.setLanguage(Sql);
|
|
54
|
+
this.parsers.markdown.setLanguage(Markdown);
|
|
55
|
+
this.parsers.toml.setLanguage(Toml);
|
|
36
56
|
this.heuristicsManager = new HeuristicsManager();
|
|
37
57
|
}
|
|
38
58
|
getParser(filePath) {
|
|
39
59
|
const ext = extname(filePath);
|
|
40
60
|
switch (ext) {
|
|
61
|
+
case '.json': return this.parsers.json;
|
|
62
|
+
case '.css': return this.parsers.css;
|
|
63
|
+
case '.scss': return this.parsers.css;
|
|
64
|
+
case '.html': return this.parsers.html;
|
|
65
|
+
case '.sql': return this.parsers.sql;
|
|
66
|
+
case '.md': return this.parsers.markdown;
|
|
67
|
+
case '.toml': return this.parsers.toml;
|
|
68
|
+
case '.jsx': return this.parsers.javascript;
|
|
41
69
|
case '.ts': return this.parsers.typescript;
|
|
42
70
|
case '.tsx': return this.parsers.tsx;
|
|
43
71
|
case '.rs': return this.parsers.rust;
|
|
@@ -78,16 +106,43 @@ export default class SemanticSplitter {
|
|
|
78
106
|
if (!existsSync(fullPath))
|
|
79
107
|
return [];
|
|
80
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
|
+
}
|
|
81
114
|
const parser = this.getParser(relativePath);
|
|
82
115
|
const tree = parser.parse(content);
|
|
83
116
|
const root = tree.rootNode;
|
|
117
|
+
const ext = extname(relativePath).toLowerCase();
|
|
84
118
|
const elements = {
|
|
85
119
|
functions: [],
|
|
86
120
|
types: [],
|
|
87
|
-
imports:
|
|
121
|
+
imports: []
|
|
88
122
|
};
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
if (['.js', '.jsx', '.ts', '.tsx', '.rs'].includes(ext)) {
|
|
124
|
+
elements.imports = this.extractImports(root, content, relativePath);
|
|
125
|
+
// Traverse AST for functions and types
|
|
126
|
+
this.traverse(root, content, relativePath, elements);
|
|
127
|
+
}
|
|
128
|
+
else if (ext === '.json') {
|
|
129
|
+
this.extractJsonStructures(root, content, relativePath, elements);
|
|
130
|
+
}
|
|
131
|
+
else if (ext === '.css' || ext === '.scss') {
|
|
132
|
+
this.extractCssStructures(root, content, relativePath, elements);
|
|
133
|
+
}
|
|
134
|
+
else if (ext === '.html') {
|
|
135
|
+
this.extractHtmlStructures(root, content, relativePath, elements);
|
|
136
|
+
}
|
|
137
|
+
else if (ext === '.sql') {
|
|
138
|
+
this.extractSqlStructures(root, content, relativePath, elements);
|
|
139
|
+
}
|
|
140
|
+
else if (ext === '.md') {
|
|
141
|
+
this.extractMarkdownStructures(root, content, relativePath, elements);
|
|
142
|
+
}
|
|
143
|
+
else if (ext === '.toml') {
|
|
144
|
+
this.extractTomlStructures(root, content, relativePath, elements);
|
|
145
|
+
}
|
|
91
146
|
// Create chunks from elements
|
|
92
147
|
return this.createChunks(elements, content, relativePath);
|
|
93
148
|
}
|
|
@@ -176,9 +231,186 @@ export default class SemanticSplitter {
|
|
|
176
231
|
startLine: node.startPosition.row + 1,
|
|
177
232
|
code,
|
|
178
233
|
isExported: this.isExported(node),
|
|
179
|
-
isAsync: code.includes('async')
|
|
234
|
+
isAsync: code.includes('async'),
|
|
235
|
+
category: 'function'
|
|
180
236
|
};
|
|
181
237
|
}
|
|
238
|
+
mapStructureNode(name, node, content, filePath) {
|
|
239
|
+
return {
|
|
240
|
+
name,
|
|
241
|
+
type: node.type,
|
|
242
|
+
filePath,
|
|
243
|
+
startLine: node.startPosition.row + 1,
|
|
244
|
+
code: content.slice(node.startIndex, node.endIndex),
|
|
245
|
+
isExported: false,
|
|
246
|
+
isAsync: false,
|
|
247
|
+
category: 'structure'
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
extractJsonStructures(root, content, filePath, elements) {
|
|
251
|
+
const rootNode = root.namedChild(0);
|
|
252
|
+
if (!rootNode)
|
|
253
|
+
return;
|
|
254
|
+
if (rootNode.type === 'object') {
|
|
255
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
256
|
+
const child = rootNode.namedChild(i);
|
|
257
|
+
if (child && child.type === 'pair') {
|
|
258
|
+
const keyNode = child.childForFieldName('key') || child.namedChild(0);
|
|
259
|
+
const name = keyNode ? content.slice(keyNode.startIndex, keyNode.endIndex).replace(/['"]/g, '') : 'pair';
|
|
260
|
+
const structure = this.mapStructureNode(name, child, content, filePath);
|
|
261
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
262
|
+
elements.functions.push(structure);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (rootNode.type === 'array') {
|
|
268
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
269
|
+
const child = rootNode.namedChild(i);
|
|
270
|
+
if (child) {
|
|
271
|
+
const structure = this.mapStructureNode(`item_${i + 1}`, child, content, filePath);
|
|
272
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
273
|
+
elements.functions.push(structure);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
extractCssStructures(root, content, filePath, elements) {
|
|
280
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
281
|
+
const node = root.namedChild(i);
|
|
282
|
+
if (!node)
|
|
283
|
+
continue;
|
|
284
|
+
if (node.type === 'rule_set' || node.type === 'at_rule') {
|
|
285
|
+
const name = this.getCssRuleName(node, content);
|
|
286
|
+
const structure = this.mapStructureNode(name, node, content, filePath);
|
|
287
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
288
|
+
elements.functions.push(structure);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
getCssRuleName(node, content) {
|
|
294
|
+
const selectorsNode = node.childForFieldName('selectors') || node.namedChild(0);
|
|
295
|
+
if (selectorsNode) {
|
|
296
|
+
return content.slice(selectorsNode.startIndex, selectorsNode.endIndex).trim();
|
|
297
|
+
}
|
|
298
|
+
const code = content.slice(node.startIndex, node.endIndex);
|
|
299
|
+
return code.split('{')[0].trim() || 'rule';
|
|
300
|
+
}
|
|
301
|
+
extractHtmlStructures(root, content, filePath, elements) {
|
|
302
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
303
|
+
const node = root.namedChild(i);
|
|
304
|
+
if (!node)
|
|
305
|
+
continue;
|
|
306
|
+
if (node.type === 'element' || node.type === 'script_element' || node.type === 'style_element') {
|
|
307
|
+
const name = this.getHtmlElementName(node, content);
|
|
308
|
+
const structure = this.mapStructureNode(name, node, content, filePath);
|
|
309
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
310
|
+
elements.functions.push(structure);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
getHtmlElementName(node, content) {
|
|
316
|
+
const startTag = node.childForFieldName('start_tag') || node.namedChild(0);
|
|
317
|
+
const tagNameNode = startTag?.childForFieldName('tag_name') || startTag?.namedChild(0);
|
|
318
|
+
if (tagNameNode) {
|
|
319
|
+
return content.slice(tagNameNode.startIndex, tagNameNode.endIndex);
|
|
320
|
+
}
|
|
321
|
+
return node.type;
|
|
322
|
+
}
|
|
323
|
+
extractSqlStructures(root, content, filePath, elements) {
|
|
324
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
325
|
+
const node = root.namedChild(i);
|
|
326
|
+
if (!node)
|
|
327
|
+
continue;
|
|
328
|
+
const name = this.getSqlStatementName(node, content);
|
|
329
|
+
const structure = this.mapStructureNode(name, node, content, filePath);
|
|
330
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
331
|
+
elements.functions.push(structure);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
getSqlStatementName(node, content) {
|
|
336
|
+
const code = content.slice(node.startIndex, node.endIndex).trim();
|
|
337
|
+
if (!code)
|
|
338
|
+
return node.type;
|
|
339
|
+
const firstLine = code.split('\n')[0];
|
|
340
|
+
const match = firstLine.match(/^\s*([A-Za-z_]+)/);
|
|
341
|
+
if (match)
|
|
342
|
+
return match[1].toUpperCase();
|
|
343
|
+
return node.type;
|
|
344
|
+
}
|
|
345
|
+
extractMarkdownStructures(root, content, filePath, elements) {
|
|
346
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
347
|
+
const node = root.namedChild(i);
|
|
348
|
+
if (!node)
|
|
349
|
+
continue;
|
|
350
|
+
if (this.isMarkdownStructureNode(node.type)) {
|
|
351
|
+
const name = this.getMarkdownNodeName(node, content);
|
|
352
|
+
const structure = this.mapStructureNode(name, node, content, filePath);
|
|
353
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
354
|
+
elements.functions.push(structure);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
isMarkdownStructureNode(type) {
|
|
360
|
+
return [
|
|
361
|
+
'atx_heading',
|
|
362
|
+
'setext_heading',
|
|
363
|
+
'fenced_code_block',
|
|
364
|
+
'indented_code_block',
|
|
365
|
+
'tight_list',
|
|
366
|
+
'loose_list',
|
|
367
|
+
'list',
|
|
368
|
+
'block_quote',
|
|
369
|
+
'thematic_break'
|
|
370
|
+
].includes(type);
|
|
371
|
+
}
|
|
372
|
+
getMarkdownNodeName(node, content) {
|
|
373
|
+
const text = content.slice(node.startIndex, node.endIndex).trim();
|
|
374
|
+
if (node.type === 'atx_heading') {
|
|
375
|
+
const withoutHashes = text.replace(/^#{1,6}\s*/, '').replace(/\s*#+\s*$/, '').trim();
|
|
376
|
+
return withoutHashes || 'heading';
|
|
377
|
+
}
|
|
378
|
+
if (node.type === 'setext_heading') {
|
|
379
|
+
const firstLine = text.split('\n')[0]?.trim();
|
|
380
|
+
return firstLine || 'heading';
|
|
381
|
+
}
|
|
382
|
+
if (node.type === 'fenced_code_block' || node.type === 'indented_code_block') {
|
|
383
|
+
return 'code_block';
|
|
384
|
+
}
|
|
385
|
+
if (node.type.includes('list')) {
|
|
386
|
+
return 'list';
|
|
387
|
+
}
|
|
388
|
+
if (node.type === 'block_quote') {
|
|
389
|
+
return 'blockquote';
|
|
390
|
+
}
|
|
391
|
+
if (node.type === 'thematic_break') {
|
|
392
|
+
return 'break';
|
|
393
|
+
}
|
|
394
|
+
return node.type;
|
|
395
|
+
}
|
|
396
|
+
extractTomlStructures(root, content, filePath, elements) {
|
|
397
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
398
|
+
const node = root.namedChild(i);
|
|
399
|
+
if (!node)
|
|
400
|
+
continue;
|
|
401
|
+
if (node.type === 'table' || node.type === 'table_array_element' || node.type === 'pair') {
|
|
402
|
+
// Legacy parser uses namedChild instead of childForFieldName
|
|
403
|
+
const keyNode = node.childForFieldName?.('name')
|
|
404
|
+
|| node.childForFieldName?.('key')
|
|
405
|
+
|| node.namedChild(0);
|
|
406
|
+
const name = keyNode ? content.slice(keyNode.startIndex, keyNode.endIndex) : node.type;
|
|
407
|
+
const structure = this.mapStructureNode(name, node, content, filePath);
|
|
408
|
+
if (structure.code.length >= this.options.minStructureSize) {
|
|
409
|
+
elements.functions.push(structure);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
182
414
|
mapTypeNode(node, content, filePath) {
|
|
183
415
|
const nameNode = node.childForFieldName('name');
|
|
184
416
|
if (!nameNode)
|
|
@@ -251,7 +483,7 @@ export default class SemanticSplitter {
|
|
|
251
483
|
id: `${filePath}:${func.name}:${func.startLine}`,
|
|
252
484
|
name: func.name,
|
|
253
485
|
filePath,
|
|
254
|
-
type: 'function',
|
|
486
|
+
type: func.category === 'structure' ? 'structure' : 'function',
|
|
255
487
|
subtype: func.type,
|
|
256
488
|
code: chunkCode,
|
|
257
489
|
startLine: func.startLine,
|
|
@@ -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);
|