opencodekit 0.9.0 → 0.9.2

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.
@@ -14,7 +14,13 @@ import { lspManager } from "../lib/lsp/client";
14
14
  import { resolveServer } from "../lib/lsp/config";
15
15
  import type {
16
16
  CodeAction,
17
+ Diagnostic,
18
+ DocumentSymbol,
19
+ HoverResult,
20
+ Location,
21
+ LocationLink,
17
22
  PrepareRenameResult,
23
+ SymbolInfo,
18
24
  WorkspaceEdit,
19
25
  } from "../lib/lsp/types";
20
26
  import {
@@ -328,5 +334,453 @@ Note: File was modified directly. Re-read before editing.`;
328
334
  },
329
335
  });
330
336
 
337
+ /**
338
+ * Get hover information (type/documentation) at a position
339
+ */
340
+ export const lsp_hover = tool({
341
+ description: `Get type information and documentation for a symbol at a specific position.
342
+
343
+ Returns type signatures, JSDoc/docstrings, and other hover information the language server provides.
344
+ Useful for understanding what a variable/function is without reading its definition.`,
345
+
346
+ args: {
347
+ filePath: tool.schema.string().describe("Path to the file"),
348
+ line: tool.schema.number().describe("Line number (1-based)"),
349
+ character: tool.schema.number().describe("Character position (0-based)"),
350
+ },
351
+
352
+ async execute(args) {
353
+ const cwd = process.cwd();
354
+ const absPath = resolve(cwd, args.filePath);
355
+ const server = resolveServer(absPath);
356
+
357
+ if (!server) {
358
+ return `Error: No LSP server available for file type: ${absPath}`;
359
+ }
360
+
361
+ try {
362
+ const client = await lspManager.getClient(cwd, server);
363
+ const result = (await client.hover(
364
+ absPath,
365
+ args.line,
366
+ args.character,
367
+ )) as HoverResult | null;
368
+ lspManager.releaseClient(cwd, server.id);
369
+
370
+ if (!result) {
371
+ return "No hover information available at this position.";
372
+ }
373
+
374
+ return formatHoverResult(result);
375
+ } catch (error) {
376
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
377
+ }
378
+ },
379
+ });
380
+
381
+ /**
382
+ * Go to definition of a symbol
383
+ */
384
+ export const lsp_goto_definition = tool({
385
+ description: `Find the definition location of a symbol at a specific position.
386
+
387
+ Returns the file path and position where the symbol is defined.
388
+ Works for functions, variables, classes, types, imports, etc.`,
389
+
390
+ args: {
391
+ filePath: tool.schema.string().describe("Path to the file"),
392
+ line: tool.schema.number().describe("Line number (1-based)"),
393
+ character: tool.schema.number().describe("Character position (0-based)"),
394
+ },
395
+
396
+ async execute(args) {
397
+ const cwd = process.cwd();
398
+ const absPath = resolve(cwd, args.filePath);
399
+ const server = resolveServer(absPath);
400
+
401
+ if (!server) {
402
+ return `Error: No LSP server available for file type: ${absPath}`;
403
+ }
404
+
405
+ try {
406
+ const client = await lspManager.getClient(cwd, server);
407
+ const result = (await client.definition(
408
+ absPath,
409
+ args.line,
410
+ args.character,
411
+ )) as Location | Location[] | LocationLink[] | null;
412
+ lspManager.releaseClient(cwd, server.id);
413
+
414
+ if (!result) {
415
+ return "No definition found at this position.";
416
+ }
417
+
418
+ return formatLocations(result);
419
+ } catch (error) {
420
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
421
+ }
422
+ },
423
+ });
424
+
425
+ /**
426
+ * Find all references to a symbol
427
+ */
428
+ export const lsp_find_references = tool({
429
+ description: `Find all references to a symbol across the workspace.
430
+
431
+ Returns all locations where the symbol is used, including the definition.
432
+ Useful for understanding usage before refactoring.`,
433
+
434
+ args: {
435
+ filePath: tool.schema.string().describe("Path to the file"),
436
+ line: tool.schema.number().describe("Line number (1-based)"),
437
+ character: tool.schema.number().describe("Character position (0-based)"),
438
+ includeDeclaration: tool.schema
439
+ .boolean()
440
+ .optional()
441
+ .describe("Include the declaration itself (default: true)"),
442
+ },
443
+
444
+ async execute(args) {
445
+ const cwd = process.cwd();
446
+ const absPath = resolve(cwd, args.filePath);
447
+ const server = resolveServer(absPath);
448
+
449
+ if (!server) {
450
+ return `Error: No LSP server available for file type: ${absPath}`;
451
+ }
452
+
453
+ try {
454
+ const client = await lspManager.getClient(cwd, server);
455
+ const result = (await client.references(
456
+ absPath,
457
+ args.line,
458
+ args.character,
459
+ args.includeDeclaration ?? true,
460
+ )) as Location[] | null;
461
+ lspManager.releaseClient(cwd, server.id);
462
+
463
+ if (!result || result.length === 0) {
464
+ return "No references found.";
465
+ }
466
+
467
+ const locations = result.slice(0, 50); // Limit results
468
+ const formatted = locations.map((loc) => formatLocation(loc)).join("\n");
469
+ const suffix =
470
+ result.length > 50 ? `\n\n... and ${result.length - 50} more` : "";
471
+
472
+ return `Found ${result.length} reference(s):\n\n${formatted}${suffix}`;
473
+ } catch (error) {
474
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
475
+ }
476
+ },
477
+ });
478
+
479
+ /**
480
+ * Get document symbols (outline) for a file
481
+ */
482
+ export const lsp_document_symbols = tool({
483
+ description: `Get the symbol outline of a file (classes, functions, variables, etc.).
484
+
485
+ Returns a hierarchical structure of all symbols in the file.
486
+ Useful for understanding file structure quickly.`,
487
+
488
+ args: {
489
+ filePath: tool.schema.string().describe("Path to the file"),
490
+ },
491
+
492
+ async execute(args) {
493
+ const cwd = process.cwd();
494
+ const absPath = resolve(cwd, args.filePath);
495
+ const server = resolveServer(absPath);
496
+
497
+ if (!server) {
498
+ return `Error: No LSP server available for file type: ${absPath}`;
499
+ }
500
+
501
+ try {
502
+ const client = await lspManager.getClient(cwd, server);
503
+ const result = (await client.documentSymbols(absPath)) as
504
+ | DocumentSymbol[]
505
+ | SymbolInfo[]
506
+ | null;
507
+ lspManager.releaseClient(cwd, server.id);
508
+
509
+ if (!result || result.length === 0) {
510
+ return "No symbols found in this file.";
511
+ }
512
+
513
+ return formatDocumentSymbols(result);
514
+ } catch (error) {
515
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
516
+ }
517
+ },
518
+ });
519
+
520
+ /**
521
+ * Search for symbols across the workspace
522
+ */
523
+ export const lsp_workspace_symbols = tool({
524
+ description: `Search for symbols by name across the entire workspace.
525
+
526
+ Performs fuzzy matching on symbol names (functions, classes, variables, etc.).
527
+ Useful for finding where something is defined without knowing the exact file.`,
528
+
529
+ args: {
530
+ query: tool.schema
531
+ .string()
532
+ .describe("Symbol name to search for (fuzzy match)"),
533
+ filePath: tool.schema
534
+ .string()
535
+ .optional()
536
+ .describe("Any file in the project (used to determine language server)"),
537
+ },
538
+
539
+ async execute(args) {
540
+ const cwd = process.cwd();
541
+ const absPath = args.filePath
542
+ ? resolve(cwd, args.filePath)
543
+ : resolve(cwd, ".");
544
+ const server = resolveServer(absPath);
545
+
546
+ if (!server) {
547
+ return `Error: No LSP server available. Provide a filePath to help identify the language.`;
548
+ }
549
+
550
+ try {
551
+ const client = await lspManager.getClient(cwd, server);
552
+ const result = (await client.workspaceSymbols(args.query)) as
553
+ | SymbolInfo[]
554
+ | null;
555
+ lspManager.releaseClient(cwd, server.id);
556
+
557
+ if (!result || result.length === 0) {
558
+ return `No symbols found matching "${args.query}".`;
559
+ }
560
+
561
+ const symbols = result.slice(0, 30); // Limit results
562
+ const formatted = symbols
563
+ .map(
564
+ (sym) =>
565
+ `${getSymbolKindName(sym.kind)} ${sym.name}${sym.containerName ? ` (in ${sym.containerName})` : ""}\n ${formatUri(sym.location.uri)}:${sym.location.range.start.line + 1}`,
566
+ )
567
+ .join("\n\n");
568
+ const suffix =
569
+ result.length > 30 ? `\n\n... and ${result.length - 30} more` : "";
570
+
571
+ return `Found ${result.length} symbol(s):\n\n${formatted}${suffix}`;
572
+ } catch (error) {
573
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
574
+ }
575
+ },
576
+ });
577
+
578
+ /**
579
+ * Get diagnostics (errors, warnings) for a file
580
+ */
581
+ export const lsp_diagnostics = tool({
582
+ description: `Get diagnostics (errors, warnings, hints) for a file from the language server.
583
+
584
+ Returns type errors, lint warnings, and other issues detected by the language server.
585
+ Useful for checking if code has problems before running tests.`,
586
+
587
+ args: {
588
+ filePath: tool.schema.string().describe("Path to the file"),
589
+ severity: tool.schema
590
+ .string()
591
+ .optional()
592
+ .describe(
593
+ "Filter by severity: 'error', 'warning', 'info', 'hint', or 'all' (default: all)",
594
+ ),
595
+ },
596
+
597
+ async execute(args) {
598
+ const cwd = process.cwd();
599
+ const absPath = resolve(cwd, args.filePath);
600
+ const server = resolveServer(absPath);
601
+
602
+ if (!server) {
603
+ return `Error: No LSP server available for file type: ${absPath}`;
604
+ }
605
+
606
+ try {
607
+ const client = await lspManager.getClient(cwd, server);
608
+ const result = await client.diagnostics(absPath);
609
+ lspManager.releaseClient(cwd, server.id);
610
+
611
+ let diagnostics = result.items || [];
612
+
613
+ // Filter by severity if specified
614
+ if (args.severity && args.severity !== "all") {
615
+ const severityMap: Record<string, number> = {
616
+ error: 1,
617
+ warning: 2,
618
+ info: 3,
619
+ hint: 4,
620
+ };
621
+ const targetSeverity = severityMap[args.severity.toLowerCase()];
622
+ if (targetSeverity) {
623
+ diagnostics = diagnostics.filter(
624
+ (d) => d.severity === targetSeverity,
625
+ );
626
+ }
627
+ }
628
+
629
+ if (diagnostics.length === 0) {
630
+ return args.severity && args.severity !== "all"
631
+ ? `No ${args.severity} diagnostics found.`
632
+ : "No diagnostics found. File appears clean.";
633
+ }
634
+
635
+ return formatDiagnostics(diagnostics, args.filePath);
636
+ } catch (error) {
637
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
638
+ }
639
+ },
640
+ });
641
+
642
+ // ============ Helper Functions ============
643
+
644
+ function formatHoverResult(hover: HoverResult): string {
645
+ const contents = hover.contents;
646
+
647
+ if (typeof contents === "string") {
648
+ return contents;
649
+ }
650
+
651
+ if (Array.isArray(contents)) {
652
+ return contents
653
+ .map((c) => (typeof c === "string" ? c : c.value))
654
+ .join("\n\n");
655
+ }
656
+
657
+ if (typeof contents === "object" && "value" in contents) {
658
+ return contents.value;
659
+ }
660
+
661
+ return JSON.stringify(contents);
662
+ }
663
+
664
+ function formatUri(uri: string): string {
665
+ return uri.replace("file://", "").replace(process.cwd() + "/", "");
666
+ }
667
+
668
+ function formatLocation(loc: Location | LocationLink): string {
669
+ if ("targetUri" in loc) {
670
+ return `${formatUri(loc.targetUri)}:${loc.targetRange.start.line + 1}:${loc.targetRange.start.character}`;
671
+ }
672
+ return `${formatUri(loc.uri)}:${loc.range.start.line + 1}:${loc.range.start.character}`;
673
+ }
674
+
675
+ function formatLocations(
676
+ result: Location | Location[] | LocationLink[],
677
+ ): string {
678
+ const locations = Array.isArray(result) ? result : [result];
679
+
680
+ if (locations.length === 0) {
681
+ return "No locations found.";
682
+ }
683
+
684
+ if (locations.length === 1) {
685
+ return `Definition: ${formatLocation(locations[0] as Location | LocationLink)}`;
686
+ }
687
+
688
+ return `Found ${locations.length} location(s):\n${locations.map((l) => ` ${formatLocation(l as Location | LocationLink)}`).join("\n")}`;
689
+ }
690
+
691
+ function formatDocumentSymbols(
692
+ symbols: DocumentSymbol[] | SymbolInfo[],
693
+ indent = 0,
694
+ ): string {
695
+ const prefix = " ".repeat(indent);
696
+ const lines: string[] = [];
697
+
698
+ for (const sym of symbols) {
699
+ const kindName = getSymbolKindName(sym.kind);
700
+ const line =
701
+ "range" in sym
702
+ ? sym.range.start.line + 1
703
+ : "location" in sym
704
+ ? sym.location.range.start.line + 1
705
+ : "?";
706
+
707
+ lines.push(`${prefix}${kindName} ${sym.name} (line ${line})`);
708
+
709
+ if ("children" in sym && sym.children) {
710
+ lines.push(formatDocumentSymbols(sym.children, indent + 1));
711
+ }
712
+ }
713
+
714
+ return lines.join("\n");
715
+ }
716
+
717
+ function formatDiagnostics(
718
+ diagnostics: Diagnostic[],
719
+ filePath: string,
720
+ ): string {
721
+ const severityNames = ["", "Error", "Warning", "Info", "Hint"];
722
+
723
+ const sorted = [...diagnostics].sort((a, b) => {
724
+ const sevA = a.severity ?? 4;
725
+ const sevB = b.severity ?? 4;
726
+ if (sevA !== sevB) return sevA - sevB;
727
+ return a.range.start.line - b.range.start.line;
728
+ });
729
+
730
+ const lines = sorted.map((d) => {
731
+ const sev = severityNames[d.severity ?? 4] || "Unknown";
732
+ const line = d.range.start.line + 1;
733
+ const col = d.range.start.character + 1;
734
+ const source = d.source ? `[${d.source}]` : "";
735
+ const code = d.code ? ` (${d.code})` : "";
736
+ return `${sev}: ${filePath}:${line}:${col}${code} ${source}\n ${d.message}`;
737
+ });
738
+
739
+ const errorCount = diagnostics.filter((d) => d.severity === 1).length;
740
+ const warnCount = diagnostics.filter((d) => d.severity === 2).length;
741
+
742
+ let summary = `Found ${diagnostics.length} diagnostic(s)`;
743
+ if (errorCount > 0 || warnCount > 0) {
744
+ const parts: string[] = [];
745
+ if (errorCount > 0) parts.push(`${errorCount} error(s)`);
746
+ if (warnCount > 0) parts.push(`${warnCount} warning(s)`);
747
+ summary += `: ${parts.join(", ")}`;
748
+ }
749
+
750
+ return `${summary}\n\n${lines.join("\n\n")}`;
751
+ }
752
+
753
+ function getSymbolKindName(kind: number): string {
754
+ const kinds: Record<number, string> = {
755
+ 1: "File",
756
+ 2: "Module",
757
+ 3: "Namespace",
758
+ 4: "Package",
759
+ 5: "Class",
760
+ 6: "Method",
761
+ 7: "Property",
762
+ 8: "Field",
763
+ 9: "Constructor",
764
+ 10: "Enum",
765
+ 11: "Interface",
766
+ 12: "Function",
767
+ 13: "Variable",
768
+ 14: "Constant",
769
+ 15: "String",
770
+ 16: "Number",
771
+ 17: "Boolean",
772
+ 18: "Array",
773
+ 19: "Object",
774
+ 20: "Key",
775
+ 21: "Null",
776
+ 22: "EnumMember",
777
+ 23: "Struct",
778
+ 24: "Event",
779
+ 25: "Operator",
780
+ 26: "TypeParameter",
781
+ };
782
+ return kinds[kind] || `Kind(${kind})`;
783
+ }
784
+
331
785
  // Default export for single-tool registration (uses rename as primary)
332
786
  export default lsp_rename;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "type": "module",
6
6
  "repository": {