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 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';
@@ -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.map(t => {
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.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../templates/TOOLS.md'), 'utf8')}
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.
@@ -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.',
@@ -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: this.extractImports(root, content, relativePath)
121
+ imports: []
88
122
  };
89
- // Traverse AST for functions and types
90
- this.traverse(root, content, relativePath, elements);
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 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);