@@ -11,9 +11,9 @@ import {
1111 fileExtractStructure ,
1212 fileReadContent ,
1313 fileHighlightReadRange ,
14- OrphanedSymbolNode ,
15- UnifiedFileSymbol ,
16- UnifiedFileResult ,
14+ type OrphanedItem ,
15+ type FileSymbol ,
16+ type FileStructure ,
1717} from '../../client-pipe.js' ;
1818import { getClientWorkspace } from '../../config.js' ;
1919import { zod } from '../../third_party/index.js' ;
@@ -27,11 +27,6 @@ function resolveFilePath(file: string): string {
2727 return path . resolve ( getClientWorkspace ( ) , file ) ;
2828}
2929
30- // Supported TS/JS file extensions for structured extraction
31- const STRUCTURED_EXTS = new Set ( [
32- 'ts' , 'tsx' , 'js' , 'jsx' , 'mts' , 'mjs' , 'cts' , 'cjs' ,
33- ] ) ;
34-
3530// Special target keywords for orphaned content
3631const SPECIAL_TARGETS = [ '#imports' , '#exports' , '#comments' ] as const ;
3732type SpecialTarget = typeof SPECIAL_TARGETS [ number ] ;
@@ -55,12 +50,12 @@ function addLineNumbers(content: string, startLine1: number): string {
5550}
5651
5752function formatSkeletonEntry (
58- symbol : SymbolLike | OrphanedSymbolNode ,
53+ symbol : SymbolLike | OrphanedItem ,
5954 indent = '' ,
6055 recursive = false ,
6156) : string [ ] {
6257 const lines : string [ ] = [ ] ;
63- // Both UnifiedFileSymbol and OrphanedSymbolNode are 1-indexed
58+ // FileSymbol uses startLine/endLine, OrphanedItem uses start/end
6459 const startLine = 'startLine' in symbol . range ? symbol . range . startLine : symbol . range . start ;
6560 const endLine = 'startLine' in symbol . range ? symbol . range . endLine : symbol . range . end ;
6661 const range = startLine === endLine ? `${ startLine } ` : `${ startLine } -${ endLine } ` ;
@@ -129,15 +124,15 @@ interface NonSymbolBlock {
129124}
130125
131126type LineOwner =
132- | { type : 'symbol' ; symbol : UnifiedFileSymbol }
127+ | { type : 'symbol' ; symbol : FileSymbol }
133128 | { type : 'block' ; block : NonSymbolBlock } ;
134129
135130/**
136131 * Group consecutive non-symbol items of the same type into atomic blocks.
137132 * Each block represents a contiguous run of the same non-symbol category.
138133 * Lines within symbol ranges are excluded — only "between-symbol" content forms blocks.
139134 */
140- function buildNonSymbolBlocks ( structure : UnifiedFileResult ) : NonSymbolBlock [ ] {
135+ function buildNonSymbolBlocks ( structure : FileStructure ) : NonSymbolBlock [ ] {
141136 // Build set of lines owned by symbols so we can exclude them
142137 const symbolLines = new Set < number > ( ) ;
143138 for ( const sym of structure . symbols ) {
@@ -148,26 +143,19 @@ function buildNonSymbolBlocks(structure: UnifiedFileResult): NonSymbolBlock[] {
148143
149144 const tagged : Array < { line : number ; type : NonSymbolType } > = [ ] ;
150145
151- for ( const imp of structure . imports ) {
152- for ( let line = imp . range . start ; line <= imp . range . end ; line ++ ) {
153- if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : 'import' } ) ;
154- }
155- }
156- for ( const exp of structure . exports ) {
157- for ( let line = exp . range . start ; line <= exp . range . end ; line ++ ) {
158- if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : 'export' } ) ;
159- }
160- }
161- for ( const comment of structure . orphanComments ) {
162- for ( let line = comment . range . start ; line <= comment . range . end ; line ++ ) {
163- if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : 'comment' } ) ;
164- }
165- }
166- for ( const dir of structure . directives ) {
167- for ( let line = dir . range . start ; line <= dir . range . end ; line ++ ) {
168- if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : 'directive' } ) ;
146+ for ( const item of structure . orphaned . items ) {
147+ const mappedType : NonSymbolType =
148+ item . category === 'import' ? 'import' :
149+ item . category === 'export' ? 'export' :
150+ item . category === 'comment' ? 'comment' :
151+ item . category === 'directive' ? 'directive' :
152+ 'comment' ; // footnote/linkdef fall under comment for rendering
153+
154+ for ( let line = item . range . start ; line <= item . range . end ; line ++ ) {
155+ if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : mappedType } ) ;
169156 }
170157 }
158+
171159 for ( const gap of structure . gaps ) {
172160 for ( let line = gap . start ; line <= gap . end ; line ++ ) {
173161 if ( ! symbolLines . has ( line ) ) tagged . push ( { line, type : 'gap' } ) ;
@@ -197,7 +185,7 @@ function buildNonSymbolBlocks(structure: UnifiedFileResult): NonSymbolBlock[] {
197185 * Only covers lines within the requested range for efficiency.
198186 */
199187function classifyLines (
200- structure : UnifiedFileResult ,
188+ structure : FileStructure ,
201189 blocks : NonSymbolBlock [ ] ,
202190 startLine : number ,
203191 endLine : number ,
@@ -259,7 +247,7 @@ function expandToBlockBoundaries(
259247 * Returns the actual source-line range that the output covers (for highlighting).
260248 */
261249function renderStructuredRange (
262- structure : UnifiedFileResult ,
250+ structure : FileStructure ,
263251 allLines : string [ ] ,
264252 requestedStart : number ,
265253 requestedEnd : number ,
@@ -280,7 +268,7 @@ function renderStructuredRange(
280268 const owners = classifyLines ( structure , blocks , expandedStart , expandedEnd ) ;
281269
282270 const result : string [ ] = [ ] ;
283- const emittedSymbols = new Set < UnifiedFileSymbol > ( ) ;
271+ const emittedSymbols = new Set < FileSymbol > ( ) ;
284272 const emittedBlocks = new Set < NonSymbolBlock > ( ) ;
285273
286274 // Track the actual source-line range that the output covers
@@ -504,14 +492,11 @@ export const read = defineTool({
504492 }
505493
506494 // Check if this is a TS/JS file that supports structured extraction
507- const ext = path . extname ( filePath ) . slice ( 1 ) . toLowerCase ( ) ;
508- const isStructuredFile = STRUCTURED_EXTS . has ( ext ) ;
509-
510- // Get unified structure for TS/JS files (one call replaces three)
511- let structure : UnifiedFileResult | undefined ;
495+ // Get file structure via registry (supports any registered language)
496+ let structure : FileStructure | undefined ;
512497 let allLines : string [ ] = [ ] ;
513- if ( isStructuredFile ) {
514- structure = await fileExtractStructure ( filePath ) ;
498+ structure = await fileExtractStructure ( filePath ) ;
499+ if ( structure ) {
515500 allLines = structure . content . split ( '\n' ) ;
516501 }
517502
@@ -573,7 +558,7 @@ export const read = defineTool({
573558 // ── Skeleton mode (no targets, no line range) ─────────────
574559 if ( targets . length === 0 && skeleton ) {
575560 if ( ! structure ) {
576- response . appendResponseLine ( 'Skeleton mode requires a TypeScript or JavaScript file.' ) ;
561+ response . appendResponseLine ( 'Skeleton mode requires a supported structured file type .' ) ;
577562 return ;
578563 }
579564
@@ -582,22 +567,19 @@ export const read = defineTool({
582567 startLine : number ;
583568 endLine : number ;
584569 category : 'imports' | 'exports' | 'comments' | 'directives' | 'symbol' | 'raw' ;
585- symbol ?: UnifiedFileSymbol ;
570+ symbol ?: FileSymbol ;
586571 }
587572
588573 const pieces : SkeletonPiece [ ] = [ ] ;
589574
590- for ( const imp of structure . imports ) {
591- pieces . push ( { startLine : imp . range . start , endLine : imp . range . end , category : 'imports' } ) ;
592- }
593- for ( const exp of structure . exports ) {
594- pieces . push ( { startLine : exp . range . start , endLine : exp . range . end , category : 'exports' } ) ;
595- }
596- for ( const comment of structure . orphanComments ) {
597- pieces . push ( { startLine : comment . range . start , endLine : comment . range . end , category : 'comments' } ) ;
598- }
599- for ( const dir of structure . directives ) {
600- pieces . push ( { startLine : dir . range . start , endLine : dir . range . end , category : 'directives' } ) ;
575+ for ( const item of structure . orphaned . items ) {
576+ const cat : SkeletonPiece [ 'category' ] =
577+ item . category === 'import' ? 'imports' :
578+ item . category === 'export' ? 'exports' :
579+ item . category === 'comment' ? 'comments' :
580+ item . category === 'directive' ? 'directives' :
581+ 'comments' ;
582+ pieces . push ( { startLine : item . range . start , endLine : item . range . end , category : cat } ) ;
601583 }
602584 for ( const sym of structure . symbols ) {
603585 pieces . push ( { startLine : sym . range . startLine , endLine : sym . range . endLine , category : 'symbol' , symbol : sym } ) ;
@@ -662,22 +644,22 @@ export const read = defineTool({
662644
663645 // ── Targets mode ─────────────────────────────────────────
664646
665- // Targets require structured extraction (TS/JS only)
647+ // Targets require structured extraction
666648 if ( ! structure ) {
667649 response . appendResponseLine (
668- 'Target-based reading requires a TypeScript or JavaScript file.' ,
650+ 'Target-based reading requires a supported structured file type .' ,
669651 ) ;
670652 return ;
671653 }
672654
673655 for ( const target of targets ) {
674656 if ( isSpecialTarget ( target ) ) {
675657 // Handle special keywords: #imports, #exports, #comments
676- const items = target === '#imports'
677- ? structure . imports
678- : target === '#exports'
679- ? structure . exports
680- : structure . orphanComments ;
658+ const categoryFilter =
659+ target === '# imports' ? 'import' :
660+ target === '#exports' ? 'export' :
661+ 'comment' ;
662+ const items = structure . orphaned . items . filter ( i => i . category === categoryFilter ) ;
681663
682664 if ( skeleton ) {
683665 for ( const item of items ) {
@@ -694,7 +676,7 @@ export const read = defineTool({
694676 }
695677 }
696678 } else {
697- // Symbol targeting (1-indexed ranges from ts-morph )
679+ // Symbol targeting (1-indexed ranges)
698680 const match = resolveSymbolTarget ( structure . symbols , target ) ;
699681
700682 if ( ! match ) {
0 commit comments