@mrclrchtr/supi-code-intelligence 0.1.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.
Files changed (146) hide show
  1. package/README.md +212 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
  52. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
  66. package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
  69. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
  70. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
  73. package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
  74. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
  127. package/package.json +47 -0
  128. package/src/actions/affected-action.ts +310 -0
  129. package/src/actions/brief-action.ts +242 -0
  130. package/src/actions/callees-action.ts +134 -0
  131. package/src/actions/callers-action.ts +215 -0
  132. package/src/actions/implementations-action.ts +190 -0
  133. package/src/actions/index-action.ts +187 -0
  134. package/src/actions/pattern-action.ts +232 -0
  135. package/src/architecture.ts +367 -0
  136. package/src/brief-focused.ts +383 -0
  137. package/src/brief.ts +228 -0
  138. package/src/code-intelligence.ts +122 -0
  139. package/src/git-context.ts +65 -0
  140. package/src/guidance.ts +39 -0
  141. package/src/index.ts +28 -0
  142. package/src/resolve-target.ts +104 -0
  143. package/src/search-helpers.ts +283 -0
  144. package/src/target-resolution.ts +368 -0
  145. package/src/tool-actions.ts +109 -0
  146. package/src/types.ts +57 -0
@@ -0,0 +1,358 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
+ import type { OverlayHandle } from "@earendil-works/pi-tui";
4
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
5
+ import type { LspManager } from "./manager/manager.ts";
6
+ import type { OutstandingDiagnosticSummaryEntry } from "./manager/manager-types.ts";
7
+ import type { Diagnostic, ProjectServerInfo } from "./types.ts";
8
+ import { DiagnosticSeverity } from "./types.ts";
9
+
10
+ export interface LspInspectorState {
11
+ handle: OverlayHandle | null;
12
+ close: (() => void) | null;
13
+ }
14
+
15
+ export function updateLspUi(
16
+ ctx: ExtensionContext,
17
+ manager: LspManager,
18
+ inlineSeverity: number,
19
+ servers: ProjectServerInfo[],
20
+ ): void {
21
+ const diagnostics = manager.getOutstandingDiagnosticSummary(inlineSeverity);
22
+
23
+ ctx.ui.setStatus("lsp", buildLspStatus(ctx, servers, diagnostics));
24
+ ctx.ui.setWidget(
25
+ "lsp",
26
+ hasWidgetContent(diagnostics)
27
+ ? (_tui, theme) => buildLspWidgetComponent(theme, diagnostics)
28
+ : undefined,
29
+ { placement: "belowEditor" },
30
+ );
31
+ }
32
+
33
+ // biome-ignore lint/complexity/useMaxParams: overlay inputs travel together
34
+ export function toggleLspStatusOverlay(
35
+ ctx: ExtensionContext,
36
+ manager: LspManager,
37
+ inlineSeverity: number,
38
+ inspector: LspInspectorState,
39
+ servers: ProjectServerInfo[],
40
+ ): void {
41
+ if (inspector.handle && inspector.close) {
42
+ inspector.close();
43
+ return;
44
+ }
45
+
46
+ void ctx.ui
47
+ .custom<void>(
48
+ (_tui, theme, _kb, done) => {
49
+ inspector.close = () => done(undefined);
50
+ return createLspInspectorComponent(theme, manager, inlineSeverity, servers);
51
+ },
52
+ {
53
+ overlay: true,
54
+ overlayOptions: {
55
+ anchor: "right-center",
56
+ width: "60%",
57
+ minWidth: 72,
58
+ maxHeight: "90%",
59
+ margin: { right: 1, top: 1, bottom: 1 },
60
+ nonCapturing: true,
61
+ },
62
+ onHandle: (handle) => {
63
+ inspector.handle = handle;
64
+ },
65
+ },
66
+ )
67
+ .finally(() => {
68
+ inspector.handle = null;
69
+ inspector.close = null;
70
+ });
71
+ }
72
+
73
+ function createLspInspectorComponent(
74
+ theme: ExtensionContext["ui"]["theme"],
75
+ manager: LspManager,
76
+ inlineSeverity: number,
77
+ servers: ProjectServerInfo[],
78
+ ): { render: (width: number) => string[]; invalidate: () => void } {
79
+ return {
80
+ render: (width) =>
81
+ buildLspInspectorContainer({
82
+ theme,
83
+ manager,
84
+ inlineSeverity,
85
+ servers,
86
+ width,
87
+ }).render(width),
88
+ invalidate: () => {},
89
+ };
90
+ }
91
+
92
+ interface LspInspectorContainerInput {
93
+ theme: ExtensionContext["ui"]["theme"];
94
+ manager: LspManager;
95
+ inlineSeverity: number;
96
+ servers: ProjectServerInfo[];
97
+ width: number;
98
+ }
99
+
100
+ function buildLspInspectorContainer(input: LspInspectorContainerInput): Container {
101
+ const diagnostics = input.manager.getOutstandingDiagnosticSummary(input.inlineSeverity);
102
+ const detailedDiagnostics = input.manager.getOutstandingDiagnostics(input.inlineSeverity);
103
+ const container = new Container();
104
+
105
+ const { theme, servers, width } = input;
106
+
107
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
108
+ container.addChild(
109
+ new Text(
110
+ theme.fg("accent", theme.bold(" λ LSP")) + theme.fg("dim", " inspector /lsp-status toggles"),
111
+ 1,
112
+ 0,
113
+ ),
114
+ );
115
+
116
+ if (servers.length === 0 && diagnostics.length === 0) {
117
+ container.addChild(
118
+ new Text(theme.fg("dim", "no LSP servers available for this project"), 1, 0),
119
+ );
120
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
121
+ return container;
122
+ }
123
+
124
+ container.addChild(new Text(buildOverlaySummaryLine(theme, servers, diagnostics), 1, 0));
125
+ container.addChild(new Spacer(1));
126
+ container.addChild(
127
+ buildOverlaySection(theme, "Servers", buildOverlayServerLines(theme, servers)),
128
+ );
129
+ container.addChild(new Spacer(1));
130
+ container.addChild(
131
+ buildOverlaySection(
132
+ theme,
133
+ diagnostics.length > 0 ? "Problems" : "Diagnostics",
134
+ buildOverlayDiagnosticLines(theme, diagnostics, detailedDiagnostics, width),
135
+ ),
136
+ );
137
+ container.addChild(new Spacer(1));
138
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
139
+
140
+ return container;
141
+ }
142
+
143
+ function buildLspStatus(
144
+ ctx: ExtensionContext,
145
+ servers: ProjectServerInfo[],
146
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
147
+ ): string | undefined {
148
+ const runningServers = servers.filter((server) => server.status === "running").length;
149
+ const openFiles = servers.reduce((sum, server) => sum + server.openFiles.length, 0);
150
+ const totals = collectDiagnosticTotals(diagnostics);
151
+
152
+ if (runningServers === 0 && openFiles === 0 && diagnostics.length === 0) {
153
+ return undefined;
154
+ }
155
+
156
+ const { theme } = ctx.ui;
157
+ const parts = [theme.fg("accent", "λ lsp")];
158
+ if (runningServers > 0) parts.push(theme.fg("dim", pluralize(runningServers, "server")));
159
+ if (openFiles > 0) parts.push(theme.fg("dim", pluralize(openFiles, "open file")));
160
+ if (totals.errors > 0) parts.push(theme.fg("error", pluralize(totals.errors, "error")));
161
+ if (totals.warnings > 0) parts.push(theme.fg("warning", pluralize(totals.warnings, "warning")));
162
+ if (totals.information > 0) parts.push(theme.fg("accent", pluralize(totals.information, "info")));
163
+ if (totals.hints > 0) parts.push(theme.fg("dim", pluralize(totals.hints, "hint")));
164
+ return parts.join(theme.fg("dim", " • "));
165
+ }
166
+
167
+ function hasWidgetContent(diagnostics: OutstandingDiagnosticSummaryEntry[]): boolean {
168
+ return diagnostics.length > 0;
169
+ }
170
+
171
+ function buildLspWidgetComponent(
172
+ theme: ExtensionContext["ui"]["theme"],
173
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
174
+ ): Container {
175
+ const container = new Container();
176
+
177
+ for (const line of buildWidgetDiagnosticLines(theme, diagnostics)) {
178
+ container.addChild(new Text(line, 0, 0));
179
+ }
180
+
181
+ return container;
182
+ }
183
+
184
+ function buildWidgetDiagnosticLines(
185
+ theme: ExtensionContext["ui"]["theme"],
186
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
187
+ ): string[] {
188
+ if (diagnostics.length === 1) {
189
+ const [entry] = diagnostics;
190
+ if (!entry) return [];
191
+ return [
192
+ `${theme.fg("error", "●")} ${entry.file} ${theme.fg("dim", `— ${formatDiagnosticCounts(entry)}`)}`,
193
+ ];
194
+ }
195
+
196
+ const totals = collectDiagnosticTotals(diagnostics);
197
+ const counts: string[] = [];
198
+ if (totals.errors > 0) counts.push(theme.fg("error", pluralize(totals.errors, "error")));
199
+ if (totals.warnings > 0) counts.push(theme.fg("warning", pluralize(totals.warnings, "warning")));
200
+ if (totals.information > 0)
201
+ counts.push(theme.fg("accent", pluralize(totals.information, "info")));
202
+ if (totals.hints > 0) counts.push(theme.fg("dim", pluralize(totals.hints, "hint")));
203
+
204
+ const visibleFiles = diagnostics.slice(0, 2).map((entry) => entry.file);
205
+ const remaining = diagnostics.length - visibleFiles.length;
206
+ const suffix = remaining > 0 ? `${theme.fg("dim", ` +${remaining} more`)}` : "";
207
+
208
+ return [
209
+ `${theme.fg("accent", theme.bold("λ LSP diagnostics"))} ${theme.fg("dim", `— ${pluralize(diagnostics.length, "file")}`)} ${counts.join(theme.fg("dim", " • "))}`,
210
+ `${theme.fg("error", "↳")} ${visibleFiles.join(", ")}${suffix}`,
211
+ ];
212
+ }
213
+
214
+ function buildOverlaySummaryLine(
215
+ theme: ExtensionContext["ui"]["theme"],
216
+ servers: ProjectServerInfo[],
217
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
218
+ ): string {
219
+ const runningServers = servers.filter((server) => server.status === "running").length;
220
+ const openFiles = servers.reduce((sum, server) => sum + server.openFiles.length, 0);
221
+ const totals = collectDiagnosticTotals(diagnostics);
222
+
223
+ const parts = [
224
+ theme.fg(
225
+ "dim",
226
+ `${pluralize(runningServers, "server")} • ${pluralize(openFiles, "open file")}`,
227
+ ),
228
+ ];
229
+ if (totals.errors > 0) {
230
+ parts.push(theme.fg("error", pluralize(totals.errors, "error")));
231
+ } else if (totals.warnings > 0) {
232
+ parts.push(theme.fg("warning", pluralize(totals.warnings, "warning")));
233
+ } else if (totals.information > 0) {
234
+ parts.push(theme.fg("accent", pluralize(totals.information, "info")));
235
+ } else if (totals.hints > 0) {
236
+ parts.push(theme.fg("dim", pluralize(totals.hints, "hint")));
237
+ } else {
238
+ parts.push(theme.fg("success", "clean"));
239
+ }
240
+
241
+ return parts.join(theme.fg("dim", " "));
242
+ }
243
+
244
+ function buildOverlaySection(
245
+ theme: ExtensionContext["ui"]["theme"],
246
+ title: string,
247
+ lines: string[],
248
+ ): Container {
249
+ const container = new Container();
250
+ container.addChild(new Text(theme.fg("accent", theme.bold(` ${title}`)), 1, 0));
251
+ for (const line of lines) {
252
+ container.addChild(new Text(line, 2, 0));
253
+ }
254
+ return container;
255
+ }
256
+
257
+ function buildOverlayServerLines(
258
+ theme: ExtensionContext["ui"]["theme"],
259
+ servers: ProjectServerInfo[],
260
+ ): string[] {
261
+ if (servers.length === 0) {
262
+ return [theme.fg("dim", "no LSP servers available for this project")];
263
+ }
264
+
265
+ return servers.flatMap((server) => {
266
+ const statusColor =
267
+ server.status === "running" ? "success" : server.status === "error" ? "error" : "warning";
268
+ const actions =
269
+ server.supportedActions.length > 0 ? server.supportedActions.join(", ") : "none";
270
+ const fileTypes = server.fileTypes.map((entry) => `.${entry}`).join(", ");
271
+ const openFiles =
272
+ server.openFiles.length > 0 ? server.openFiles.slice(0, 3).join(", ") : "none";
273
+ const remaining = server.openFiles.length - Math.min(server.openFiles.length, 3);
274
+ const suffix = remaining > 0 ? theme.fg("dim", ` +${remaining} more`) : "";
275
+
276
+ return [
277
+ `${theme.fg("accent", "◆")} ${server.name} ${theme.fg(statusColor, server.status)} ${theme.fg("dim", `— root: ${server.root}`)}`,
278
+ `${theme.fg("dim", "↳")} files: ${fileTypes}`,
279
+ `${theme.fg("dim", "↳")} actions: ${actions}`,
280
+ `${theme.fg("dim", "↳")} open: ${openFiles}${suffix}`,
281
+ ];
282
+ });
283
+ }
284
+
285
+ function buildOverlayDiagnosticLines(
286
+ theme: ExtensionContext["ui"]["theme"],
287
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
288
+ detailedDiagnostics: Array<{ file: string; diagnostics: Diagnostic[] }>,
289
+ width: number,
290
+ ): string[] {
291
+ if (diagnostics.length === 0) {
292
+ return [theme.fg("success", "✓ no outstanding diagnostics")];
293
+ }
294
+
295
+ const lines: string[] = [];
296
+ const maxFiles = 5;
297
+ const maxMessagesPerFile = 5;
298
+ // Budget for prefix: 2 spaces + tree char + space + line:col + space + Text paddingX(2)*2
299
+ const maxMessageLen = Math.max(24, width - 18);
300
+
301
+ for (const entry of detailedDiagnostics.slice(0, maxFiles)) {
302
+ const summary = diagnostics.find((d) => d.file === entry.file);
303
+ const countLabel = summary ? formatDiagnosticCounts(summary) : "";
304
+ lines.push(`${theme.fg("error", "●")} ${entry.file} ${theme.fg("dim", `— ${countLabel}`)}`);
305
+
306
+ for (const diag of entry.diagnostics.slice(0, maxMessagesPerFile)) {
307
+ const line = diag.range.start.line + 1;
308
+ const col = diag.range.start.character + 1;
309
+ const sevColor = diag.severity === DiagnosticSeverity.Error ? "error" : "warning";
310
+ const message = truncate(diag.message, maxMessageLen);
311
+ lines.push(` ${theme.fg(sevColor, "└")} ${line}:${col} ${theme.fg("dim", message)}`);
312
+ }
313
+
314
+ const remainingMessages = entry.diagnostics.length - maxMessagesPerFile;
315
+ if (remainingMessages > 0) {
316
+ lines.push(` ${theme.fg("dim", `└ +${remainingMessages} more`)}`);
317
+ }
318
+ }
319
+
320
+ const remainingFiles = diagnostics.length - Math.min(diagnostics.length, maxFiles);
321
+ if (remainingFiles > 0) {
322
+ lines.push(theme.fg("dim", `↳ +${remainingFiles} more file${remainingFiles === 1 ? "" : "s"}`));
323
+ }
324
+
325
+ return lines;
326
+ }
327
+
328
+ function truncate(text: string, maxLength: number): string {
329
+ if (text.length <= maxLength) return text;
330
+ return `${text.slice(0, maxLength - 1)}…`;
331
+ }
332
+
333
+ function formatDiagnosticCounts(entry: OutstandingDiagnosticSummaryEntry): string {
334
+ const counts: string[] = [];
335
+ if (entry.errors > 0) counts.push(pluralize(entry.errors, "error"));
336
+ if (entry.warnings > 0) counts.push(pluralize(entry.warnings, "warning"));
337
+ if (entry.information > 0) counts.push(pluralize(entry.information, "info"));
338
+ if (entry.hints > 0) counts.push(pluralize(entry.hints, "hint"));
339
+ return counts.join(", ");
340
+ }
341
+
342
+ function collectDiagnosticTotals(
343
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
344
+ ): Pick<OutstandingDiagnosticSummaryEntry, "errors" | "warnings" | "information" | "hints"> {
345
+ return diagnostics.reduce(
346
+ (totals, entry) => ({
347
+ errors: totals.errors + entry.errors,
348
+ warnings: totals.warnings + entry.warnings,
349
+ information: totals.information + entry.information,
350
+ hints: totals.hints + entry.hints,
351
+ }),
352
+ { errors: 0, warnings: 0, information: 0, hints: 0 },
353
+ );
354
+ }
355
+
356
+ function pluralize(count: number, word: string): string {
357
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
358
+ }
@@ -0,0 +1,122 @@
1
+ // URI and language utilities for LSP.
2
+
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ // ── URI Handling ──────────────────────────────────────────────────────
6
+
7
+ /** Convert a file path to a file:// URI. */
8
+ export function fileToUri(filePath: string): string {
9
+ const resolved = path.resolve(filePath);
10
+ if (process.platform === "win32") {
11
+ return `file:///${resolved.replace(/\\/g, "/")}`;
12
+ }
13
+ return `file://${resolved}`;
14
+ }
15
+
16
+ /** Convert a file:// URI to a file path. */
17
+ export function uriToFile(uri: string): string {
18
+ if (!uri.startsWith("file://")) return uri;
19
+ let filePath = decodeURIComponent(uri.slice(7));
20
+ if (
21
+ process.platform === "win32" &&
22
+ filePath.startsWith("/") &&
23
+ /^[A-Za-z]:/.test(filePath.slice(1))
24
+ ) {
25
+ filePath = filePath.slice(1);
26
+ }
27
+ return filePath;
28
+ }
29
+
30
+ // ── Language ID Detection ─────────────────────────────────────────────
31
+
32
+ const EXT_TO_LANGUAGE: Record<string, string> = {
33
+ ts: "typescript",
34
+ tsx: "typescriptreact",
35
+ js: "javascript",
36
+ jsx: "javascriptreact",
37
+ mts: "typescript",
38
+ cts: "typescript",
39
+ mjs: "javascript",
40
+ cjs: "javascript",
41
+ py: "python",
42
+ pyi: "python",
43
+ rs: "rust",
44
+ go: "go",
45
+ mod: "go",
46
+ c: "c",
47
+ h: "c",
48
+ cpp: "cpp",
49
+ hpp: "cpp",
50
+ cc: "cpp",
51
+ cxx: "cpp",
52
+ hxx: "cpp",
53
+ "c++": "cpp",
54
+ "h++": "cpp",
55
+ json: "json",
56
+ yaml: "yaml",
57
+ yml: "yaml",
58
+ md: "markdown",
59
+ html: "html",
60
+ htm: "html",
61
+ xhtml: "html",
62
+ css: "css",
63
+ scss: "scss",
64
+ sh: "shellscript",
65
+ bash: "shellscript",
66
+ zsh: "shellscript",
67
+ ksh: "shellscript",
68
+ toml: "toml",
69
+ xml: "xml",
70
+ sql: "sql",
71
+ r: "r",
72
+ rb: "ruby",
73
+ erb: "ruby",
74
+ gemspec: "ruby",
75
+ java: "java",
76
+ kt: "kotlin",
77
+ kts: "kotlin",
78
+ swift: "swift",
79
+ lua: "lua",
80
+ zig: "zig",
81
+ };
82
+
83
+ /** Detect the LSP languageId from a file path. */
84
+ export function detectLanguageId(filePath: string): string {
85
+ const ext = path.extname(filePath).slice(1).toLowerCase();
86
+ return EXT_TO_LANGUAGE[ext] ?? ext;
87
+ }
88
+
89
+ /** Get the file extension (without dot) from a file path. */
90
+ export function getFileExtension(filePath: string): string {
91
+ return path.extname(filePath).slice(1).toLowerCase();
92
+ }
93
+
94
+ // ── PATH Validation ───────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Check if a command exists on PATH.
98
+ * Uses synchronous check to avoid complexity.
99
+ */
100
+ export function commandExists(command: string): boolean {
101
+ // If it's an absolute path, check directly
102
+ if (path.isAbsolute(command)) {
103
+ return fs.existsSync(command);
104
+ }
105
+
106
+ const pathDirs = (process.env.PATH ?? "").split(path.delimiter);
107
+ const extensions =
108
+ process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
109
+
110
+ for (const dir of pathDirs) {
111
+ for (const ext of extensions) {
112
+ const fullPath = path.join(dir, command + ext);
113
+ try {
114
+ fs.accessSync(fullPath, fs.constants.X_OK);
115
+ return true;
116
+ } catch {
117
+ // Not found here, continue
118
+ }
119
+ }
120
+ }
121
+ return false;
122
+ }
@@ -0,0 +1,114 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { FileChangeType, type FileEvent } from "./types.ts";
4
+ import { fileToUri } from "./utils.ts";
5
+
6
+ const IGNORED_DIRECTORIES = new Set(["node_modules", ".pnpm", ".git", "dist", "coverage"]);
7
+ const ROOT_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
8
+
9
+ /** Build a fresh snapshot of workspace sentinel files and their mtimes. */
10
+ export function scanWorkspaceSentinels(cwd: string): Map<string, number> {
11
+ const resolvedCwd = path.resolve(cwd);
12
+ const snapshot = new Map<string, number>();
13
+
14
+ if (!fs.existsSync(resolvedCwd)) return snapshot;
15
+
16
+ try {
17
+ walkWorkspace(resolvedCwd, resolvedCwd, snapshot);
18
+ } catch {
19
+ return snapshot;
20
+ }
21
+
22
+ return snapshot;
23
+ }
24
+
25
+ /** Diff two sentinel snapshots into LSP file change events. */
26
+ export function diffWorkspaceSentinelSnapshot(
27
+ previous: Map<string, number>,
28
+ next: Map<string, number>,
29
+ ): FileEvent[] {
30
+ const changes: FileEvent[] = [];
31
+
32
+ for (const [filePath, mtime] of next) {
33
+ const previousMtime = previous.get(filePath);
34
+ if (previousMtime === undefined) {
35
+ changes.push({ uri: fileToUri(filePath), type: FileChangeType.Created });
36
+ continue;
37
+ }
38
+ if (previousMtime !== mtime) {
39
+ changes.push({ uri: fileToUri(filePath), type: FileChangeType.Changed });
40
+ }
41
+ }
42
+
43
+ for (const filePath of previous.keys()) {
44
+ if (next.has(filePath)) continue;
45
+ changes.push({ uri: fileToUri(filePath), type: FileChangeType.Deleted });
46
+ }
47
+
48
+ return changes.sort((a, b) => a.uri.localeCompare(b.uri));
49
+ }
50
+
51
+ /** Refresh a previous snapshot and return the new snapshot plus change events. */
52
+ export function syncWorkspaceSentinelSnapshot(
53
+ cwd: string,
54
+ previous: Map<string, number>,
55
+ ): { snapshot: Map<string, number>; changes: FileEvent[] } {
56
+ const snapshot = scanWorkspaceSentinels(cwd);
57
+ return {
58
+ snapshot,
59
+ changes: diffWorkspaceSentinelSnapshot(previous, snapshot),
60
+ };
61
+ }
62
+
63
+ /** Determine whether a file path should trigger workspace recovery. */
64
+ export function isWorkspaceRecoveryTrigger(filePath: string, cwd: string): boolean {
65
+ const root = path.resolve(cwd);
66
+ return isWorkspaceSentinelPath(path.resolve(root, filePath), root);
67
+ }
68
+
69
+ function walkWorkspace(root: string, directory: string, snapshot: Map<string, number>): void {
70
+ let entries: fs.Dirent[];
71
+ try {
72
+ entries = fs.readdirSync(directory, { withFileTypes: true });
73
+ } catch {
74
+ // Permission error or deleted directory — skip it rather than
75
+ // failing the entire scan. A partial snapshot still detects
76
+ // changes in accessible subtrees.
77
+ return;
78
+ }
79
+
80
+ for (const entry of entries) {
81
+ if (entry.isDirectory()) {
82
+ if (IGNORED_DIRECTORIES.has(entry.name)) continue;
83
+ walkWorkspace(root, path.join(directory, entry.name), snapshot);
84
+ continue;
85
+ }
86
+
87
+ if (!entry.isFile()) continue;
88
+
89
+ const filePath = path.join(directory, entry.name);
90
+ if (!isWorkspaceSentinelPath(filePath, root)) continue;
91
+ try {
92
+ snapshot.set(filePath, fs.statSync(filePath).mtimeMs);
93
+ } catch {
94
+ // File deleted between readdir and stat — skip.
95
+ }
96
+ }
97
+ }
98
+
99
+ function isWorkspaceSentinelPath(filePath: string, root: string): boolean {
100
+ const name = path.basename(filePath);
101
+
102
+ if (name === "package.json") return true;
103
+ if (name === "jsconfig.json") return true;
104
+ if (name === "tsconfig.json") return true;
105
+ if (name.startsWith("tsconfig.") && name.endsWith(".json")) return true;
106
+ if (filePath.endsWith(".d.ts")) return true;
107
+
108
+ return isRootLockfile(filePath, root);
109
+ }
110
+
111
+ function isRootLockfile(filePath: string, root: string): boolean {
112
+ if (path.dirname(filePath) !== root) return false;
113
+ return ROOT_LOCKFILES.includes(path.basename(filePath));
114
+ }
@@ -0,0 +1,97 @@
1
+ # @mrclrchtr/supi-tree-sitter
2
+
3
+ Tree-sitter structural analysis for the [pi coding agent](https://github.com/earendil-works/pi).
4
+
5
+ This package registers a `tree_sitter` tool and also exports a small TypeScript service API for other SuPi extensions. It is designed as a standalone structural-analysis substrate: it does not depend on `supi-lsp` or semantic language-server tooling, and it remains correct and useful when installed by itself.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:@mrclrchtr/supi-tree-sitter
11
+ ```
12
+
13
+ Standalone installs include the runtime grammar dependencies needed for the supported non-vendored languages. Kotlin and SQL use vendored WASM grammars bundled with this package.
14
+
15
+ It is also bundled by the full SuPi meta-package:
16
+
17
+ ```bash
18
+ pi install npm:@mrclrchtr/supi
19
+ ```
20
+
21
+ ## Supported files
22
+
23
+ The runtime can parse these file families:
24
+
25
+ - **JavaScript / TypeScript** — `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, `.cjs`
26
+ - **Python** — `.py`, `.pyi`
27
+ - **Rust** — `.rs`
28
+ - **Go** — `.go`, `.mod`
29
+ - **C / C++** — `.c`, `.h`, `.cpp`, `.hpp`, `.cc`, `.cxx`, `.hxx`, `.c++`, `.h++`
30
+ - **Java** — `.java`
31
+ - **Kotlin** — `.kt`, `.kts`
32
+ - **Ruby** — `.rb`
33
+ - **Bash / Shell** — `.sh`, `.bash`, `.zsh`
34
+ - **HTML** — `.html`, `.htm`, `.xhtml`
35
+ - **R** — `.r`
36
+ - **SQL** — `.sql`
37
+
38
+ Grammar `.wasm` files are resolved from installed package metadata for npm-shipped grammars, not from repository-relative paths.
39
+
40
+ ## `tree_sitter` tool
41
+
42
+ Actions:
43
+
44
+ - `outline` — list structural declarations such as functions, classes, interfaces, methods, and variables (**currently JavaScript / TypeScript only**)
45
+ - `imports` — list import statements and module specifiers (**currently JavaScript / TypeScript only**)
46
+ - `exports` — list exported declarations, re-exports, and TypeScript `export =` assignments (**currently JavaScript / TypeScript only**)
47
+ - `node_at` — return the smallest syntax node at a 1-based `line`/`character` position, plus ancestry (all supported grammars)
48
+ - `query` — run a custom Tree-sitter query and return captures (all supported grammars)
49
+ - `callees` — find outgoing function/method calls from a position; supports all grammars with a callee query configured
50
+
51
+ Coordinates are 1-based and compatible with the `lsp` tool. `character` is a UTF-16 code-unit column. Relative file paths resolve from the pi session working directory.
52
+
53
+ Large result sets are capped at 100 emitted items per tool response. For outlines, nested children count toward the same cap so deeply nested classes do not bypass the limit.
54
+
55
+ ## Service API
56
+
57
+ ```ts
58
+ import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter";
59
+
60
+ const session = createTreeSitterSession(process.cwd());
61
+ try {
62
+ const parseable = await session.canParse("src/index.ts");
63
+ if (parseable.kind === "success") {
64
+ console.log(parseable.data.file, parseable.data.language);
65
+ }
66
+
67
+ const outline = await session.outline("src/index.ts");
68
+ if (outline.kind === "success") {
69
+ console.log(outline.data);
70
+ }
71
+
72
+ const callees = await session.calleesAt("src/index.ts", 1, 10);
73
+ if (callees.kind === "success") {
74
+ console.log(callees.data.enclosingScope.name, callees.data.callees);
75
+ }
76
+ } finally {
77
+ session.dispose();
78
+ }
79
+ ```
80
+
81
+ `canParse(file)` validates that a supported file can be read and parsed, then returns the resolved file path and grammar id. It does not expose the raw Tree-sitter tree; use `outline`, `query`, `imports`, `exports`, `nodeAt`, or `calleesAt` for structured results.
82
+
83
+ `calleesAt(file, line, character)` extracts structural outgoing calls from the enclosing function/method scope at the given position. It returns the enclosing scope name and a deduplicated list of callees with their source ranges.
84
+
85
+ Exported types include `TreeSitterResult`, `TreeSitterSession`, `OutlineItem`, `ImportRecord`, `ExportRecord`, `NodeAtResult`, `QueryCapture`, `CalleesAtResult`, `SourceRange`, `GrammarId`, and `SupportedExtension`.
86
+
87
+ Always call `dispose()` when the session is no longer needed. The runtime lazily initializes grammars, reuses parser instances within a session, deduplicates concurrent first-use grammar initialization, and retries after initialization failures.
88
+
89
+ ## Positioning
90
+
91
+ `supi-tree-sitter` is the structural-analysis substrate in SuPi's layered code-understanding stack:
92
+
93
+ - `supi-tree-sitter` — parser-backed structural analysis (this package)
94
+ - `supi-lsp` — live semantic analysis through language servers
95
+ - `supi-code-intelligence` — higher-level agent-facing analysis built on top of both substrates
96
+
97
+ Each substrate can be installed and used independently. `supi-tree-sitter` does not require `supi-lsp` to be present, and its prompt guidance is written so it remains correct in standalone installs.