prjct-cli 0.44.1 → 0.45.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.
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Signatures Tool - Extract code signatures without full content
3
+ *
4
+ * Extracts:
5
+ * - Function names + params + return types
6
+ * - Interface/type definitions
7
+ * - Class names + methods
8
+ * - Export lists
9
+ *
10
+ * Achieves ~90% token reduction by returning structure only.
11
+ *
12
+ * Uses regex patterns for broad language support.
13
+ * Falls back to full file if language not supported.
14
+ *
15
+ * @module context-tools/signatures-tool
16
+ * @version 1.0.0
17
+ */
18
+
19
+ import fs from 'fs/promises'
20
+ import path from 'path'
21
+ import type { SignaturesToolOutput, CodeSignature, SignatureType } from './types'
22
+ import { measureCompression, noCompression } from './token-counter'
23
+ import { isNotFoundError } from '../types/fs'
24
+
25
+ // =============================================================================
26
+ // Language Support
27
+ // =============================================================================
28
+
29
+ type LanguageId =
30
+ | 'typescript'
31
+ | 'javascript'
32
+ | 'python'
33
+ | 'go'
34
+ | 'rust'
35
+ | 'java'
36
+ | 'csharp'
37
+ | 'php'
38
+ | 'ruby'
39
+ | 'unknown'
40
+
41
+ /**
42
+ * Map file extensions to language identifiers
43
+ */
44
+ const EXTENSION_TO_LANGUAGE: Record<string, LanguageId> = {
45
+ '.ts': 'typescript',
46
+ '.tsx': 'typescript',
47
+ '.js': 'javascript',
48
+ '.jsx': 'javascript',
49
+ '.mjs': 'javascript',
50
+ '.cjs': 'javascript',
51
+ '.py': 'python',
52
+ '.go': 'go',
53
+ '.rs': 'rust',
54
+ '.java': 'java',
55
+ '.cs': 'csharp',
56
+ '.php': 'php',
57
+ '.rb': 'ruby',
58
+ }
59
+
60
+ // =============================================================================
61
+ // Extraction Patterns
62
+ // =============================================================================
63
+
64
+ interface ExtractionPattern {
65
+ type: SignatureType
66
+ pattern: RegExp
67
+ nameIndex: number
68
+ signatureIndex?: number
69
+ exported?: boolean
70
+ }
71
+
72
+ /**
73
+ * TypeScript/JavaScript extraction patterns
74
+ */
75
+ const TS_PATTERNS: ExtractionPattern[] = [
76
+ // Exported function declarations
77
+ {
78
+ type: 'function',
79
+ pattern:
80
+ /^export\s+(?:async\s+)?function\s+(\w+)\s*(<[^>]*>)?\s*\(([^)]*)\)\s*(?::\s*([^{;]+))?/gm,
81
+ nameIndex: 1,
82
+ exported: true,
83
+ },
84
+ // Exported const arrow functions
85
+ {
86
+ type: 'function',
87
+ pattern:
88
+ /^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/gm,
89
+ nameIndex: 1,
90
+ exported: true,
91
+ },
92
+ // Regular function declarations
93
+ {
94
+ type: 'function',
95
+ pattern:
96
+ /^(?:async\s+)?function\s+(\w+)\s*(<[^>]*>)?\s*\(([^)]*)\)\s*(?::\s*([^{;]+))?/gm,
97
+ nameIndex: 1,
98
+ },
99
+ // Const arrow functions
100
+ {
101
+ type: 'function',
102
+ pattern:
103
+ /^const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/gm,
104
+ nameIndex: 1,
105
+ },
106
+ // Interface declarations
107
+ {
108
+ type: 'interface',
109
+ pattern: /^export\s+interface\s+(\w+)(?:<[^>]+>)?\s*(?:extends\s+[^{]+)?\s*\{/gm,
110
+ nameIndex: 1,
111
+ exported: true,
112
+ },
113
+ {
114
+ type: 'interface',
115
+ pattern: /^interface\s+(\w+)(?:<[^>]+>)?\s*(?:extends\s+[^{]+)?\s*\{/gm,
116
+ nameIndex: 1,
117
+ },
118
+ // Type aliases
119
+ {
120
+ type: 'type',
121
+ pattern: /^export\s+type\s+(\w+)(?:<[^>]+>)?\s*=/gm,
122
+ nameIndex: 1,
123
+ exported: true,
124
+ },
125
+ {
126
+ type: 'type',
127
+ pattern: /^type\s+(\w+)(?:<[^>]+>)?\s*=/gm,
128
+ nameIndex: 1,
129
+ },
130
+ // Class declarations
131
+ {
132
+ type: 'class',
133
+ pattern:
134
+ /^export\s+(?:abstract\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+[^\{]+)?(?:\s+implements\s+[^\{]+)?\s*\{/gm,
135
+ nameIndex: 1,
136
+ exported: true,
137
+ },
138
+ {
139
+ type: 'class',
140
+ pattern:
141
+ /^(?:abstract\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+[^\{]+)?(?:\s+implements\s+[^\{]+)?\s*\{/gm,
142
+ nameIndex: 1,
143
+ },
144
+ // Enum declarations
145
+ {
146
+ type: 'enum',
147
+ pattern: /^export\s+enum\s+(\w+)\s*\{/gm,
148
+ nameIndex: 1,
149
+ exported: true,
150
+ },
151
+ {
152
+ type: 'enum',
153
+ pattern: /^enum\s+(\w+)\s*\{/gm,
154
+ nameIndex: 1,
155
+ },
156
+ // Exported constants
157
+ {
158
+ type: 'const',
159
+ pattern: /^export\s+const\s+(\w+)\s*(?::\s*([^=]+))?\s*=/gm,
160
+ nameIndex: 1,
161
+ exported: true,
162
+ },
163
+ ]
164
+
165
+ /**
166
+ * Python extraction patterns
167
+ */
168
+ const PYTHON_PATTERNS: ExtractionPattern[] = [
169
+ // Function definitions
170
+ {
171
+ type: 'function',
172
+ pattern: /^def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*([^:]+))?\s*:/gm,
173
+ nameIndex: 1,
174
+ },
175
+ // Async function definitions
176
+ {
177
+ type: 'function',
178
+ pattern: /^async\s+def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*([^:]+))?\s*:/gm,
179
+ nameIndex: 1,
180
+ },
181
+ // Class definitions
182
+ {
183
+ type: 'class',
184
+ pattern: /^class\s+(\w+)(?:\(([^)]*)\))?\s*:/gm,
185
+ nameIndex: 1,
186
+ },
187
+ ]
188
+
189
+ /**
190
+ * Go extraction patterns
191
+ */
192
+ const GO_PATTERNS: ExtractionPattern[] = [
193
+ // Function declarations
194
+ {
195
+ type: 'function',
196
+ pattern: /^func\s+(\w+)\s*\(([^)]*)\)\s*(?:\(([^)]*)\)|([^\s{]+))?\s*\{/gm,
197
+ nameIndex: 1,
198
+ },
199
+ // Method declarations
200
+ {
201
+ type: 'method',
202
+ pattern: /^func\s+\([^)]+\)\s+(\w+)\s*\(([^)]*)\)\s*(?:\(([^)]*)\)|([^\s{]+))?\s*\{/gm,
203
+ nameIndex: 1,
204
+ },
205
+ // Type definitions
206
+ {
207
+ type: 'type',
208
+ pattern: /^type\s+(\w+)\s+(?:struct|interface)\s*\{/gm,
209
+ nameIndex: 1,
210
+ },
211
+ ]
212
+
213
+ /**
214
+ * Rust extraction patterns
215
+ */
216
+ const RUST_PATTERNS: ExtractionPattern[] = [
217
+ // Public function declarations
218
+ {
219
+ type: 'function',
220
+ pattern: /^pub\s+(?:async\s+)?fn\s+(\w+)(?:<[^>]+>)?\s*\(([^)]*)\)\s*(?:->\s*([^{]+))?\s*\{/gm,
221
+ nameIndex: 1,
222
+ exported: true,
223
+ },
224
+ // Private function declarations
225
+ {
226
+ type: 'function',
227
+ pattern: /^(?:async\s+)?fn\s+(\w+)(?:<[^>]+>)?\s*\(([^)]*)\)\s*(?:->\s*([^{]+))?\s*\{/gm,
228
+ nameIndex: 1,
229
+ },
230
+ // Struct definitions
231
+ {
232
+ type: 'class',
233
+ pattern: /^pub\s+struct\s+(\w+)(?:<[^>]+>)?\s*(?:\{|;)/gm,
234
+ nameIndex: 1,
235
+ exported: true,
236
+ },
237
+ {
238
+ type: 'class',
239
+ pattern: /^struct\s+(\w+)(?:<[^>]+>)?\s*(?:\{|;)/gm,
240
+ nameIndex: 1,
241
+ },
242
+ // Trait definitions
243
+ {
244
+ type: 'interface',
245
+ pattern: /^pub\s+trait\s+(\w+)(?:<[^>]+>)?\s*(?:\{|:)/gm,
246
+ nameIndex: 1,
247
+ exported: true,
248
+ },
249
+ {
250
+ type: 'interface',
251
+ pattern: /^trait\s+(\w+)(?:<[^>]+>)?\s*(?:\{|:)/gm,
252
+ nameIndex: 1,
253
+ },
254
+ // Enum definitions
255
+ {
256
+ type: 'enum',
257
+ pattern: /^pub\s+enum\s+(\w+)(?:<[^>]+>)?\s*\{/gm,
258
+ nameIndex: 1,
259
+ exported: true,
260
+ },
261
+ {
262
+ type: 'enum',
263
+ pattern: /^enum\s+(\w+)(?:<[^>]+>)?\s*\{/gm,
264
+ nameIndex: 1,
265
+ },
266
+ ]
267
+
268
+ /**
269
+ * Java extraction patterns
270
+ */
271
+ const JAVA_PATTERNS: ExtractionPattern[] = [
272
+ // Class declarations
273
+ {
274
+ type: 'class',
275
+ pattern:
276
+ /^(?:public\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+\w+)?(?:\s+implements\s+[^{]+)?\s*\{/gm,
277
+ nameIndex: 1,
278
+ exported: true,
279
+ },
280
+ // Interface declarations
281
+ {
282
+ type: 'interface',
283
+ pattern: /^(?:public\s+)?interface\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+[^{]+)?\s*\{/gm,
284
+ nameIndex: 1,
285
+ exported: true,
286
+ },
287
+ // Method declarations
288
+ {
289
+ type: 'method',
290
+ pattern:
291
+ /^\s+(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+[^{]+)?\s*\{/gm,
292
+ nameIndex: 2,
293
+ },
294
+ ]
295
+
296
+ /**
297
+ * Language to patterns mapping
298
+ */
299
+ const LANGUAGE_PATTERNS: Record<LanguageId, ExtractionPattern[]> = {
300
+ typescript: TS_PATTERNS,
301
+ javascript: TS_PATTERNS,
302
+ python: PYTHON_PATTERNS,
303
+ go: GO_PATTERNS,
304
+ rust: RUST_PATTERNS,
305
+ java: JAVA_PATTERNS,
306
+ csharp: JAVA_PATTERNS, // Similar enough for basic extraction
307
+ php: [], // Fallback to full file
308
+ ruby: [], // Fallback to full file
309
+ unknown: [],
310
+ }
311
+
312
+ // =============================================================================
313
+ // Main Function
314
+ // =============================================================================
315
+
316
+ /**
317
+ * Extract code signatures from a file
318
+ *
319
+ * @param filePath - Path to the file (absolute or relative to cwd)
320
+ * @param projectPath - Project root path (for resolving relative paths)
321
+ * @returns Extracted signatures with compression metrics
322
+ */
323
+ export async function extractSignatures(
324
+ filePath: string,
325
+ projectPath: string = process.cwd()
326
+ ): Promise<SignaturesToolOutput> {
327
+ // Resolve to absolute path
328
+ const absolutePath = path.isAbsolute(filePath)
329
+ ? filePath
330
+ : path.join(projectPath, filePath)
331
+
332
+ // Read file content
333
+ let content: string
334
+ try {
335
+ content = await fs.readFile(absolutePath, 'utf-8')
336
+ } catch (error) {
337
+ if (isNotFoundError(error)) {
338
+ return {
339
+ file: filePath,
340
+ language: 'unknown',
341
+ signatures: [],
342
+ fallback: true,
343
+ fallbackReason: 'File not found',
344
+ metrics: noCompression(''),
345
+ }
346
+ }
347
+ throw error
348
+ }
349
+
350
+ // Detect language
351
+ const ext = path.extname(filePath).toLowerCase()
352
+ const language = EXTENSION_TO_LANGUAGE[ext] || 'unknown'
353
+ const patterns = LANGUAGE_PATTERNS[language]
354
+
355
+ // No patterns = fallback to full file
356
+ if (!patterns || patterns.length === 0) {
357
+ return {
358
+ file: filePath,
359
+ language,
360
+ signatures: [],
361
+ fallback: true,
362
+ fallbackReason: `No extraction patterns for ${language}`,
363
+ metrics: noCompression(content),
364
+ }
365
+ }
366
+
367
+ // Extract signatures
368
+ const signatures = extractFromContent(content, patterns)
369
+
370
+ // Build filtered output (just signatures)
371
+ const filteredContent = signatures
372
+ .map((sig) => {
373
+ const exportPrefix = sig.exported ? 'export ' : ''
374
+ return `${exportPrefix}${sig.type} ${sig.name}: ${sig.signature}`
375
+ })
376
+ .join('\n')
377
+
378
+ return {
379
+ file: filePath,
380
+ language,
381
+ signatures,
382
+ fallback: false,
383
+ metrics: measureCompression(content, filteredContent),
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Extract signatures from multiple files in a directory
389
+ */
390
+ export async function extractDirectorySignatures(
391
+ dirPath: string,
392
+ projectPath: string = process.cwd(),
393
+ options: { recursive?: boolean } = {}
394
+ ): Promise<SignaturesToolOutput[]> {
395
+ const absolutePath = path.isAbsolute(dirPath)
396
+ ? dirPath
397
+ : path.join(projectPath, dirPath)
398
+
399
+ const results: SignaturesToolOutput[] = []
400
+
401
+ async function processDir(dir: string): Promise<void> {
402
+ const entries = await fs.readdir(dir, { withFileTypes: true })
403
+
404
+ for (const entry of entries) {
405
+ const fullPath = path.join(dir, entry.name)
406
+ const relativePath = path.relative(projectPath, fullPath)
407
+
408
+ if (entry.isDirectory()) {
409
+ // Skip common ignore patterns
410
+ if (
411
+ entry.name === 'node_modules' ||
412
+ entry.name === '.git' ||
413
+ entry.name.startsWith('.')
414
+ ) {
415
+ continue
416
+ }
417
+ if (options.recursive) {
418
+ await processDir(fullPath)
419
+ }
420
+ } else if (entry.isFile()) {
421
+ const ext = path.extname(entry.name).toLowerCase()
422
+ if (EXTENSION_TO_LANGUAGE[ext]) {
423
+ const result = await extractSignatures(relativePath, projectPath)
424
+ results.push(result)
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ await processDir(absolutePath)
431
+ return results
432
+ }
433
+
434
+ // =============================================================================
435
+ // Helper Functions
436
+ // =============================================================================
437
+
438
+ /**
439
+ * Extract signatures from content using patterns
440
+ */
441
+ function extractFromContent(
442
+ content: string,
443
+ patterns: ExtractionPattern[]
444
+ ): CodeSignature[] {
445
+ const signatures: CodeSignature[] = []
446
+ const lines = content.split('\n')
447
+
448
+ // Track what we've extracted to avoid duplicates
449
+ const seen = new Set<string>()
450
+
451
+ for (const patternDef of patterns) {
452
+ // Reset lastIndex for global regex
453
+ patternDef.pattern.lastIndex = 0
454
+
455
+ let match
456
+ while ((match = patternDef.pattern.exec(content)) !== null) {
457
+ const name = match[patternDef.nameIndex]
458
+ if (!name) continue
459
+
460
+ // Create a key for deduplication
461
+ const key = `${patternDef.type}:${name}`
462
+ if (seen.has(key)) continue
463
+ seen.add(key)
464
+
465
+ // Get line number
466
+ const matchIndex = match.index
467
+ const lineNumber = content.substring(0, matchIndex).split('\n').length
468
+
469
+ // Extract the full signature line
470
+ const signatureLine = match[0].trim()
471
+
472
+ // Try to extract docstring (line before the signature)
473
+ let docstring: string | undefined
474
+ if (lineNumber > 1) {
475
+ const prevLine = lines[lineNumber - 2]?.trim()
476
+ if (prevLine?.startsWith('/**') || prevLine?.startsWith('///') || prevLine?.startsWith('#')) {
477
+ docstring = prevLine
478
+ }
479
+ }
480
+
481
+ signatures.push({
482
+ type: patternDef.type,
483
+ name,
484
+ signature: cleanSignature(signatureLine),
485
+ exported: patternDef.exported || false,
486
+ line: lineNumber,
487
+ docstring,
488
+ })
489
+ }
490
+ }
491
+
492
+ // Sort by line number
493
+ return signatures.sort((a, b) => a.line - b.line)
494
+ }
495
+
496
+ /**
497
+ * Clean up a signature line for display
498
+ */
499
+ function cleanSignature(signature: string): string {
500
+ return signature
501
+ .replace(/\{$/, '') // Remove trailing {
502
+ .replace(/\s+/g, ' ') // Normalize whitespace
503
+ .trim()
504
+ }
505
+
506
+ // =============================================================================
507
+ // Exports
508
+ // =============================================================================
509
+
510
+ export default { extractSignatures, extractDirectorySignatures }