ucn 3.6.28 → 3.7.1

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.
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: ucn
3
- description: "Code relationship analyzer (callers, call trees, impact, dead code) via tree-sitter AST. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on Python, JS/TS, Go, Rust, Java. Skip for plain text search or codebases under 500 LOC."
3
+ description: "Code relationship analyzer (callers, call trees, impact, dead code) via tree-sitter AST. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on JS/TS, Python, Go, Rust, Java, HTML. Skip for plain text search or codebases under 500 LOC."
4
4
  allowed-tools: Bash(ucn *), Bash(npx ucn *)
5
5
  argument-hint: "[command] [symbol-name] [--flags]"
6
6
  ---
7
7
 
8
8
  # UCN — Universal Code Navigator
9
9
 
10
- Understands code structure via tree-sitter ASTs: who calls what, what breaks if you change something, full call trees, dead code. Works on Python, JS/TS, Go, Rust, Java.
10
+ Understands code structure via tree-sitter ASTs: who calls what, what breaks if you change something, full call trees, dead code. Works on JS/TS, Python, Go, Rust, Java. Also parses HTML files (inline scripts and event handlers).
11
11
 
12
12
  ## When to Reach for UCN Instead of Grep/Read
13
13
 
@@ -23,7 +23,7 @@ Understands code structure via tree-sitter ASTs: who calls what, what breaks if
23
23
 
24
24
  - Searching for a string literal, error message, TODO, or config value
25
25
  - The codebase is under 500 LOC — just read the files
26
- - Language not supported (only Python, JS/TS, Go, Rust, Java)
26
+ - Language not supported (only JS/TS, Python, Go, Rust, Java, HTML)
27
27
  - Finding files by name — use glob
28
28
 
29
29
  ## The 5 Commands You'll Use Most
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  UCN is designed to work with large files and codebases, helping AI agents ingest exactly the data they need. Its surgical output discourages agents from cutting corners, and without UCN, agents working with large codebases tend to skip parts of the code structure, assuming they have "enough data."
4
4
 
5
+ Supported languages: JS/TS, Python, Go, Rust, Java. Also parses HTML files (inline scripts and event handlers).
6
+
5
7
  ---
6
8
 
7
9
  ## Three Ways to Use UCN
@@ -91,15 +93,10 @@ Instead of reading entire files, ask precise questions:
91
93
 
92
94
  tree-sitter AST
93
95
 
94
- ┌───────────┼───────────┐
95
-
96
- ┌───┴───┐ ┌────┴────┐ ┌───┴──┐
97
- │ JS/TS │ │ Python │ │ Go │
98
- └───────┘ └─────────┘ └──────┘
99
- ┌───────────┼───────────┐
100
- ┌───┴───┐ ┌────┴───┐
101
- │ Rust │ │ Java │
102
- └───────┘ └────────┘
96
+ ┌───────────────────┴─────────────────┐
97
+ Supported Languages
98
+ │ JS/TS, Python, Go, Rust, Java, HTML │
99
+ └─────────────────────────────────────┘
103
100
  ```
104
101
 
105
102
  No cloud. No API keys. Parses locally, stays local.
@@ -502,7 +499,7 @@ ucn toc # Project overview
502
499
  │ Limitation │ What happens │
503
500
  ├──────────────────────────┼──────────────────────────────────────────┤
504
501
  │ │ │
505
- │ 5 languages only │ JS/TS, Python, Go, Rust, Java. │
502
+ │ 5 languages + HTML │ JS/TS, Python, Go, Rust, Java. │
506
503
  │ (no C, Ruby, PHP, etc.) │ Agents fall back to grep for the rest. │
507
504
  │ │ UCN complements, doesn't replace. │
508
505
  │ │ │
package/cli/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
- const { parse, parseFile, extractFunction, extractClass, detectLanguage, isSupported } = require('../core/parser');
13
+ const { parse, parseFile, extractFunction, extractClass, cleanHtmlScriptTags, detectLanguage, isSupported } = require('../core/parser');
14
14
  const { getParser, getLanguageModule } = require('../languages');
15
15
  const { ProjectIndex } = require('../core/project');
16
16
  const { expandGlob, findProjectRoot, isTestFile } = require('../core/discovery');
@@ -823,6 +823,7 @@ function runProjectCommand(rootDir, command, arg) {
823
823
  case 'smart': {
824
824
  requireArg(arg, 'Usage: ucn . smart <name>');
825
825
  const smart = index.smart(arg, {
826
+ file: flags.file,
826
827
  withTypes: flags.withTypes,
827
828
  includeMethods: flags.includeMethods,
828
829
  includeUncertain: flags.includeUncertain
@@ -1069,7 +1070,8 @@ function extractFunctionFromProject(index, name) {
1069
1070
  const m = matches[i];
1070
1071
  const code = fs.readFileSync(m.file, 'utf-8');
1071
1072
  const lines = code.split('\n');
1072
- const fnCode = lines.slice(m.startLine - 1, m.endLine).join('\n');
1073
+ const extracted = lines.slice(m.startLine - 1, m.endLine);
1074
+ const fnCode = cleanHtmlScriptTags(extracted, detectLanguage(m.file)).join('\n');
1073
1075
  if (i > 0) console.log('');
1074
1076
  if (flags.json) {
1075
1077
  console.log(output.formatFunctionJson(m, fnCode));
@@ -1093,7 +1095,8 @@ function extractFunctionFromProject(index, name) {
1093
1095
  // Extract code directly using symbol index location (works for class methods and overloads)
1094
1096
  const code = fs.readFileSync(match.file, 'utf-8');
1095
1097
  const lines = code.split('\n');
1096
- const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
1098
+ const extracted = lines.slice(match.startLine - 1, match.endLine);
1099
+ const fnCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
1097
1100
 
1098
1101
  if (flags.json) {
1099
1102
  console.log(output.formatFunctionJson(match, fnCode));
@@ -1118,7 +1121,8 @@ function extractClassFromProject(index, name) {
1118
1121
  const m = matches[i];
1119
1122
  const code = fs.readFileSync(m.file, 'utf-8');
1120
1123
  const codeLines = code.split('\n');
1121
- const clsCode = codeLines.slice(m.startLine - 1, m.endLine).join('\n');
1124
+ const extracted = codeLines.slice(m.startLine - 1, m.endLine);
1125
+ const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(m.file)).join('\n');
1122
1126
  if (i > 0) console.log('');
1123
1127
  if (flags.json) {
1124
1128
  console.log(JSON.stringify({ ...m, code: clsCode }, null, 2));
@@ -1142,7 +1146,8 @@ function extractClassFromProject(index, name) {
1142
1146
  // Use index data directly instead of re-parsing the file
1143
1147
  const code = fs.readFileSync(match.file, 'utf-8');
1144
1148
  const codeLines = code.split('\n');
1145
- const clsCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
1149
+ const extracted = codeLines.slice(match.startLine - 1, match.endLine);
1150
+ const clsCode = cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
1146
1151
 
1147
1152
  if (flags.json) {
1148
1153
  console.log(JSON.stringify({ ...match, code: clsCode }, null, 2));
@@ -1673,7 +1678,7 @@ function isCommentOrString(line) {
1673
1678
  function printUsage() {
1674
1679
  console.log(`UCN - Universal Code Navigator
1675
1680
 
1676
- Supported: JavaScript, TypeScript, Python, Go, Rust, Java
1681
+ Supported: JavaScript, TypeScript, Python, Go, Rust, Java, HTML
1677
1682
 
1678
1683
  Usage:
1679
1684
  ucn [command] [args] Project mode (current directory)
@@ -1928,7 +1933,7 @@ function executeInteractiveCommand(index, command, arg) {
1928
1933
  console.log('Usage: smart <name>');
1929
1934
  return;
1930
1935
  }
1931
- const smart = index.smart(arg, { includeUncertain: flags.includeUncertain });
1936
+ const smart = index.smart(arg, { file: flags.file, includeUncertain: flags.includeUncertain });
1932
1937
  if (smart) {
1933
1938
  console.log(output.formatSmart(smart, {
1934
1939
  uncertainHint: 'use --include-uncertain to include all'
package/core/discovery.js CHANGED
@@ -389,7 +389,7 @@ function detectProjectPattern(projectRoot) {
389
389
  // Helper to check for config files in a directory
390
390
  const checkDir = (dir) => {
391
391
  if (fs.existsSync(path.join(dir, 'package.json'))) {
392
- extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs');
392
+ extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'html', 'htm');
393
393
  }
394
394
 
395
395
  if (fs.existsSync(path.join(dir, 'pyproject.toml')) ||
@@ -433,7 +433,7 @@ function detectProjectPattern(projectRoot) {
433
433
  return `**/*.{${unique.join(',')}}`;
434
434
  }
435
435
 
436
- return '**/*.{js,jsx,ts,tsx,py,go,java,rs,rb,php,c,cpp,h,hpp}';
436
+ return '**/*.{js,jsx,ts,tsx,py,go,java,rs,rb,php,c,cpp,h,hpp,html,htm}';
437
437
  }
438
438
 
439
439
  /**
package/core/parser.js CHANGED
@@ -128,7 +128,8 @@ function extractFunction(code, language, name) {
128
128
  return { fn: null, code: '' };
129
129
  }
130
130
 
131
- const fnCode = lines.slice(fn.startLine - 1, fn.endLine).join('\n');
131
+ const extracted = lines.slice(fn.startLine - 1, fn.endLine);
132
+ const fnCode = cleanHtmlScriptTags(extracted, language).join('\n');
132
133
  return { fn, code: fnCode };
133
134
  }
134
135
 
@@ -148,7 +149,8 @@ function extractClass(code, language, name) {
148
149
  return { cls: null, code: '' };
149
150
  }
150
151
 
151
- const clsCode = lines.slice(cls.startLine - 1, cls.endLine).join('\n');
152
+ const extracted = lines.slice(cls.startLine - 1, cls.endLine);
153
+ const clsCode = cleanHtmlScriptTags(extracted, language).join('\n');
152
154
  return { cls, code: clsCode };
153
155
  }
154
156
 
@@ -266,6 +268,22 @@ function getExportedSymbols(result) {
266
268
  return exported;
267
269
  }
268
270
 
271
+ /**
272
+ * Strip <script> and </script> tags from extracted code lines for HTML files.
273
+ * Only affects the first and last lines when they contain script tags alongside JS code.
274
+ * @param {string[]} lines - Extracted lines
275
+ * @param {string} language - Language name
276
+ * @returns {string[]} Cleaned lines (same array mutated)
277
+ */
278
+ function cleanHtmlScriptTags(lines, language) {
279
+ if (language === 'html' && lines.length > 0) {
280
+ lines[0] = lines[0].replace(/^(\s*)<script[^>]*>/i, '$1');
281
+ const last = lines.length - 1;
282
+ lines[last] = lines[last].replace(/<\/script>\s*$/i, '');
283
+ }
284
+ return lines;
285
+ }
286
+
269
287
  module.exports = {
270
288
  parse,
271
289
  parseFile,
@@ -274,6 +292,7 @@ module.exports = {
274
292
  getToc,
275
293
  findSymbol,
276
294
  getExportedSymbols,
295
+ cleanHtmlScriptTags,
277
296
  detectLanguage,
278
297
  isSupported
279
298
  };
package/core/project.js CHANGED
@@ -11,7 +11,7 @@ const crypto = require('crypto');
11
11
  const { execSync } = require('child_process');
12
12
  const { expandGlob, findProjectRoot, detectProjectPattern, isTestFile, parseGitignore, DEFAULT_IGNORES } = require('./discovery');
13
13
  const { extractImports, extractExports, resolveImport } = require('./imports');
14
- const { parseFile } = require('./parser');
14
+ const { parseFile, cleanHtmlScriptTags } = require('./parser');
15
15
  const { detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS, safeParse } = require('../languages');
16
16
  const { getTokenTypeAtPosition } = require('../languages/utils');
17
17
 
@@ -1881,7 +1881,9 @@ class ProjectIndex {
1881
1881
  try {
1882
1882
  const content = fs.readFileSync(symbol.file, 'utf-8');
1883
1883
  const lines = content.split('\n');
1884
- return lines.slice(symbol.startLine - 1, symbol.endLine).join('\n');
1884
+ const extracted = lines.slice(symbol.startLine - 1, symbol.endLine);
1885
+ cleanHtmlScriptTags(extracted, detectLanguage(symbol.file));
1886
+ return extracted.join('\n');
1885
1887
  } catch (e) {
1886
1888
  return '';
1887
1889
  }
@@ -2605,11 +2607,26 @@ class ProjectIndex {
2605
2607
  const language = detectLanguage(filePath);
2606
2608
  if (!language) continue;
2607
2609
 
2608
- const parser = getParser(language);
2609
- if (!parser) continue;
2610
-
2611
2610
  const content = fs.readFileSync(filePath, 'utf-8');
2612
- const tree = parser.parse(content, undefined, PARSE_OPTIONS);
2611
+
2612
+ // For HTML files, parse the virtual JS content instead of raw HTML
2613
+ // (HTML tree-sitter sees script content as raw_text, not JS identifiers)
2614
+ let tree;
2615
+ if (language === 'html') {
2616
+ const htmlModule = getLanguageModule('html');
2617
+ const htmlParser = getParser('html');
2618
+ const jsParser = getParser('javascript');
2619
+ const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
2620
+ if (blocks.length === 0 && !htmlModule.extractEventHandlerCalls) continue;
2621
+ if (blocks.length > 0) {
2622
+ const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
2623
+ tree = jsParser.parse(virtualJS, undefined, PARSE_OPTIONS);
2624
+ }
2625
+ } else {
2626
+ const parser = getParser(language);
2627
+ if (!parser) continue;
2628
+ tree = parser.parse(content, undefined, PARSE_OPTIONS);
2629
+ }
2613
2630
 
2614
2631
  // Collect all identifiers from this file in one pass
2615
2632
  const traverse = (node) => {
@@ -2634,7 +2651,25 @@ class ProjectIndex {
2634
2651
  traverse(node.child(i));
2635
2652
  }
2636
2653
  };
2637
- traverse(tree.rootNode);
2654
+ if (tree) traverse(tree.rootNode);
2655
+
2656
+ // For HTML files, also extract identifiers from event handler attributes
2657
+ // (onclick="foo()" etc. — these are in HTML, not in <script> blocks)
2658
+ if (language === 'html') {
2659
+ const htmlModule = getLanguageModule('html');
2660
+ const htmlParser = getParser('html');
2661
+ const handlerCalls = htmlModule.extractEventHandlerCalls(content, htmlParser);
2662
+ for (const call of handlerCalls) {
2663
+ if (!usageIndex.has(call.name)) {
2664
+ usageIndex.set(call.name, []);
2665
+ }
2666
+ usageIndex.get(call.name).push({
2667
+ file: filePath,
2668
+ line: call.line,
2669
+ relativePath: fileEntry.relativePath
2670
+ });
2671
+ }
2672
+ }
2638
2673
  } catch (e) {
2639
2674
  // Skip files that can't be processed
2640
2675
  }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * languages/html.js - HTML file support via inline <script> extraction
3
+ *
4
+ * Parses HTML with tree-sitter-html, extracts inline JavaScript from <script> blocks,
5
+ * builds a line-preserving virtual JS string, and delegates to javascript.js for analysis.
6
+ * Line numbers are automatically correct because the virtual JS has the same line count
7
+ * as the original HTML file, with empty lines for non-script content.
8
+ */
9
+
10
+ const { getParser, getLanguageModule } = require('./index');
11
+
12
+ // Script type values that indicate JavaScript content
13
+ const JS_TYPES = new Set([
14
+ 'text/javascript',
15
+ 'application/javascript',
16
+ 'module',
17
+ ''
18
+ ]);
19
+
20
+ /**
21
+ * Extract inline script blocks from HTML using tree-sitter-html AST.
22
+ * Skips external scripts (src=...) and non-JS types (application/json, importmap, etc.).
23
+ *
24
+ * @param {string} htmlContent - Raw HTML source
25
+ * @param {object} htmlParser - tree-sitter parser configured for HTML
26
+ * @returns {Array<{text: string, startRow: number, startCol: number}>}
27
+ */
28
+ function extractScriptBlocks(htmlContent, htmlParser) {
29
+ const { safeParse, getParseOptions } = require('./index');
30
+ const tree = safeParse(htmlParser, htmlContent, undefined, getParseOptions(htmlContent.length));
31
+ const blocks = [];
32
+
33
+ // Walk the AST looking for script_element nodes
34
+ const visit = (node) => {
35
+ if (node.type === 'script_element') {
36
+ const startTag = node.childForFieldName('start_tag') ||
37
+ node.children.find(c => c.type === 'start_tag');
38
+ const rawText = node.children.find(c => c.type === 'raw_text');
39
+
40
+ if (!startTag || !rawText || !rawText.text) return;
41
+
42
+ // Check attributes on start_tag
43
+ let hasSrc = false;
44
+ let typeValue = null;
45
+
46
+ for (let i = 0; i < startTag.childCount; i++) {
47
+ const attr = startTag.child(i);
48
+ if (attr.type !== 'attribute') continue;
49
+
50
+ const nameNode = attr.children.find(c => c.type === 'attribute_name');
51
+ if (!nameNode) continue;
52
+ const attrName = nameNode.text.toLowerCase();
53
+
54
+ if (attrName === 'src') {
55
+ hasSrc = true;
56
+ break;
57
+ }
58
+
59
+ if (attrName === 'type') {
60
+ const valueNode = attr.children.find(c =>
61
+ c.type === 'quoted_attribute_value' || c.type === 'attribute_value'
62
+ );
63
+ if (valueNode) {
64
+ // Extract value text - strip quotes if quoted_attribute_value
65
+ const innerValue = valueNode.type === 'quoted_attribute_value'
66
+ ? valueNode.children.find(c => c.type === 'attribute_value')
67
+ : valueNode;
68
+ typeValue = innerValue ? innerValue.text.toLowerCase().trim() : '';
69
+ } else {
70
+ typeValue = '';
71
+ }
72
+ }
73
+ }
74
+
75
+ // Skip external scripts
76
+ if (hasSrc) return;
77
+
78
+ // Skip non-JS types
79
+ if (typeValue !== null && !JS_TYPES.has(typeValue)) return;
80
+
81
+ blocks.push({
82
+ text: rawText.text,
83
+ startRow: rawText.startPosition.row,
84
+ startCol: rawText.startPosition.column
85
+ });
86
+ return; // Don't recurse into script_element children
87
+ }
88
+
89
+ for (let i = 0; i < node.childCount; i++) {
90
+ visit(node.child(i));
91
+ }
92
+ };
93
+
94
+ visit(tree.rootNode);
95
+ return blocks;
96
+ }
97
+
98
+ /**
99
+ * Build a virtual JS string with the same line count as the HTML file.
100
+ * Script block lines are placed at their original positions; everything else is empty.
101
+ * The first line of each block is padded with spaces to match its column offset.
102
+ *
103
+ * @param {string} htmlContent - Raw HTML source
104
+ * @param {Array<{text: string, startRow: number, startCol: number}>} blocks
105
+ * @returns {string}
106
+ */
107
+ function buildVirtualJSContent(htmlContent, blocks) {
108
+ const totalLines = htmlContent.split('\n').length;
109
+ const lines = new Array(totalLines).fill('');
110
+
111
+ for (const block of blocks) {
112
+ const blockLines = block.text.split('\n');
113
+ for (let i = 0; i < blockLines.length; i++) {
114
+ const row = block.startRow + i;
115
+ if (row >= totalLines) break;
116
+ if (i === 0 && block.startCol > 0) {
117
+ // Pad first line to match column offset
118
+ lines[row] = ' '.repeat(block.startCol) + blockLines[i];
119
+ } else {
120
+ lines[row] = blockLines[i];
121
+ }
122
+ }
123
+ }
124
+
125
+ return lines.join('\n');
126
+ }
127
+
128
+ /**
129
+ * Extract JavaScript from HTML and prepare for JS parsing.
130
+ * Returns null if no inline scripts found.
131
+ *
132
+ * @param {string} htmlContent - Raw HTML source
133
+ * @param {object} htmlParser - tree-sitter parser configured for HTML
134
+ * @returns {{virtualJS: string, jsParser: object, jsModule: object}|null}
135
+ */
136
+ function extractJS(htmlContent, htmlParser) {
137
+ const blocks = extractScriptBlocks(htmlContent, htmlParser);
138
+ if (blocks.length === 0) return null;
139
+
140
+ const virtualJS = buildVirtualJSContent(htmlContent, blocks);
141
+ const jsParser = getParser('javascript');
142
+ const jsModule = getLanguageModule('javascript');
143
+
144
+ return { virtualJS, jsParser, jsModule };
145
+ }
146
+
147
+ /**
148
+ * Extract function calls from HTML event handler attributes (onclick, onchange, etc.).
149
+ * Walks the HTML AST for elements with on* attributes, extracts function names
150
+ * from the attribute values using regex, and returns call objects.
151
+ *
152
+ * @param {string} htmlContent - Raw HTML source
153
+ * @param {object} htmlParser - tree-sitter parser configured for HTML
154
+ * @returns {Array<{name: string, line: number, isMethod: boolean, enclosingFunction: null, uncertain: boolean, isEventHandler: boolean}>}
155
+ */
156
+ function extractEventHandlerCalls(htmlContent, htmlParser) {
157
+ const { safeParse, getParseOptions } = require('./index');
158
+ const tree = safeParse(htmlParser, htmlContent, undefined, getParseOptions(htmlContent.length));
159
+ const calls = [];
160
+
161
+ const JS_KEYWORDS = new Set([
162
+ 'if', 'for', 'while', 'switch', 'catch', 'function', 'return',
163
+ 'typeof', 'void', 'delete', 'new', 'throw', 'class', 'const',
164
+ 'let', 'var', 'true', 'false', 'null', 'undefined', 'this'
165
+ ]);
166
+
167
+ const visit = (node) => {
168
+ // Skip script elements — their content is handled separately
169
+ if (node.type === 'script_element') return;
170
+
171
+ if (node.type === 'attribute') {
172
+ const nameNode = node.children.find(c => c.type === 'attribute_name');
173
+ if (!nameNode || !nameNode.text.toLowerCase().startsWith('on')) {
174
+ for (let i = 0; i < node.childCount; i++) visit(node.child(i));
175
+ return;
176
+ }
177
+
178
+ const valueNode = node.children.find(c =>
179
+ c.type === 'quoted_attribute_value' || c.type === 'attribute_value'
180
+ );
181
+ if (!valueNode) return;
182
+
183
+ let valueText;
184
+ if (valueNode.type === 'quoted_attribute_value') {
185
+ const inner = valueNode.children.find(c => c.type === 'attribute_value');
186
+ valueText = inner ? inner.text : '';
187
+ } else {
188
+ valueText = valueNode.text;
189
+ }
190
+ if (!valueText) return;
191
+
192
+ const line = nameNode.startPosition.row + 1; // 1-indexed
193
+
194
+ // Extract standalone function calls (not method calls like obj.method())
195
+ const regex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
196
+ let match;
197
+ while ((match = regex.exec(valueText)) !== null) {
198
+ const fnName = match[1];
199
+ if (JS_KEYWORDS.has(fnName)) continue;
200
+ // Skip if preceded by dot (method call on object)
201
+ if (match.index > 0 && valueText[match.index - 1] === '.') continue;
202
+
203
+ calls.push({
204
+ name: fnName,
205
+ line,
206
+ isMethod: false,
207
+ enclosingFunction: null,
208
+ uncertain: false,
209
+ isEventHandler: true
210
+ });
211
+ }
212
+ return;
213
+ }
214
+
215
+ for (let i = 0; i < node.childCount; i++) visit(node.child(i));
216
+ };
217
+
218
+ visit(tree.rootNode);
219
+ return calls;
220
+ }
221
+
222
+ // ── Exported language module interface ──────────────────────────────────────
223
+
224
+ function parse(code, parser) {
225
+ const result = extractJS(code, parser);
226
+ if (!result) {
227
+ return {
228
+ language: 'html',
229
+ totalLines: code.split('\n').length,
230
+ functions: [],
231
+ classes: [],
232
+ stateObjects: [],
233
+ imports: [],
234
+ exports: []
235
+ };
236
+ }
237
+
238
+ const jsResult = result.jsModule.parse(result.virtualJS, result.jsParser);
239
+ jsResult.language = 'html';
240
+ jsResult.totalLines = code.split('\n').length;
241
+ return jsResult;
242
+ }
243
+
244
+ function findFunctions(code, parser) {
245
+ const result = extractJS(code, parser);
246
+ if (!result) return [];
247
+ return result.jsModule.findFunctions(result.virtualJS, result.jsParser);
248
+ }
249
+
250
+ function findClasses(code, parser) {
251
+ const result = extractJS(code, parser);
252
+ if (!result) return [];
253
+ return result.jsModule.findClasses(result.virtualJS, result.jsParser);
254
+ }
255
+
256
+ function findStateObjects(code, parser) {
257
+ const result = extractJS(code, parser);
258
+ if (!result) return [];
259
+ return result.jsModule.findStateObjects(result.virtualJS, result.jsParser);
260
+ }
261
+
262
+ function findCallsInCode(code, parser) {
263
+ const scriptCalls = (() => {
264
+ const result = extractJS(code, parser);
265
+ if (!result) return [];
266
+ return result.jsModule.findCallsInCode(result.virtualJS, result.jsParser);
267
+ })();
268
+ const handlerCalls = extractEventHandlerCalls(code, parser);
269
+ if (handlerCalls.length === 0) return scriptCalls;
270
+ return scriptCalls.concat(handlerCalls);
271
+ }
272
+
273
+ function findCallbackUsages(code, name, parser) {
274
+ const result = extractJS(code, parser);
275
+ if (!result) return [];
276
+ return result.jsModule.findCallbackUsages(result.virtualJS, name, result.jsParser);
277
+ }
278
+
279
+ function findReExports(code, parser) {
280
+ const result = extractJS(code, parser);
281
+ if (!result) return [];
282
+ return result.jsModule.findReExports(result.virtualJS, result.jsParser);
283
+ }
284
+
285
+ function findImportsInCode(code, parser) {
286
+ const result = extractJS(code, parser);
287
+ if (!result) return [];
288
+ return result.jsModule.findImportsInCode(result.virtualJS, result.jsParser);
289
+ }
290
+
291
+ function findExportsInCode(code, parser) {
292
+ const result = extractJS(code, parser);
293
+ if (!result) return [];
294
+ return result.jsModule.findExportsInCode(result.virtualJS, result.jsParser);
295
+ }
296
+
297
+ function findUsagesInCode(code, name, parser) {
298
+ const result = extractJS(code, parser);
299
+ if (!result) return [];
300
+ return result.jsModule.findUsagesInCode(result.virtualJS, name, result.jsParser);
301
+ }
302
+
303
+ module.exports = {
304
+ parse,
305
+ findFunctions,
306
+ findClasses,
307
+ findStateObjects,
308
+ findCallsInCode,
309
+ findCallbackUsages,
310
+ findReExports,
311
+ findImportsInCode,
312
+ findExportsInCode,
313
+ findUsagesInCode,
314
+ // Exported for testing
315
+ extractScriptBlocks,
316
+ buildVirtualJSContent,
317
+ extractEventHandlerCalls
318
+ };
@@ -55,6 +55,12 @@ const LANGUAGES = {
55
55
  extensions: ['.java'],
56
56
  treeSitterLang: 'java',
57
57
  module: () => require('./java')
58
+ },
59
+ html: {
60
+ name: 'html',
61
+ extensions: ['.html', '.htm'],
62
+ treeSitterLang: 'html',
63
+ module: () => require('./html')
58
64
  }
59
65
  };
60
66
 
@@ -124,6 +130,9 @@ function getParser(language) {
124
130
  case 'rust':
125
131
  lang = require('tree-sitter-rust');
126
132
  break;
133
+ case 'html':
134
+ lang = require('tree-sitter-html');
135
+ break;
127
136
  default:
128
137
  throw new Error(`No tree-sitter grammar for: ${language}`);
129
138
  }
@@ -238,9 +247,12 @@ function safeParse(parser, content, oldTree = undefined, options = {}) {
238
247
  } catch (e) {
239
248
  lastError = e;
240
249
  // Only retry on buffer-related errors
241
- if (!e.message?.toLowerCase().includes('buffer') &&
242
- !e.message?.toLowerCase().includes('memory') &&
243
- !e.message?.toLowerCase().includes('alloc')) {
250
+ // tree-sitter throws "Invalid argument" when buffer is too small
251
+ const msg = e.message?.toLowerCase() || '';
252
+ if (!msg.includes('buffer') &&
253
+ !msg.includes('memory') &&
254
+ !msg.includes('alloc') &&
255
+ !msg.includes('invalid argument')) {
244
256
  throw e; // Non-buffer error, don't retry
245
257
  }
246
258
  // Continue to next buffer size
package/mcp/server.js CHANGED
@@ -169,7 +169,7 @@ function requireName(name) {
169
169
  // CONSOLIDATED TOOL REGISTRATION
170
170
  // ============================================================================
171
171
 
172
- const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies — across JavaScript/TypeScript, Python, Go, Rust, and Java. Use instead of grep/read for code relationships.
172
+ const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies — across JavaScript/TypeScript, Python, Go, Rust, Java, and HTML (inline scripts and event handlers). Use instead of grep/read for code relationships.
173
173
 
174
174
  TOP 5 (covers 90% of tasks): about, impact, trace, find, deadcode
175
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.6.28",
3
+ "version": "3.7.1",
4
4
  "description": "Universal Code Navigator — function relationships, call trees, and impact analysis across large codebases without reading entire files.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "go",
22
22
  "rust",
23
23
  "java",
24
+ "html",
24
25
  "ai",
25
26
  "agent"
26
27
  ],
@@ -34,6 +35,7 @@
34
35
  "dependencies": {
35
36
  "tree-sitter": "^0.21.0",
36
37
  "tree-sitter-go": "^0.21.0",
38
+ "tree-sitter-html": "^0.23.0",
37
39
  "tree-sitter-java": "^0.21.0",
38
40
  "tree-sitter-javascript": "^0.21.0",
39
41
  "tree-sitter-python": "^0.21.0",