pi-read-map 1.0.0

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/src/index.ts ADDED
@@ -0,0 +1,259 @@
1
+ import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
2
+
3
+ import {
4
+ createReadTool,
5
+ DEFAULT_MAX_LINES,
6
+ DEFAULT_MAX_BYTES,
7
+ } from "@mariozechner/pi-coding-agent";
8
+ import { Text } from "@mariozechner/pi-tui";
9
+ import { Type } from "@sinclair/typebox";
10
+ import { exec } from "node:child_process";
11
+ import { stat } from "node:fs/promises";
12
+ import { basename, resolve } from "node:path";
13
+ import { promisify } from "node:util";
14
+
15
+ import type { FileMapMessageDetails } from "./types.js";
16
+
17
+ import { formatFileMapWithBudget } from "./formatter.js";
18
+ import { generateMap, shouldGenerateMap } from "./mapper.js";
19
+
20
+ export type { FileMapMessageDetails } from "./types.js";
21
+
22
+ const execAsync = promisify(exec);
23
+
24
+ // In-memory cache for maps
25
+ const mapCache = new Map<string, { mtime: number; map: string }>();
26
+
27
+ // Pending maps waiting to be sent after tool_result
28
+ const pendingMaps = new Map<
29
+ string,
30
+ {
31
+ path: string;
32
+ map: string;
33
+ details: FileMapMessageDetails;
34
+ }
35
+ >();
36
+
37
+ /**
38
+ * Reset the map cache. Exported for testing purposes only.
39
+ */
40
+ export function resetMapCache(): void {
41
+ mapCache.clear();
42
+ }
43
+
44
+ /**
45
+ * Reset the pending maps. Exported for testing purposes only.
46
+ */
47
+ export function resetPendingMaps(): void {
48
+ pendingMaps.clear();
49
+ }
50
+
51
+ /**
52
+ * Get pending maps for testing inspection.
53
+ */
54
+ export function getPendingMaps(): Map<
55
+ string,
56
+ { path: string; map: string; details: FileMapMessageDetails }
57
+ > {
58
+ return pendingMaps;
59
+ }
60
+
61
+ export default function piReadMapExtension(pi: ExtensionAPI): void {
62
+ // Get the current working directory
63
+ const cwd = process.cwd();
64
+
65
+ // Create the built-in read tool to delegate to
66
+ const builtInRead = createReadTool(cwd);
67
+
68
+ // Register tool_result handler to send pending maps
69
+ pi.on("tool_result", (event, _ctx) => {
70
+ if (event.toolName !== "read") {
71
+ return;
72
+ }
73
+
74
+ const pending = pendingMaps.get(event.toolCallId);
75
+ if (!pending) {
76
+ return;
77
+ }
78
+
79
+ // Send the map as a custom message
80
+ pi.sendMessage({
81
+ customType: "file-map",
82
+ content: pending.map,
83
+ display: true,
84
+ details: pending.details,
85
+ });
86
+
87
+ // Clean up
88
+ pendingMaps.delete(event.toolCallId);
89
+ });
90
+
91
+ // Register custom message renderer for file-map type
92
+ pi.registerMessageRenderer<FileMapMessageDetails>(
93
+ "file-map",
94
+ (message, options, theme: Theme) => {
95
+ const { expanded } = options;
96
+ const { details } = message;
97
+
98
+ if (expanded) {
99
+ // Expanded: show full formatted map
100
+ // message.content can be string or array of content blocks
101
+ const content =
102
+ typeof message.content === "string"
103
+ ? message.content
104
+ : message.content
105
+ .filter((c) => c.type === "text")
106
+ .map((c) => (c as { type: "text"; text: string }).text)
107
+ .join("\n");
108
+ return new Text(content, 0, 0);
109
+ }
110
+
111
+ // Collapsed: show summary
112
+ const fileName = details ? basename(details.filePath) : "file";
113
+ const symbolCount = details?.symbolCount ?? 0;
114
+ const totalLines = details?.totalLines ?? 0;
115
+ const detailLanguage = details?.language ?? "unknown";
116
+
117
+ let summary = theme.fg("accent", "📄 File Map: ");
118
+ summary += theme.fg("toolTitle", theme.bold(fileName));
119
+ summary += theme.fg("muted", ` │ `);
120
+ summary += theme.fg("dim", `${symbolCount} symbols`);
121
+ summary += theme.fg("muted", ` │ `);
122
+ summary += theme.fg("dim", `${totalLines.toLocaleString()} lines`);
123
+ summary += theme.fg("muted", ` │ `);
124
+ summary += theme.fg("dim", detailLanguage);
125
+ summary += theme.fg("muted", ` │ `);
126
+ summary += theme.fg("warning", "Ctrl+O to expand");
127
+
128
+ return new Text(summary, 0, 0);
129
+ }
130
+ );
131
+
132
+ // Register our enhanced read tool
133
+ pi.registerTool({
134
+ name: "read",
135
+ label: "Read",
136
+ description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${Math.round(DEFAULT_MAX_BYTES / 1024)}KB (whichever is hit first). If truncated, a structural map of the file is included to enable targeted reads. Use offset/limit for large files.`,
137
+ parameters: Type.Object({
138
+ path: Type.String({
139
+ description: "Path to the file to read (relative or absolute)",
140
+ }),
141
+ offset: Type.Optional(
142
+ Type.Number({
143
+ description: "Line number to start reading from (1-indexed)",
144
+ })
145
+ ),
146
+ limit: Type.Optional(
147
+ Type.Number({ description: "Maximum number of lines to read" })
148
+ ),
149
+ }),
150
+
151
+ async execute(toolCallId, params, signal, onUpdate) {
152
+ const { path: inputPath, offset, limit } = params;
153
+
154
+ // If offset or limit provided, delegate directly (targeted read)
155
+ if (offset !== undefined || limit !== undefined) {
156
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
157
+ }
158
+
159
+ // Resolve path
160
+ const absPath = resolve(cwd, inputPath.replace(/^@/, ""));
161
+
162
+ // Check file size and line count
163
+ let stats;
164
+ try {
165
+ stats = await stat(absPath);
166
+ } catch {
167
+ // Let built-in handle the error
168
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
169
+ }
170
+
171
+ // For small files or non-regular files, delegate directly
172
+ if (!stats.isFile()) {
173
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
174
+ }
175
+
176
+ // Quick check: if file is small enough by bytes, delegate
177
+ if (stats.size <= DEFAULT_MAX_BYTES) {
178
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
179
+ }
180
+
181
+ // Need to check line count too
182
+ let totalLines: number;
183
+ try {
184
+ const { stdout } = await execAsync(`wc -l < "${absPath}"`, { signal });
185
+ totalLines = Number.parseInt(stdout.trim(), 10) || 0;
186
+ } catch {
187
+ // If we can't count lines, delegate
188
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
189
+ }
190
+
191
+ // If within threshold, delegate
192
+ if (!shouldGenerateMap(totalLines, stats.size)) {
193
+ return builtInRead.execute(toolCallId, params, signal, onUpdate);
194
+ }
195
+
196
+ // File exceeds threshold - generate map
197
+ // First, get the built-in result
198
+ const result = await builtInRead.execute(
199
+ toolCallId,
200
+ params,
201
+ signal,
202
+ onUpdate
203
+ );
204
+
205
+ // Check cache
206
+ const cached = mapCache.get(absPath);
207
+ let mapText: string;
208
+ let symbolCount: number;
209
+ let language: string;
210
+
211
+ if (cached && cached.mtime === stats.mtimeMs) {
212
+ // Cache hit - we need to regenerate map for metadata
213
+ // (alternatively, cache could store metadata too)
214
+ const fileMap = await generateMap(absPath, { signal });
215
+ if (fileMap) {
216
+ mapText = cached.map;
217
+ ({ language } = fileMap);
218
+ symbolCount = fileMap.symbols.length;
219
+ } else {
220
+ return result;
221
+ }
222
+ } else {
223
+ // Generate new map
224
+ const fileMap = await generateMap(absPath, { signal });
225
+
226
+ if (fileMap) {
227
+ mapText = formatFileMapWithBudget(fileMap);
228
+ ({ language } = fileMap);
229
+ symbolCount = fileMap.symbols.length;
230
+ // Cache it
231
+ mapCache.set(absPath, { mtime: stats.mtimeMs, map: mapText });
232
+ } else {
233
+ // Map generation failed, return original result
234
+ return result;
235
+ }
236
+ }
237
+
238
+ // Store map in pendingMaps for delivery after tool_result event
239
+ pendingMaps.set(toolCallId, {
240
+ path: absPath,
241
+ map: mapText,
242
+ details: {
243
+ filePath: absPath,
244
+ totalLines,
245
+ totalBytes: stats.size,
246
+ symbolCount,
247
+ language,
248
+ },
249
+ });
250
+
251
+ // Return the built-in result unmodified (with cleared truncation details
252
+ // since we're providing a map separately)
253
+ return {
254
+ ...result,
255
+ details: undefined,
256
+ };
257
+ },
258
+ });
259
+ }
@@ -0,0 +1,88 @@
1
+ import { extname } from "node:path";
2
+
3
+ import type { LanguageInfo } from "./types.js";
4
+
5
+ /**
6
+ * Map of file extensions to language info.
7
+ */
8
+ const EXTENSION_MAP: Record<string, LanguageInfo> = {
9
+ // Python
10
+ ".py": { id: "python", name: "Python" },
11
+ ".pyw": { id: "python", name: "Python" },
12
+ ".pyi": { id: "python", name: "Python" },
13
+
14
+ // TypeScript
15
+ ".ts": { id: "typescript", name: "TypeScript" },
16
+ ".tsx": { id: "typescript", name: "TypeScript" },
17
+ ".mts": { id: "typescript", name: "TypeScript" },
18
+ ".cts": { id: "typescript", name: "TypeScript" },
19
+
20
+ // JavaScript
21
+ ".js": { id: "javascript", name: "JavaScript" },
22
+ ".jsx": { id: "javascript", name: "JavaScript" },
23
+ ".mjs": { id: "javascript", name: "JavaScript" },
24
+ ".cjs": { id: "javascript", name: "JavaScript" },
25
+
26
+ // Go
27
+ ".go": { id: "go", name: "Go" },
28
+
29
+ // Rust
30
+ ".rs": { id: "rust", name: "Rust" },
31
+
32
+ // C/C++
33
+ ".c": { id: "c", name: "C" },
34
+ ".h": { id: "c-header", name: "C Header" },
35
+ ".cpp": { id: "cpp", name: "C++" },
36
+ ".cc": { id: "cpp", name: "C++" },
37
+ ".cxx": { id: "cpp", name: "C++" },
38
+ ".hpp": { id: "cpp", name: "C++" },
39
+ ".hxx": { id: "cpp", name: "C++" },
40
+
41
+ // SQL
42
+ ".sql": { id: "sql", name: "SQL" },
43
+
44
+ // JSON
45
+ ".json": { id: "json", name: "JSON" },
46
+ ".jsonc": { id: "json", name: "JSON" },
47
+
48
+ // JSON Lines
49
+ ".jsonl": { id: "jsonl", name: "JSON Lines" },
50
+
51
+ // Markdown
52
+ ".md": { id: "markdown", name: "Markdown" },
53
+ ".mdx": { id: "markdown", name: "Markdown" },
54
+
55
+ // YAML
56
+ ".yml": { id: "yaml", name: "YAML" },
57
+ ".yaml": { id: "yaml", name: "YAML" },
58
+
59
+ // TOML
60
+ ".toml": { id: "toml", name: "TOML" },
61
+
62
+ // CSV
63
+ ".csv": { id: "csv", name: "CSV" },
64
+ ".tsv": { id: "csv", name: "TSV" },
65
+ };
66
+
67
+ /**
68
+ * Detect language from file path.
69
+ * Returns null for unknown file types.
70
+ */
71
+ export function detectLanguage(filePath: string): LanguageInfo | null {
72
+ const ext = extname(filePath).toLowerCase();
73
+ return EXTENSION_MAP[ext] ?? null;
74
+ }
75
+
76
+ /**
77
+ * Get all supported extensions.
78
+ */
79
+ export function getSupportedExtensions(): string[] {
80
+ return Object.keys(EXTENSION_MAP);
81
+ }
82
+
83
+ /**
84
+ * Check if a file extension is supported.
85
+ */
86
+ export function isSupported(filePath: string): boolean {
87
+ return detectLanguage(filePath) !== null;
88
+ }
package/src/mapper.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { FileMap, MapOptions } from "./types.js";
2
+
3
+ import { THRESHOLDS } from "./constants.js";
4
+ import { detectLanguage } from "./language-detect.js";
5
+ import { cMapper } from "./mappers/c.js";
6
+ import { cppMapper } from "./mappers/cpp.js";
7
+ import { csvMapper } from "./mappers/csv.js";
8
+ import { ctagsMapper } from "./mappers/ctags.js";
9
+ import { fallbackMapper } from "./mappers/fallback.js";
10
+ import { goMapper } from "./mappers/go.js";
11
+ import { jsonMapper } from "./mappers/json.js";
12
+ import { jsonlMapper } from "./mappers/jsonl.js";
13
+ import { markdownMapper } from "./mappers/markdown.js";
14
+ import { pythonMapper } from "./mappers/python.js";
15
+ import { rustMapper } from "./mappers/rust.js";
16
+ import { sqlMapper } from "./mappers/sql.js";
17
+ import { tomlMapper } from "./mappers/toml.js";
18
+ import { typescriptMapper } from "./mappers/typescript.js";
19
+ import { yamlMapper } from "./mappers/yaml.js";
20
+
21
+ type MapperFn = (
22
+ filePath: string,
23
+ signal?: AbortSignal
24
+ ) => Promise<FileMap | null>;
25
+
26
+ /**
27
+ * Registry of language-specific mappers.
28
+ *
29
+ * Uses internal tree-sitter/ts-morph mappers for all supported languages.
30
+ */
31
+ const MAPPERS: Record<string, MapperFn> = {
32
+ // Phase 1: Python AST-based
33
+ python: pythonMapper,
34
+
35
+ // Phase 2: Go AST-based
36
+ go: goMapper,
37
+
38
+ // Phase 3: Internal ts-morph mappers
39
+ typescript: typescriptMapper,
40
+ javascript: typescriptMapper,
41
+
42
+ // Phase 3: Internal regex-based markdown
43
+ markdown: markdownMapper,
44
+
45
+ // Phase 3: Internal tree-sitter mappers
46
+ rust: rustMapper,
47
+ cpp: cppMapper,
48
+ "c-header": cppMapper, // .h files
49
+
50
+ // Phase 2: Regex/subprocess mappers
51
+ sql: sqlMapper,
52
+ json: jsonMapper,
53
+ jsonl: jsonlMapper,
54
+ c: cMapper, // .c files use regex
55
+
56
+ // Phase 4: Extended coverage
57
+ yaml: yamlMapper,
58
+ toml: tomlMapper,
59
+ csv: csvMapper,
60
+ };
61
+
62
+ /**
63
+ * Generate a structural map for a file.
64
+ *
65
+ * Dispatches to the appropriate language-specific mapper,
66
+ * falling back to ctags (if available) then grep-based extraction.
67
+ */
68
+ export async function generateMap(
69
+ filePath: string,
70
+ options: MapOptions = {}
71
+ ): Promise<FileMap | null> {
72
+ const { signal } = options;
73
+
74
+ // Detect language
75
+ const langInfo = detectLanguage(filePath);
76
+
77
+ if (!langInfo) {
78
+ // Unknown language, try ctags then fallback
79
+ const ctagsResult = await ctagsMapper(filePath, signal);
80
+ if (ctagsResult) {
81
+ return ctagsResult;
82
+ }
83
+ return fallbackMapper(filePath, signal);
84
+ }
85
+
86
+ // Try language-specific mapper
87
+ const mapper = MAPPERS[langInfo.id];
88
+
89
+ if (mapper) {
90
+ const result = await mapper(filePath, signal);
91
+ if (result) {
92
+ return result;
93
+ }
94
+ // Mapper failed, fall through to ctags/fallback
95
+ }
96
+
97
+ // Try ctags as intermediate fallback (better than grep when available)
98
+ const ctagsResult = await ctagsMapper(filePath, signal);
99
+ if (ctagsResult) {
100
+ return ctagsResult;
101
+ }
102
+
103
+ // Use grep-based fallback mapper
104
+ return fallbackMapper(filePath, signal);
105
+ }
106
+
107
+ /**
108
+ * Check if a file should have a map generated.
109
+ * Returns true if the file exceeds the truncation threshold.
110
+ */
111
+ export function shouldGenerateMap(
112
+ totalLines: number,
113
+ totalBytes: number
114
+ ): boolean {
115
+ return totalLines > THRESHOLDS.MAX_LINES || totalBytes > THRESHOLDS.MAX_BYTES;
116
+ }
@@ -0,0 +1,236 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+
3
+ import type { FileMap, FileSymbol } from "../types.js";
4
+
5
+ import { DetailLevel, SymbolKind } from "../enums.js";
6
+
7
+ /**
8
+ * Regex patterns for C language constructs.
9
+ */
10
+ const C_PATTERNS = {
11
+ // Function definition: type name(params) { or type name(params);
12
+ function: /^(\w+(?:\s*\*)?)\s+(\w+)\s*\(([^)]*)\)\s*(?:\{|;)?/,
13
+
14
+ // Struct definition: struct name { or typedef struct { ... } name;
15
+ struct: /^(?:typedef\s+)?struct\s+(\w+)?\s*\{/,
16
+
17
+ // Enum definition
18
+ enum: /^(?:typedef\s+)?enum\s+(\w+)?\s*\{/,
19
+
20
+ // Union definition
21
+ union: /^(?:typedef\s+)?union\s+(\w+)?\s*\{/,
22
+
23
+ // #define macro
24
+ define: /^#define\s+(\w+)(?:\([^)]*\))?\s*/,
25
+
26
+ // Global variable declaration
27
+ variable:
28
+ /^(?:static\s+)?(?:const\s+)?(?:volatile\s+)?(\w+(?:\s*\*)?)\s+(\w+)\s*(?:=|;)/,
29
+
30
+ // Typedef
31
+ typedef: /^typedef\s+(?:struct|enum|union)?\s*\w*\s*\{?[^}]*\}?\s*(\w+)\s*;/,
32
+ };
33
+
34
+ interface CMatch {
35
+ name: string;
36
+ kind: SymbolKind;
37
+ startLine: number;
38
+ signature?: string;
39
+ modifiers?: string[];
40
+ }
41
+
42
+ /**
43
+ * Find the end of a brace-delimited block.
44
+ */
45
+ function findBlockEnd(lines: string[], startIdx: number): number {
46
+ let braceCount = 0;
47
+ let foundOpen = false;
48
+
49
+ for (let i = startIdx; i < lines.length; i++) {
50
+ const line = lines[i];
51
+ if (!line) {
52
+ continue;
53
+ }
54
+ for (const char of line) {
55
+ if (char === "{") {
56
+ braceCount++;
57
+ foundOpen = true;
58
+ } else if (char === "}") {
59
+ braceCount--;
60
+ if (foundOpen && braceCount === 0) {
61
+ return i + 1; // 1-indexed
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ return startIdx + 1; // Single line if no block found
68
+ }
69
+
70
+ /**
71
+ * Check if a line is inside a function body.
72
+ */
73
+ function isInsideFunction(lines: string[], lineIdx: number): boolean {
74
+ let braceCount = 0;
75
+ for (let i = 0; i < lineIdx; i++) {
76
+ const line = lines[i];
77
+ if (!line) {
78
+ continue;
79
+ }
80
+ for (const char of line) {
81
+ if (char === "{") {
82
+ braceCount++;
83
+ } else if (char === "}") {
84
+ braceCount--;
85
+ }
86
+ }
87
+ }
88
+ return braceCount > 0;
89
+ }
90
+
91
+ /**
92
+ * Generate a file map for a C file using regex patterns.
93
+ */
94
+ export async function cMapper(
95
+ filePath: string,
96
+ _signal?: AbortSignal
97
+ ): Promise<FileMap | null> {
98
+ try {
99
+ const stats = await stat(filePath);
100
+ const totalBytes = stats.size;
101
+
102
+ const content = await readFile(filePath, "utf8");
103
+ const lines = content.split("\n");
104
+ const totalLines = lines.length;
105
+
106
+ const matches: CMatch[] = [];
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i];
110
+ if (!line) {
111
+ continue;
112
+ }
113
+ const trimmed = line.trim();
114
+ const lineNum = i + 1;
115
+
116
+ // Skip empty lines and comments
117
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*")) {
118
+ continue;
119
+ }
120
+
121
+ // Skip lines inside function bodies (only want top-level)
122
+ if (isInsideFunction(lines, i) && !C_PATTERNS.define.test(trimmed)) {
123
+ continue;
124
+ }
125
+
126
+ // Check for #define
127
+ const defineMatch = C_PATTERNS.define.exec(trimmed);
128
+ if (defineMatch) {
129
+ const [, matchName] = defineMatch;
130
+ if (matchName) {
131
+ matches.push({
132
+ name: matchName,
133
+ kind: SymbolKind.Constant,
134
+ startLine: lineNum,
135
+ });
136
+ }
137
+ continue;
138
+ }
139
+
140
+ // Check for struct
141
+ const structMatch = C_PATTERNS.struct.exec(trimmed);
142
+ if (structMatch) {
143
+ matches.push({
144
+ name: structMatch[1] ?? "(anonymous)",
145
+ kind: SymbolKind.Class,
146
+ startLine: lineNum,
147
+ });
148
+ continue;
149
+ }
150
+
151
+ // Check for enum
152
+ const enumMatch = C_PATTERNS.enum.exec(trimmed);
153
+ if (enumMatch) {
154
+ matches.push({
155
+ name: enumMatch[1] ?? "(anonymous)",
156
+ kind: SymbolKind.Enum,
157
+ startLine: lineNum,
158
+ });
159
+ continue;
160
+ }
161
+
162
+ // Check for union
163
+ const unionMatch = C_PATTERNS.union.exec(trimmed);
164
+ if (unionMatch) {
165
+ matches.push({
166
+ name: unionMatch[1] ?? "(anonymous)",
167
+ kind: SymbolKind.Class,
168
+ startLine: lineNum,
169
+ modifiers: ["union"],
170
+ });
171
+ continue;
172
+ }
173
+
174
+ // Check for function
175
+ const funcMatch = C_PATTERNS.function.exec(trimmed);
176
+ if (funcMatch) {
177
+ const [, returnType, name, params] = funcMatch;
178
+
179
+ // Skip if it's a variable declaration or control statement
180
+ if (
181
+ !trimmed.includes("(") ||
182
+ !name ||
183
+ /^(if|while|for|switch|return)$/.test(name)
184
+ ) {
185
+ continue;
186
+ }
187
+
188
+ matches.push({
189
+ name,
190
+ kind: SymbolKind.Function,
191
+ startLine: lineNum,
192
+ signature: `(${params ?? ""}): ${returnType ?? "void"}`,
193
+ });
194
+ continue;
195
+ }
196
+ }
197
+
198
+ // Convert to symbols with end lines
199
+ const symbols: FileSymbol[] = matches.map((m) => {
200
+ const { startLine } = m;
201
+ const endLine =
202
+ m.kind === SymbolKind.Constant
203
+ ? startLine
204
+ : findBlockEnd(lines, startLine - 1);
205
+
206
+ const symbol: FileSymbol = {
207
+ name: m.name,
208
+ kind: m.kind,
209
+ startLine,
210
+ endLine,
211
+ };
212
+
213
+ if (m.signature) {
214
+ symbol.signature = m.signature;
215
+ }
216
+
217
+ if (m.modifiers) {
218
+ symbol.modifiers = m.modifiers;
219
+ }
220
+
221
+ return symbol;
222
+ });
223
+
224
+ return {
225
+ path: filePath,
226
+ totalLines,
227
+ totalBytes,
228
+ language: "C",
229
+ symbols,
230
+ detailLevel: DetailLevel.Full,
231
+ };
232
+ } catch (error) {
233
+ console.error(`C mapper failed: ${error}`);
234
+ return null;
235
+ }
236
+ }