@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,122 @@
1
+ // Code Intelligence extension entry point — registers the `code_intel` tool with pi.
2
+ // Provides architecture briefs, caller/callee analysis, impact assessment, and pattern search.
3
+
4
+ import { StringEnum } from "@earendil-works/pi-ai";
5
+ import type { BeforeAgentStartEventResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { Type } from "typebox";
7
+ import { buildArchitectureModel } from "./architecture.ts";
8
+ import { generateOverview } from "./brief.ts";
9
+ import { promptGuidelines, promptSnippet, toolDescription } from "./guidance.ts";
10
+ import { type CodeIntelAction, executeAction } from "./tool-actions.ts";
11
+
12
+ const OVERVIEW_CUSTOM_TYPE = "code-intelligence-overview";
13
+
14
+ const CodeIntelActionEnum = StringEnum([
15
+ "brief",
16
+ "callers",
17
+ "callees",
18
+ "implementations",
19
+ "affected",
20
+ "pattern",
21
+ "index",
22
+ ] as const);
23
+
24
+ /**
25
+ * Register the `code_intel` tool and inject a lightweight architecture overview
26
+ * once per session.
27
+ */
28
+ export default function codeIntelligenceExtension(pi: ExtensionAPI) {
29
+ let hasInjectedOverview = false;
30
+
31
+ pi.on("session_start", (_event, ctx) => {
32
+ hasInjectedOverview = false;
33
+
34
+ // Scan active branch for existing overview to avoid duplicates on reload/resume
35
+ const branch = ctx.sessionManager.getBranch();
36
+ for (const entry of branch) {
37
+ if (entry.type === "custom_message" && entry.customType === OVERVIEW_CUSTOM_TYPE) {
38
+ hasInjectedOverview = true;
39
+ break;
40
+ }
41
+ }
42
+ });
43
+
44
+ pi.on(
45
+ "before_agent_start",
46
+ async (_event, ctx): Promise<BeforeAgentStartEventResult | undefined> => {
47
+ if (hasInjectedOverview) return;
48
+ hasInjectedOverview = true;
49
+
50
+ const model = await buildArchitectureModel(ctx.cwd);
51
+ if (!model || model.modules.length === 0) return;
52
+
53
+ const overview = generateOverview(model);
54
+ if (!overview) return;
55
+
56
+ return {
57
+ message: {
58
+ customType: OVERVIEW_CUSTOM_TYPE,
59
+ display: false,
60
+ content: overview,
61
+ },
62
+ };
63
+ },
64
+ );
65
+
66
+ pi.registerTool({
67
+ name: "code_intel",
68
+ label: "Code Intelligence",
69
+ description: toolDescription,
70
+ parameters: Type.Object({
71
+ action: CodeIntelActionEnum,
72
+ path: Type.Optional(
73
+ Type.String({ description: "Scope or focus path (package, directory, or file)" }),
74
+ ),
75
+ file: Type.Optional(
76
+ Type.String({ description: "Anchored target file (use with line/character)" }),
77
+ ),
78
+ line: Type.Optional(Type.Number({ description: "1-based line number for anchored target" })),
79
+ character: Type.Optional(
80
+ Type.Number({ description: "1-based character column (UTF-16) for anchored target" }),
81
+ ),
82
+ symbol: Type.Optional(
83
+ Type.String({ description: "Symbol name for discovery-based resolution" }),
84
+ ),
85
+ pattern: Type.Optional(
86
+ Type.String({
87
+ description: "Text search pattern (pattern action only; literal by default)",
88
+ }),
89
+ ),
90
+ regex: Type.Optional(
91
+ Type.Boolean({
92
+ description: "Use regex semantics for pattern action (default: false, literal search)",
93
+ }),
94
+ ),
95
+ kind: Type.Optional(Type.String({ description: "Symbol kind filter for discovery" })),
96
+ exportedOnly: Type.Optional(
97
+ Type.Boolean({ description: "Limit discovery to exported symbols" }),
98
+ ),
99
+ maxResults: Type.Optional(Type.Number({ description: "Maximum results to return" })),
100
+ contextLines: Type.Optional(Type.Number({ description: "Context lines around matches" })),
101
+ summary: Type.Optional(
102
+ Type.Boolean({
103
+ description:
104
+ "Aggregate counts by directory instead of line-level matches (pattern action only)",
105
+ }),
106
+ ),
107
+ }),
108
+ promptSnippet,
109
+ promptGuidelines,
110
+ // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
111
+ execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
112
+ const { content, details } = await executeAction(
113
+ params as unknown as { action: CodeIntelAction } & Record<string, unknown>,
114
+ { cwd: ctx.cwd },
115
+ );
116
+ return {
117
+ content: [{ type: "text", text: content }],
118
+ details,
119
+ };
120
+ },
121
+ });
122
+ }
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ function execGit(cwd: string, args: string[]): string {
4
+ return execFileSync("git", args, {
5
+ cwd,
6
+ encoding: "utf-8",
7
+ stdio: ["ignore", "pipe", "ignore"],
8
+ timeout: 5000,
9
+ });
10
+ }
11
+
12
+ export interface GitContext {
13
+ branch: string;
14
+ dirtyFiles: string[];
15
+ lastCommitMessage: string | null;
16
+ }
17
+
18
+ export function gatherGitContext(cwd: string): GitContext | null {
19
+ try {
20
+ const branch = execGit(cwd, ["branch", "--show-current"]).trim();
21
+
22
+ const status = execGit(cwd, ["status", "--porcelain"]).trim();
23
+
24
+ const dirtyFiles = status
25
+ .split("\n")
26
+ .filter(Boolean)
27
+ .map((line) => line.slice(3).trim());
28
+
29
+ let lastCommitMessage: string | null = null;
30
+ try {
31
+ lastCommitMessage = execGit(cwd, ["log", "-1", "--format=%s"]).trim();
32
+ } catch {
33
+ // No commits yet
34
+ }
35
+
36
+ return { branch, dirtyFiles, lastCommitMessage };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function formatGitContext(ctx: GitContext): string {
43
+ const lines: string[] = [];
44
+ lines.push("## Git Context");
45
+ lines.push("");
46
+ lines.push(`Branch: \`${ctx.branch}\``);
47
+ if (ctx.dirtyFiles.length > 0) {
48
+ lines.push(
49
+ `Uncommitted: ${ctx.dirtyFiles.length} file${ctx.dirtyFiles.length !== 1 ? "s" : ""}`,
50
+ );
51
+ for (const f of ctx.dirtyFiles.slice(0, 5)) {
52
+ lines.push(`- \`${f}\``);
53
+ }
54
+ if (ctx.dirtyFiles.length > 5) {
55
+ lines.push(`- _+${ctx.dirtyFiles.length - 5} more_`);
56
+ }
57
+ } else {
58
+ lines.push("Working tree clean.");
59
+ }
60
+ if (ctx.lastCommitMessage) {
61
+ lines.push(`Last commit: \`${ctx.lastCommitMessage}\``);
62
+ }
63
+ lines.push("");
64
+ return lines.join("\n");
65
+ }
@@ -0,0 +1,39 @@
1
+ // Prompt guidance and tool description for the code_intel tool.
2
+
3
+ export const toolDescription = `Code intelligence tool — architecture briefs, semantic relationships, impact analysis, structured text search, and project indexing.
4
+
5
+ Actions:
6
+ - brief: Architecture overview or focused brief for a project, package, directory, file, or anchored symbol
7
+ - callers: Find call sites for a symbol (LSP-first, heuristic text-search fallback)
8
+ - callees: Best-effort outgoing calls from a symbol (structural tree-sitter analysis across supported grammars)
9
+ - implementations: Find concrete implementations of an interface or abstract type
10
+ - affected: Blast-radius analysis — direct references, downstream dependents, risk level, likely tests
11
+ - pattern: Bounded text search with grouped matches, context lines, scope enforcement, and regex opt-in via \`regex: true\`
12
+ - index: Factual project map — file counts by language, top-level directory tree, landmark config files
13
+
14
+ Coordinates are 1-based (line, character) with UTF-16 character columns, matching lsp and tree_sitter conventions.
15
+ Relative paths resolve from the session working directory. A leading @ on path/file is stripped automatically.
16
+
17
+ Examples:
18
+ { "action": "brief" }
19
+ { "action": "brief", "path": "packages/supi-lsp/" }
20
+ { "action": "brief", "file": "packages/supi-lsp/lsp.ts", "line": 42, "character": 7 }
21
+ { "action": "callers", "symbol": "registerSettings", "path": "packages/supi-core/" }
22
+ { "action": "callees", "file": "src/handler.ts", "line": 88, "character": 12 }
23
+ { "action": "implementations", "symbol": "SessionLspService", "path": "packages/" }
24
+ { "action": "affected", "file": "packages/supi-core/index.ts", "line": 12, "character": 8 }
25
+ { "action": "pattern", "pattern": "registerSettings", "path": "packages/", "maxResults": 10 }
26
+ { "action": "pattern", "pattern": "register(Settings|Config)", "path": "packages/", "regex": true, "maxResults": 10 }`;
27
+
28
+ export const promptSnippet =
29
+ "Use the code_intel tool for architecture orientation, semantic relationships, impact analysis, and structured search before broad file reads.";
30
+
31
+ export const promptGuidelines = [
32
+ "Use `code_intel brief` before editing an unfamiliar package, directory, or file to get architecture context and reduce blind reads.",
33
+ "Use `code_intel affected` before changing exported APIs, shared helpers, config surfaces, or cross-package contracts to check blast radius and risk.",
34
+ "Use `code_intel callers` before modifying a function to verify all call sites; use `callees` and `implementations` for dependency and interface analysis.",
35
+ "Use `code_intel pattern` for bounded, scope-aware text search when the question is textual rather than semantic; it treats patterns as literal strings by default and supports `regex: true` when needed.",
36
+ "Use `code_intel index` for a factual project map (file counts, directory structure, landmark files) when you need to orient yourself in a new codebase.",
37
+ "After `code_intel` narrows the target, use raw `lsp` and `tree_sitter` tools for precise drill-down on exact symbols, types, or AST nodes.",
38
+ "Do not prefer `code_intel` over direct file reads or lower-level tools for trivial, already-localized edits or exact symbol/AST drill-down tasks.",
39
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Package root exports for @mrclrchtr/supi-code-intelligence.
2
+ // Peer extensions can import these APIs for programmatic access.
3
+
4
+ export type { ArchitectureModel, DependencyEdge, ModuleInfo } from "./architecture.ts";
5
+ export {
6
+ buildArchitectureModel,
7
+ findModuleForPath,
8
+ getDependencies,
9
+ getDependents,
10
+ } from "./architecture.ts";
11
+
12
+ export { generateFocusedBrief, generateOverview, generateProjectBrief } from "./brief.ts";
13
+ export type { ResolvedTarget, TargetResolutionResult } from "./target-resolution.ts";
14
+ export {
15
+ normalizePath,
16
+ resolveAnchoredTarget,
17
+ resolveSymbolTarget,
18
+ toZeroBased,
19
+ } from "./target-resolution.ts";
20
+
21
+ export type {
22
+ AffectedDetails,
23
+ BriefDetails,
24
+ CodeIntelResult,
25
+ ConfidenceMode,
26
+ DisambiguationCandidate,
27
+ SearchDetails,
28
+ } from "./types.ts";
@@ -0,0 +1,104 @@
1
+ import {
2
+ type ResolvedTarget,
3
+ resolveAnchoredTarget,
4
+ resolveSymbolTarget,
5
+ } from "./target-resolution.ts";
6
+ import type { ActionParams } from "./tool-actions.ts";
7
+
8
+ /**
9
+ * Resolve a target from action params. Returns either a ResolvedTarget
10
+ * or a string error/disambiguation message to return directly.
11
+ */
12
+ export async function resolveTarget(
13
+ params: ActionParams,
14
+ cwd: string,
15
+ ): Promise<ResolvedTarget | string> {
16
+ if (!params.file && !params.symbol) {
17
+ return "**Error:** Semantic actions require either anchored coordinates (`file`, `line`, `character`) or a `symbol` for discovery.";
18
+ }
19
+
20
+ if (params.file && params.line != null && params.character != null) {
21
+ return resolveAnchored(params.file, params.line, params.character, cwd);
22
+ }
23
+
24
+ if (params.file && !params.symbol) {
25
+ return "**Error:** Semantic actions with `file` require `line` and `character`, or provide `symbol` for discovery.";
26
+ }
27
+
28
+ if (params.symbol) {
29
+ return resolveBySymbol(params, cwd);
30
+ }
31
+
32
+ return "**Error:** Could not resolve target.";
33
+ }
34
+
35
+ function resolveAnchored(
36
+ file: string,
37
+ line: number,
38
+ character: number,
39
+ cwd: string,
40
+ ): ResolvedTarget | string {
41
+ const result = resolveAnchoredTarget(file, line, character, cwd);
42
+ if (result.kind === "error") return result.message;
43
+ if (result.kind === "resolved") return result.target;
44
+ return "**Error:** Unexpected disambiguation for anchored target.";
45
+ }
46
+
47
+ async function resolveBySymbol(
48
+ params: ActionParams,
49
+ cwd: string,
50
+ ): Promise<ResolvedTarget | string> {
51
+ const result = await resolveSymbolTarget(params.symbol ?? "", cwd, {
52
+ path: params.path,
53
+ kind: params.kind,
54
+ exportedOnly: params.exportedOnly,
55
+ });
56
+
57
+ if (result.kind === "error") return result.message;
58
+ if (result.kind === "resolved") return result.target;
59
+
60
+ return formatDisambiguation(params, result);
61
+ }
62
+
63
+ function formatDisambiguation(
64
+ params: ActionParams,
65
+ result: {
66
+ candidates: Array<{
67
+ name: string;
68
+ kind: string | null;
69
+ container: string | null;
70
+ file: string;
71
+ line: number;
72
+ character: number;
73
+ rank: number;
74
+ }>;
75
+ omittedCount: number;
76
+ },
77
+ ): string {
78
+ const lines: string[] = [];
79
+ lines.push(`# Disambiguation needed for \`${params.symbol}\``);
80
+ lines.push("");
81
+ const omitNote = result.omittedCount > 0 ? ` (+${result.omittedCount} more)` : "";
82
+ lines.push(
83
+ `Found ${result.candidates.length} candidates${omitNote}. Rerun with anchored coordinates:`,
84
+ );
85
+ lines.push("");
86
+
87
+ for (const c of result.candidates) {
88
+ const kind = c.kind ? ` (${c.kind})` : "";
89
+ const container = c.container ? ` in ${c.container}` : "";
90
+ lines.push(
91
+ `${c.rank}. **${c.name}**${kind}${container} — \`${c.file}\`:${c.line}:${c.character}`,
92
+ );
93
+ }
94
+
95
+ lines.push("");
96
+ if (result.candidates.length > 0) {
97
+ const first = result.candidates[0];
98
+ lines.push(
99
+ `Example: \`{ "action": "${params.action}", "file": "${first.file}", "line": ${first.line}, "character": ${first.character} }\``,
100
+ );
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }
@@ -0,0 +1,283 @@
1
+ // Shared search helpers for code_intel actions.
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import * as path from "node:path";
5
+
6
+ const LOW_SIGNAL_DIRS = new Set([
7
+ "node_modules",
8
+ ".git",
9
+ ".pnpm",
10
+ "dist",
11
+ "build",
12
+ "out",
13
+ ".next",
14
+ ".nuxt",
15
+ "coverage",
16
+ ".turbo",
17
+ ".cache",
18
+ "__pycache__",
19
+ ".tsbuildinfo",
20
+ ]);
21
+
22
+ /** Check if a file path contains an obviously low-signal directory segment. */
23
+ export function isLowSignalPath(filePath: string): boolean {
24
+ const segments = filePath.split(path.sep);
25
+ return segments.some((s) => LOW_SIGNAL_DIRS.has(s));
26
+ }
27
+
28
+ /** Convert a file:// URI to a file path, matching the normalization in supi-lsp. */
29
+ export function uriToFile(uri: string): string {
30
+ if (!uri.startsWith("file://")) return uri;
31
+ let filePath = decodeURIComponent(uri.slice(7));
32
+ if (
33
+ process.platform === "win32" &&
34
+ filePath.startsWith("/") &&
35
+ /^[A-Za-z]:/.test(filePath.slice(1))
36
+ ) {
37
+ filePath = filePath.slice(1);
38
+ }
39
+ return filePath;
40
+ }
41
+
42
+ /** Check whether a resolved file path is inside the current project (within cwd, not under node_modules or .pnpm). */
43
+ export function isInProjectPath(filePath: string, cwd: string): boolean {
44
+ const relativePath = path.relative(cwd, path.resolve(cwd, filePath));
45
+ if (relativePath.startsWith(`..${path.sep}`) || relativePath === "..") return false;
46
+ const normalized = relativePath.replaceAll("\\", "/");
47
+ return !(
48
+ normalized.includes("/node_modules/") ||
49
+ normalized.startsWith("node_modules/") ||
50
+ normalized.includes("/.pnpm/") ||
51
+ normalized.startsWith(".pnpm/")
52
+ );
53
+ }
54
+
55
+ /** Escape regex special characters. */
56
+ export function escapeRegex(s: string): string {
57
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
+ }
59
+
60
+ /** Normalize a file/path value: strip leading @, resolve relative to cwd. */
61
+ export function normalizePath(input: string, cwd: string): string {
62
+ const stripped = input.startsWith("@") ? input.slice(1) : input;
63
+ return path.resolve(cwd, stripped);
64
+ }
65
+
66
+ export interface RgMatch {
67
+ file: string;
68
+ line: number;
69
+ text: string;
70
+ /** Context lines surrounding this match (from -C flag). */
71
+ context?: Array<{ line: number; text: string }>;
72
+ }
73
+
74
+ /** Result of a ripgrep invocation for callers that need both matches and execution errors. */
75
+ export interface RipgrepRunResult {
76
+ /** Parsed ripgrep matches, if any. */
77
+ matches: RgMatch[];
78
+ /** Non-no-match ripgrep execution error text, such as invalid regex syntax. */
79
+ error?: string;
80
+ }
81
+
82
+ /**
83
+ * Run ripgrep with JSON output and parse matches, filtering low-signal paths.
84
+ *
85
+ * This helper preserves the historical behavior used by most `code_intel`
86
+ * actions: any ripgrep execution failure is treated like an empty match set.
87
+ * Call `runRipgrepDetailed()` when a caller needs to surface regex parse errors
88
+ * or other non-no-match failures to the agent.
89
+ */
90
+ export function runRipgrep(
91
+ pattern: string,
92
+ scopePath: string,
93
+ cwd: string,
94
+ opts?: { maxMatches?: number; contextLines?: number; filterLowSignal?: boolean },
95
+ ): RgMatch[] {
96
+ return runRipgrepDetailed(pattern, scopePath, cwd, opts).matches;
97
+ }
98
+
99
+ /**
100
+ * Run ripgrep and preserve non-no-match execution errors for callers that need
101
+ * to distinguish invalid regex syntax from a genuine empty search result.
102
+ */
103
+ export function runRipgrepDetailed(
104
+ pattern: string,
105
+ scopePath: string,
106
+ cwd: string,
107
+ opts?: { maxMatches?: number; contextLines?: number; filterLowSignal?: boolean },
108
+ ): RipgrepRunResult {
109
+ const filter = opts?.filterLowSignal ?? true;
110
+
111
+ try {
112
+ const result = execFileSync("rg", buildRipgrepArgs(pattern, scopePath, opts), {
113
+ encoding: "utf-8",
114
+ cwd,
115
+ timeout: 10000,
116
+ stdio: ["pipe", "pipe", "pipe"],
117
+ });
118
+
119
+ return { matches: parseRgJson(result, filter) };
120
+ } catch (err: unknown) {
121
+ return handleRipgrepError(err, filter);
122
+ }
123
+ }
124
+
125
+ function buildRipgrepArgs(
126
+ pattern: string,
127
+ scopePath: string,
128
+ opts?: { maxMatches?: number; contextLines?: number },
129
+ ): string[] {
130
+ const args = ["--json", "-m", String(opts?.maxMatches ?? 30)];
131
+ if ((opts?.contextLines ?? 0) > 0) {
132
+ args.push("-C", String(opts?.contextLines ?? 0));
133
+ }
134
+ args.push("-e", pattern, scopePath);
135
+ return args;
136
+ }
137
+
138
+ function handleRipgrepError(err: unknown, filterLowSignal: boolean): RipgrepRunResult {
139
+ if (!isExecError(err)) {
140
+ return { matches: [] };
141
+ }
142
+
143
+ const stdout = typeof err.stdout === "string" ? err.stdout : "";
144
+ const stderr = typeof err.stderr === "string" ? err.stderr.trim() : "";
145
+ const matches = stdout ? parseRgJson(stdout, filterLowSignal) : [];
146
+
147
+ if (err.status === 1) {
148
+ return { matches };
149
+ }
150
+
151
+ return {
152
+ matches,
153
+ ...(stderr ? { error: stderr } : {}),
154
+ };
155
+ }
156
+
157
+ function isExecError(err: unknown): err is { status: number; stdout?: unknown; stderr?: unknown } {
158
+ return typeof err === "object" && err !== null && "status" in err;
159
+ }
160
+
161
+ interface RawRgEvent {
162
+ type: string;
163
+ data?: {
164
+ path?: { text: string };
165
+ line_number?: number;
166
+ lines?: { text: string };
167
+ };
168
+ }
169
+
170
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: JSON ripgrep output parsing with context line collection
171
+ function parseRgJson(output: string, filterLowSignal: boolean): RgMatch[] {
172
+ const matches: RgMatch[] = [];
173
+ // Pending context lines that precede the next match
174
+ let pendingContext: Array<{ line: number; text: string }> = [];
175
+
176
+ for (const line of output.split("\n")) {
177
+ if (!line.trim()) continue;
178
+ let parsed: RawRgEvent;
179
+ try {
180
+ parsed = JSON.parse(line);
181
+ } catch {
182
+ continue;
183
+ }
184
+
185
+ if (parsed.type === "context" && parsed.data) {
186
+ const lineNum = parsed.data.line_number;
187
+ const text = (parsed.data.lines?.text ?? "").trim();
188
+ if (lineNum) pendingContext.push({ line: lineNum, text });
189
+ continue;
190
+ }
191
+
192
+ if (parsed.type === "match" && parsed.data) {
193
+ const filePath = parsed.data.path?.text;
194
+ const lineNum = parsed.data.line_number;
195
+ const text = (parsed.data.lines?.text ?? "").trim();
196
+ if (filePath && lineNum) {
197
+ // Split pendingContext: lines before this match are "before" context,
198
+ // lines from before this match line that trail the previous match stay with it.
199
+ const beforeCtx = pendingContext.filter((c) => c.line < lineNum);
200
+ const trailingCtx = pendingContext.filter((c) => c.line >= lineNum);
201
+
202
+ // Attach trailing context from previous match to the last accepted match
203
+ if (beforeCtx.length > 0 && matches.length > 0) {
204
+ const prev = matches[matches.length - 1];
205
+ const prevTrailing = beforeCtx.filter((c) => c.line > prev.line);
206
+ if (prevTrailing.length > 0) {
207
+ prev.context = [...(prev.context ?? []), ...prevTrailing];
208
+ }
209
+ }
210
+
211
+ if (filterLowSignal && isLowSignalPath(filePath)) {
212
+ pendingContext = [];
213
+ continue;
214
+ }
215
+
216
+ const match: RgMatch = { file: filePath, line: lineNum, text };
217
+ // Leading context: lines just before this match
218
+ const leadingCtx =
219
+ matches.length > 0
220
+ ? beforeCtx.filter(
221
+ (c) => c.line <= lineNum && c.line > (matches[matches.length - 1]?.line ?? 0),
222
+ )
223
+ : beforeCtx;
224
+ if (leadingCtx.length > 0) {
225
+ match.context = leadingCtx;
226
+ }
227
+ // Also include any pending context at/after this match's line (trailing from rg ordering)
228
+ pendingContext = trailingCtx;
229
+ matches.push(match);
230
+ } else {
231
+ pendingContext = [];
232
+ }
233
+ }
234
+ }
235
+
236
+ // Attach any trailing context lines to the last match
237
+ if (pendingContext.length > 0 && matches.length > 0) {
238
+ const last = matches[matches.length - 1];
239
+ last.context = [...(last.context ?? []), ...pendingContext];
240
+ }
241
+
242
+ return matches;
243
+ }
244
+
245
+ /**
246
+ * Minimal interface for a reference-like object with a URI and range start.
247
+ * Used by {@link filterOutDeclaration} to avoid coupling to a specific LspRef type.
248
+ */
249
+ export interface HasLspPosition {
250
+ uri: string;
251
+ range: { start: { line: number; character: number } };
252
+ }
253
+
254
+ /**
255
+ * Filter out the declaration/definition location from LSP references.
256
+ * LSP's `textDocument/references` includes the declaration by default;
257
+ * for callers/affected analysis, the declaration is not a call site or affected reference.
258
+ *
259
+ * Uses {@link uriToFile} for robust URI-to-path conversion.
260
+ */
261
+ export function filterOutDeclaration<T extends HasLspPosition>(
262
+ refs: T[],
263
+ targetFile: string,
264
+ targetPos: { line: number; character: number },
265
+ ): T[] {
266
+ return refs.filter((ref) => {
267
+ const filePath = uriToFile(ref.uri);
268
+ if (filePath !== targetFile) return true;
269
+ const start = ref.range.start;
270
+ return start.line !== targetPos.line || start.character !== targetPos.character;
271
+ });
272
+ }
273
+
274
+ /** Group matches by file. */
275
+ export function groupByFile(matches: RgMatch[]): Map<string, RgMatch[]> {
276
+ const byFile = new Map<string, RgMatch[]>();
277
+ for (const m of matches) {
278
+ const group = byFile.get(m.file) ?? [];
279
+ group.push(m);
280
+ byFile.set(m.file, group);
281
+ }
282
+ return byFile;
283
+ }