🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

pi-read-map

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pi-read-map - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+49
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
## [1.2.0] - 2026-02-14
### Added
- **Clojure mapper** — tree-sitter-based parser for `.clj`, `.cljs`, `.cljc`, and `.edn` files. Extracts `ns`, `defn`, `defn-`, `def`, `defonce`, `defmacro`, `defmulti`, `defmethod`, `defprotocol`, `defrecord`, and `deftype` forms with docstrings, signatures, modifiers, and protocol method children. Supports reader conditionals (`#?`) with per-platform annotations. Contributed by [Baishampayan Ghose](https://github.com/ghoseb). ([#2](https://github.com/Whamp/pi-read-map/pull/2))
- Clojure demo asset (`clojure/core.clj` from the official Clojure repo)
### Changed
- Tree-sitter tests use `describe.runIf` for conditional execution
## [1.1.0] - 2026-02-10
### Added
- Docstring extraction (`FileSymbol.docstring`) across all mappers
- Export flag (`FileSymbol.isExported`) across all mappers
- Required imports (`FileMap.imports`) across all mappers
- Skipped read recovery — detects reads cancelled by steering queue and re-issues them
- JSONL session-aware maps for pi session files
- Directory read handling with EISDIR error and `ls` fallback
### Fixed
- Symbol duplication in file map output
- oxlint warnings and errors resolved across codebase
### Changed
- Test helpers refactored; `models.ts` renamed to `constants.ts`
## [1.0.0] - 2026-02-09
Initial release.
### Added
- Structural file maps for large files (>2,000 lines or >50 KB)
- 14 language mappers: TypeScript, JavaScript, Python, Go, Rust, C, C++, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown
- Budget-aware formatting with progressive detail reduction (10 KB full → 100 KB truncated)
- In-memory caching by file path and modification time
- Fallback chain: language mapper → universal-ctags → grep
- Custom `file-map` messages delivered after `tool_result` events
- E2E test infrastructure via tmux
- Demo assets from 10 major open-source projects
import { readFile, stat } from "node:fs/promises";
/**
* Clojure mapper using tree-sitter for AST extraction.
*
* The tree-sitter-clojure grammar parses at the S-expression level:
* all forms are `list_lit` nodes. We identify def forms by matching
* the first `sym_lit` child against known Clojure special forms.
*/
import { createRequire } from "node:module";
import type { FileMap, FileSymbol } from "../types.js";
import { DetailLevel, SymbolKind } from "../enums.js";
type SyntaxNode = import("tree-sitter").SyntaxNode;
/**
* Def forms we recognize and how they map to symbol kinds.
*/
const DEF_FORMS: Record<
string,
{ kind: SymbolKind; isPrivate?: boolean; hasChildren?: boolean }
> = {
defn: { kind: SymbolKind.Function },
"defn-": { kind: SymbolKind.Function, isPrivate: true },
def: { kind: SymbolKind.Variable },
defonce: { kind: SymbolKind.Variable },
defmacro: { kind: SymbolKind.Function },
defmulti: { kind: SymbolKind.Function },
defmethod: { kind: SymbolKind.Method },
defprotocol: { kind: SymbolKind.Interface, hasChildren: true },
defrecord: { kind: SymbolKind.Class },
deftype: { kind: SymbolKind.Class },
};
// Lazy-loaded parser
let parser: import("tree-sitter") | null = null;
let parserInitialized = false;
function ensureWritableTypeProperty(parserCtor: unknown): void {
const syntaxNode = (parserCtor as { SyntaxNode?: { prototype?: object } })
.SyntaxNode;
const proto = syntaxNode?.prototype;
if (!proto) {
return;
}
const desc = Object.getOwnPropertyDescriptor(proto, "type");
if (!desc || desc.set) {
return;
}
Object.defineProperty(proto, "type", { ...desc, set: () => {} });
}
function getParser(): import("tree-sitter") | null {
if (parserInitialized) {
return parser;
}
parserInitialized = true;
const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
if (isBun) {
return null;
}
try {
const require = createRequire(import.meta.url);
const ParserCtor = require("tree-sitter") as typeof import("tree-sitter");
const Clojure =
require("tree-sitter-clojure") as import("tree-sitter").Language;
ensureWritableTypeProperty(ParserCtor);
parser = new ParserCtor();
parser.setLanguage(Clojure);
return parser;
} catch {
return null;
}
}
function getNodeText(node: SyntaxNode, source: string): string {
return source.slice(node.startIndex, node.endIndex);
}
/**
* Get the text of a sym_lit's sym_name child.
*/
function getSymName(node: SyntaxNode, source: string): string | null {
if (node.type !== "sym_lit") {
return null;
}
const nameNode = node.namedChildren.find((c) => c.type === "sym_name");
if (!nameNode) {
return null;
}
return getNodeText(nameNode, source);
}
/**
* Check if a sym_lit has ^:private metadata.
*/
function hasPrivateMeta(node: SyntaxNode): boolean {
return node.namedChildren.some(
(c) =>
c.type === "meta_lit" &&
c.namedChildren.some((v) => v.type === "kwd_lit" && v.text === ":private")
);
}
/**
* Extract the string content from a str_lit node (strips surrounding quotes).
*/
function extractString(node: SyntaxNode, source: string): string {
const text = getNodeText(node, source);
return text.slice(1, -1);
}
/**
* Extract the named values from a list_lit (skipping gaps/whitespace).
*/
function getValueChildren(node: SyntaxNode): SyntaxNode[] {
return node.namedChildren.filter(
(c) => c.type !== "comment" && c.type !== "dis_expr"
);
}
/**
* Extract protocol method signatures from a defprotocol body.
*/
function extractProtocolMethods(
children: SyntaxNode[],
source: string
): FileSymbol[] {
const methods: FileSymbol[] = [];
for (const child of children) {
if (child.type !== "list_lit") {
continue;
}
const values = getValueChildren(child);
const [firstValue] = values;
if (!firstValue || firstValue.type !== "sym_lit") {
continue;
}
const name = getSymName(firstValue, source);
if (!name) {
continue;
}
const paramsNode = values.find((v) => v.type === "vec_lit");
const params = paramsNode ? getNodeText(paramsNode, source) : "";
const signature = params ? `(${name} ${params})` : `(${name})`;
const docNode = values.find(
(v, i) =>
v.type === "str_lit" && i > values.indexOf(paramsNode ?? firstValue)
);
methods.push({
name,
kind: SymbolKind.Method,
startLine: child.startPosition.row + 1,
endLine: child.endPosition.row + 1,
signature,
...(docNode ? { docstring: extractString(docNode, source) } : {}),
});
}
return methods;
}
/**
* Extract a defmethod form into an ExtractedDef-like result.
*/
function extractDefmethod(
node: SyntaxNode,
values: SyntaxNode[],
source: string
): FileSymbol | null {
const [, nameNode, dispatchNode] = values;
if (!nameNode || nameNode.type !== "sym_lit") {
return null;
}
const multiName = getSymName(nameNode, source);
if (!multiName) {
return null;
}
const dispatchVal = dispatchNode
? getNodeText(dispatchNode, source)
: "unknown";
const paramsNode = values.find((v) => v.type === "vec_lit");
const params = paramsNode ? getNodeText(paramsNode, source) : "";
const signature = `(defmethod ${multiName} ${dispatchVal} ${params})`;
return {
name: `${multiName} ${dispatchVal}`,
kind: SymbolKind.Method,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
signature,
isExported: true,
};
}
/**
* Extract docstring from values after the name.
*
* For function-like forms (defn, defmacro, defprotocol, defmulti):
* docstring is the first str_lit that appears before any vec_lit or list_lit.
*
* For value forms (def, defonce):
* a str_lit is a docstring only if another value follows it.
* e.g. (def x "doc" 42) → docstring="doc", but (def x "val") → no docstring.
*/
function extractDocstring(
restValues: SyntaxNode[],
source: string,
isValueForm: boolean
): string | undefined {
const firstStr = restValues.find((v) => v.type === "str_lit");
if (!firstStr) {
return undefined;
}
const firstVec = restValues.find((v) => v.type === "vec_lit");
const firstList = restValues.find((v) => v.type === "list_lit");
const strIdx = restValues.indexOf(firstStr);
const vecIdx = firstVec ? restValues.indexOf(firstVec) : Infinity;
const listIdx = firstList ? restValues.indexOf(firstList) : Infinity;
if (strIdx >= vecIdx || strIdx >= listIdx) {
return undefined;
}
// For def/defonce: the string is a docstring only if a value follows it
if (isValueForm) {
const hasValueAfter = restValues.some(
(v, i) => i > strIdx && v.type !== "comment" && v.type !== "dis_expr"
);
if (!hasValueAfter) {
return undefined;
}
}
return extractString(firstStr, source);
}
/**
* Build signature for function-like forms (defn, defn-, defmacro).
*/
function buildFnSignature(
formName: string,
name: string,
restValues: SyntaxNode[],
source: string
): string {
const firstVec = restValues.find((v) => v.type === "vec_lit");
if (firstVec) {
return `(${formName} ${name} ${getNodeText(firstVec, source)})`;
}
// Multi-arity: look for list_lit children starting with vec_lit
const arities = restValues.filter(
(v) =>
v.type === "list_lit" &&
getValueChildren(v).some((c) => c.type === "vec_lit")
);
if (arities.length > 0) {
const arityStrs = arities.map((a) => {
const vec = getValueChildren(a).find((c) => c.type === "vec_lit");
return vec ? getNodeText(vec, source) : "[]";
});
return `(${formName} ${name} ${arityStrs.join(" ")})`;
}
return `(${formName} ${name})`;
}
/**
* Build signature for non-function def forms.
*/
function buildDefSignature(
formName: string,
name: string,
restValues: SyntaxNode[],
source: string
): string {
if (formName === "defmulti") {
const dispatchNode = restValues.find((v) => v.type !== "str_lit");
const dispatch = dispatchNode ? getNodeText(dispatchNode, source) : "";
return dispatch ? `(defmulti ${name} ${dispatch})` : `(defmulti ${name})`;
}
if (formName === "defprotocol") {
return `(defprotocol ${name})`;
}
if (formName === "defrecord" || formName === "deftype") {
const firstVec = restValues.find((v) => v.type === "vec_lit");
return firstVec
? `(${formName} ${name} ${getNodeText(firstVec, source)})`
: `(${formName} ${name})`;
}
// def, defonce
return `(${formName} ${name})`;
}
const FN_FORMS = new Set(["defn", "defn-", "defmacro"]);
/**
* Try to extract a def form from a list_lit node.
*/
function extractDef(node: SyntaxNode, source: string): FileSymbol | null {
if (node.type !== "list_lit") {
return null;
}
const values = getValueChildren(node);
if (values.length < 2) {
return null;
}
const [formNode, nameNode] = values;
if (!formNode || formNode.type !== "sym_lit") {
return null;
}
const formName = getSymName(formNode, source);
if (!formName) {
return null;
}
const defInfo = DEF_FORMS[formName];
if (!defInfo) {
return null;
}
// defmethod has unique structure
if (formName === "defmethod") {
return extractDefmethod(node, values, source);
}
if (!nameNode || nameNode.type !== "sym_lit") {
return null;
}
const name = getSymName(nameNode, source);
if (!name) {
return null;
}
const isPrivate = defInfo.isPrivate === true || hasPrivateMeta(nameNode);
const modifiers: string[] = [];
if (isPrivate) {
modifiers.push("private");
}
if (formName === "defmacro") {
modifiers.push("macro");
}
const restValues = values.slice(2);
const isValueForm = formName === "def" || formName === "defonce";
const docstring = extractDocstring(restValues, source, isValueForm);
const signature = FN_FORMS.has(formName)
? buildFnSignature(formName, name, restValues, source)
: buildDefSignature(formName, name, restValues, source);
// Only defprotocol has extractable method children.
// defrecord/deftype inline protocol methods are not yet extracted.
let children: FileSymbol[] | undefined;
if (defInfo.hasChildren && formName === "defprotocol") {
const extracted = extractProtocolMethods(restValues, source);
children = extracted.length > 0 ? extracted : undefined;
}
const symbol: FileSymbol = {
name,
kind: defInfo.kind,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
isExported: !isPrivate,
};
if (signature) {
symbol.signature = signature;
}
if (docstring) {
symbol.docstring = docstring;
}
if (modifiers.length > 0) {
symbol.modifiers = modifiers;
}
if (children) {
symbol.children = children;
}
return symbol;
}
/**
* Extract ns form for namespace and imports.
*/
function extractNs(
node: SyntaxNode,
source: string
): { namespace: string; imports: string[]; docstring?: string } | null {
if (node.type !== "list_lit") {
return null;
}
const values = getValueChildren(node);
if (values.length < 2) {
return null;
}
const [formNode, nameNode, docNode] = values;
if (!formNode || formNode.type !== "sym_lit") {
return null;
}
if (getSymName(formNode, source) !== "ns") {
return null;
}
if (!nameNode || nameNode.type !== "sym_lit") {
return null;
}
const namespace = getSymName(nameNode, source);
if (!namespace) {
return null;
}
// Docstring at index 2
let docstring: string | undefined;
if (docNode?.type === "str_lit") {
docstring = extractString(docNode, source);
}
// Extract :require and :import clauses
const imports: string[] = [];
for (const child of values) {
if (child.type !== "list_lit") {
continue;
}
const listValues = getValueChildren(child);
const [kwd] = listValues;
if (!kwd || kwd.type !== "kwd_lit") {
continue;
}
const kwdText = getNodeText(kwd, source);
if (kwdText === ":require" || kwdText === ":import") {
for (const spec of listValues.slice(1)) {
// Unwrap reader conditionals inside require/import:
// #?(:clj [clojure.java.io] :cljs [cljs.reader])
// → each platform's specs get added with a platform tag
if (
spec.type === "read_cond_lit" ||
spec.type === "splicing_read_cond_lit"
) {
const rcChildren = getValueChildren(spec);
let platform: string | undefined;
for (const rc of rcChildren) {
if (rc.type === "kwd_lit") {
platform = getNodeText(rc, source);
continue;
}
if (platform) {
imports.push(`${getNodeText(rc, source)} ${platform}`);
platform = undefined;
}
}
} else {
imports.push(getNodeText(spec, source));
}
}
}
}
return { namespace, imports, docstring };
}
/**
* Extract def forms from a reader conditional (#? or #?@).
*
* Reader conditionals contain kwd_lit/form pairs like:
* #?(:clj (defn foo [x] ...) :cljs (defn foo [x] ...))
*
* We extract defs from all platform branches, annotating each with
* its platform keyword as a modifier. When the same name appears in
* multiple branches, all variants are included — the map consumer
* sees the full picture.
*/
function extractReaderConditionalDefs(
node: SyntaxNode,
source: string
): FileSymbol[] {
const results: FileSymbol[] = [];
const children = getValueChildren(node);
// Children alternate: kwd_lit, form, kwd_lit, form, ...
let currentPlatform: string | undefined;
for (const child of children) {
if (child.type === "kwd_lit") {
currentPlatform = getNodeText(child, source);
continue;
}
if (child.type === "list_lit") {
const def = extractDef(child, source);
if (def && currentPlatform) {
const platformName = currentPlatform.replace(/^:/, "");
const platformMod = `platform-${platformName}`;
def.modifiers = def.modifiers
? [...def.modifiers, platformMod]
: [platformMod];
results.push(def);
}
}
}
return results;
}
/**
* Generate a file map for Clojure files using tree-sitter.
*/
export async function clojureMapper(
filePath: string,
signal?: AbortSignal
): Promise<FileMap | null> {
try {
const p = getParser();
if (!p) {
return null;
}
const stats = await stat(filePath);
const totalBytes = stats.size;
const content = await readFile(filePath, "utf8");
if (signal?.aborted) {
return null;
}
let tree: import("tree-sitter").Tree;
try {
tree = p.parse(content);
} catch {
return null;
}
const symbols: FileSymbol[] = [];
const imports: string[] = [];
for (const child of tree.rootNode.namedChildren) {
// Reader conditionals: extract defs from all platform branches
if (
child.type === "read_cond_lit" ||
child.type === "splicing_read_cond_lit"
) {
const condDefs = extractReaderConditionalDefs(child, content);
symbols.push(...condDefs);
continue;
}
if (child.type !== "list_lit") {
continue;
}
const ns = extractNs(child, content);
if (ns) {
imports.push(...ns.imports);
symbols.push({
name: ns.namespace,
kind: SymbolKind.Namespace,
startLine: child.startPosition.row + 1,
endLine: child.endPosition.row + 1,
signature: `(ns ${ns.namespace})`,
...(ns.docstring ? { docstring: ns.docstring } : {}),
isExported: true,
});
continue;
}
const def = extractDef(child, content);
if (def) {
symbols.push(def);
}
}
if (symbols.length === 0) {
return null;
}
const totalLines = content.split("\n").length;
return {
path: filePath,
totalLines,
totalBytes,
language: "Clojure",
symbols,
imports,
detailLevel: DetailLevel.Full,
};
} catch (error) {
if (signal?.aborted) {
return null;
}
console.error(`Clojure mapper failed: ${error}`);
return null;
}
}
+9
-6
# AGENTS.md
> Last updated: 2026-02-10
> Last updated: 2026-02-14
Pi extension that augments the built-in `read` tool with structural file maps for large files (>2,000 lines or >50 KB). Intercepts `read` calls, generates symbol maps via language-specific parsers, and sends them as separate `file-map` messages after the tool result.
## Commands (verified 2026-02-10)
## Commands (verified 2026-02-14)

@@ -33,6 +33,7 @@ | Command | Purpose | ~Time |

├── constants.ts → THRESHOLDS: lines, bytes, budget tiers
└── mappers/ → One mapper per language (16 total)
└── mappers/ → One mapper per language (17 total)
├── typescript.ts → ts-morph (handles TS + JS)
├── rust.ts → tree-sitter-rust
├── cpp.ts → tree-sitter-cpp (C++ and .h files)
├── clojure.ts → tree-sitter-clojure (.clj, .cljs, .cljc, .edn)
├── python.ts → subprocess: scripts/python_outline.py

@@ -62,3 +63,3 @@ ├── go.ts → subprocess: scripts/go_outline.go

├── benchmarks/ → Mapper performance benchmarks
└── helpers/ → Test utilities (pi-runner, constants, models)
└── helpers/ → Test utilities (pi-runner, constants, tree-sitter)

@@ -91,2 +92,3 @@ docs/

| Complex mapper | `src/mappers/typescript.ts` | ts-morph AST walk, nested symbols, modifiers |
| Tree-sitter mapper | `src/mappers/clojure.ts` | tree-sitter AST walk, reader conditionals, platform modifiers |
| Subprocess mapper | `src/mappers/python.ts` | Calls external script, parses JSON output |

@@ -131,4 +133,5 @@ | Unit test | `tests/unit/mappers/csv.test.ts` | Fixture-based, edge cases, null returns |

- `tree-sitter` pinned to 0.22.4 due to peer dependency conflicts (see `docs/todo/upgrade-tree-sitter-0.26.md`)
- `tree-sitter-clojure` pinned to commit SHA from `github:ghoseb/tree-sitter-clojure` (third-party fork)
- Go outline script auto-compiles on first use; compiled binary checked in at `scripts/go_outline`
- Phase 1-4 of implementation plan complete; remaining TODOs in `docs/todo/`
- Phase 1-5 of implementation plan complete; remaining TODOs in `docs/todo/`

@@ -158,3 +161,3 @@ | Docstrings / JSDoc | `FileSymbol.docstring?: string` | First-line summary of doc comments |

- **Linting:** oxlint + oxfmt
- **Parsing:** ts-morph, tree-sitter, regex, subprocess (Python/Go/jq)
- **Parsing:** ts-morph, tree-sitter (rust, cpp, clojure), regex, subprocess (Python/Go/jq)
- **Framework:** pi extension API (`@mariozechner/pi-coding-agent`)
+2
-1
{
"name": "pi-read-map",
"version": "1.1.0",
"version": "1.2.0",
"description": "Pi extension that adds structural file maps for large files",

@@ -47,2 +47,3 @@ "type": "module",

"tree-sitter": "0.22.4",
"tree-sitter-clojure": "github:ghoseb/tree-sitter-clojure#78928e6",
"tree-sitter-cpp": "0.23.4",

@@ -49,0 +50,0 @@ "tree-sitter-rust": "0.23.3",

@@ -26,3 +26,3 @@ # pi-read-map

- **Generates structural maps** showing symbols, classes, functions, and their exact line ranges
- **Supports 16 languages** through specialized parsers: TypeScript, JavaScript, Python, Go, Rust, C, C++, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown
- **Supports 17 languages** through specialized parsers: TypeScript, JavaScript, Python, Go, Rust, C, C++, Clojure, ClojureScript, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown, EDN
- **Extracts structural outlines** — functions, classes, and their line ranges — typically under 1% of file size

@@ -134,2 +134,3 @@ - **Enforces budgets** through progressive detail reduction (10 KB full → 15 KB compact → 20 KB minimal → 50 KB outline → 100 KB hard cap)

├── cpp.ts # tree-sitter for C/C++
├── clojure.ts # tree-sitter for Clojure/ClojureScript/EDN
├── c.ts # Regex patterns

@@ -178,2 +179,3 @@ ├── sql.ts # Regex

- `tree-sitter-rust` - Rust parsing
- `tree-sitter-clojure` - Clojure parsing

@@ -190,4 +192,8 @@ **System tools (optional):**

### Contributors
- [Baishampayan Ghose](https://github.com/ghoseb) — Clojure tree-sitter mapper and [tree-sitter-clojure](https://github.com/ghoseb/tree-sitter-clojure) grammar
## License
MIT

@@ -41,2 +41,8 @@ import { extname } from "node:path";

// Clojure
".clj": { id: "clojure", name: "Clojure" },
".cljs": { id: "clojure", name: "ClojureScript" },
".cljc": { id: "clojure", name: "Clojure" },
".edn": { id: "clojure", name: "EDN" },
// SQL

@@ -43,0 +49,0 @@ ".sql": { id: "sql", name: "SQL" },

@@ -6,2 +6,3 @@ import type { FileMap, MapOptions } from "./types.js";

import { cMapper } from "./mappers/c.js";
import { clojureMapper } from "./mappers/clojure.js";
import { cppMapper } from "./mappers/cpp.js";

@@ -61,2 +62,5 @@ import { csvMapper } from "./mappers/csv.js";

csv: csvMapper,
// Phase 5: Clojure tree-sitter
clojure: clojureMapper,
};

@@ -63,0 +67,0 @@

@@ -22,6 +22,8 @@ /**

/**
* Map ctags kind letters to SymbolKind.
* Map ctags kind identifiers to SymbolKind.
* Handles both single-letter kinds (legacy format) and full-word kinds (JSON format).
* See: https://docs.ctags.io/en/latest/man/ctags.1.html
*/
const CTAGS_KIND_MAP: Record<string, SymbolKind> = {
// Single-letter kinds (legacy/traditional format)
c: SymbolKind.Class,

@@ -48,3 +50,3 @@ d: SymbolKind.Constant, // macro definition

T: SymbolKind.Type, // type
// Language-specific mappings
// Language-specific single-letter mappings
a: SymbolKind.Type, // alias

@@ -58,2 +60,26 @@ b: SymbolKind.Variable, // block (Ruby)

z: SymbolKind.Property, // parameter
// Full-word kinds (JSON output format)
class: SymbolKind.Class,
enum: SymbolKind.Enum,
enumerator: SymbolKind.Enum,
function: SymbolKind.Function,
interface: SymbolKind.Interface,
macro: SymbolKind.Constant,
member: SymbolKind.Property,
method: SymbolKind.Method,
module: SymbolKind.Module,
namespace: SymbolKind.Namespace,
package: SymbolKind.Module,
property: SymbolKind.Property,
struct: SymbolKind.Struct,
type: SymbolKind.Type,
typedef: SymbolKind.Type,
union: SymbolKind.Type,
variable: SymbolKind.Variable,
field: SymbolKind.Property,
constant: SymbolKind.Constant,
prototype: SymbolKind.Function,
alias: SymbolKind.Type,
trait: SymbolKind.Interface,
};

@@ -155,5 +181,6 @@

// Run ctags with JSON output
// Run ctags with JSON output and line numbers
// --output-format=json requires Universal Ctags 5.9+
const cmd = `ctags --output-format=json -f - "${filePath}" 2>/dev/null`;
// --fields=+n ensures line numbers are included in JSON output
const cmd = `ctags --output-format=json --fields=+n -f - "${filePath}" 2>/dev/null`;

@@ -160,0 +187,0 @@ let stdout: string;