@sean.holung/minicode 0.3.1 → 0.3.3

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 (107) hide show
  1. package/README.md +52 -42
  2. package/dist/scripts/run-benchmarks.js +147 -0
  3. package/dist/src/agent/config.js +149 -40
  4. package/dist/src/agent/editable-config.js +314 -0
  5. package/dist/src/analysis/structural-analysis.js +379 -0
  6. package/dist/src/benchmark/evaluator.js +79 -0
  7. package/dist/src/benchmark/index.js +4 -0
  8. package/dist/src/benchmark/reporter.js +177 -0
  9. package/dist/src/benchmark/runner.js +100 -0
  10. package/dist/src/benchmark/task-loader.js +78 -0
  11. package/dist/src/benchmark/types.js +5 -0
  12. package/dist/src/cli/args.js +10 -0
  13. package/dist/src/cli/config-slash-command.js +135 -0
  14. package/dist/src/cli/plugin-install.js +69 -0
  15. package/dist/src/index.js +76 -6
  16. package/dist/src/indexer/cache.js +6 -4
  17. package/dist/src/indexer/code-map.js +41 -13
  18. package/dist/src/indexer/plugins/typescript.js +70 -23
  19. package/dist/src/indexer/project-index.js +175 -36
  20. package/dist/src/indexer/symbol-names.js +92 -0
  21. package/dist/src/model-utils.js +18 -0
  22. package/dist/src/serve/agent-bridge.js +203 -24
  23. package/dist/src/serve/mcp-server.js +405 -0
  24. package/dist/src/serve/server.js +165 -10
  25. package/dist/src/serve/websocket.js +8 -0
  26. package/dist/src/shared/graph-styles.js +119 -0
  27. package/dist/src/tools/find-path.js +75 -0
  28. package/dist/src/tools/find-references.js +7 -2
  29. package/dist/src/tools/get-dependencies.js +3 -2
  30. package/dist/src/tools/read-symbol.js +12 -5
  31. package/dist/src/tools/registry.js +3 -1
  32. package/dist/src/tools/search-code-map.js +4 -2
  33. package/dist/src/ui/app.js +1 -1
  34. package/dist/src/ui/cli-ink.js +79 -4
  35. package/dist/src/ui/components/header-bar.js +6 -2
  36. package/dist/src/ui/state/ui-store.js +5 -0
  37. package/dist/src/web/app.js +1124 -176
  38. package/dist/src/web/index.html +113 -3
  39. package/dist/src/web/style.css +973 -55
  40. package/dist/tests/agent.test.js +31 -0
  41. package/dist/tests/analysis-helpers.test.js +89 -0
  42. package/dist/tests/analysis-ui.test.js +29 -0
  43. package/dist/tests/benchmark-harness.test.js +527 -0
  44. package/dist/tests/config-api.test.js +143 -0
  45. package/dist/tests/config-integration.test.js +751 -0
  46. package/dist/tests/config-slash-command.test.js +106 -0
  47. package/dist/tests/config.test.js +42 -1
  48. package/dist/tests/context-indicator.test.js +220 -0
  49. package/dist/tests/editable-config.test.js +109 -0
  50. package/dist/tests/find-path.test.js +183 -0
  51. package/dist/tests/focus-tracker.test.js +62 -0
  52. package/dist/tests/graph-onboarding.test.js +55 -0
  53. package/dist/tests/graph-styles.test.js +65 -0
  54. package/dist/tests/indexer.test.js +137 -0
  55. package/dist/tests/mcp-and-plugin.test.js +186 -0
  56. package/dist/tests/model-client-openai.test.js +29 -0
  57. package/dist/tests/model-selection.test.js +136 -0
  58. package/dist/tests/model-utils.test.js +22 -0
  59. package/dist/tests/reasoning-effort.test.js +264 -0
  60. package/dist/tests/run-benchmarks.test.js +161 -0
  61. package/dist/tests/search-code-map.test.js +18 -0
  62. package/dist/tests/serve.integration.test.js +218 -2
  63. package/dist/tests/session-ui.test.js +21 -0
  64. package/dist/tests/session.test.js +50 -0
  65. package/dist/tests/settings-ui.test.js +30 -0
  66. package/dist/tests/structural-analysis.test.js +218 -0
  67. package/node_modules/@minicode/agent-sdk/README.md +80 -51
  68. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +16 -5
  69. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  70. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +51 -33
  71. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  72. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +14 -0
  73. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  74. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +3 -2
  75. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  76. package/node_modules/@minicode/agent-sdk/dist/src/index.js +2 -0
  77. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  78. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts +35 -0
  79. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts.map +1 -0
  80. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js +64 -0
  81. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js.map +1 -0
  82. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +7 -0
  83. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  84. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +5 -1
  85. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  86. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +83 -11
  87. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  88. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts +1 -0
  89. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts.map +1 -1
  90. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js +8 -1
  91. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js.map +1 -1
  92. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
  93. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +4 -1
  94. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
  95. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js +3 -1
  96. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js.map +1 -1
  97. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js +8 -2
  98. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js.map +1 -1
  99. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +9 -5
  101. package/plugin/.claude-plugin/plugin.json +12 -0
  102. package/plugin/.mcp.json +8 -0
  103. package/plugin/CLAUDE.md +26 -0
  104. package/plugin/skills/analyze/SKILL.md +12 -0
  105. package/plugin/skills/focus/SKILL.md +20 -0
  106. package/plugin/skills/graph/SKILL.md +13 -0
  107. package/plugin/skills/symbols/SKILL.md +13 -0
package/dist/src/index.js CHANGED
@@ -3,12 +3,14 @@ import process from "node:process";
3
3
  import { writeFile } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { CodingAgent, createModelClient } from "@minicode/agent-sdk";
6
- import { formatConfigForDisplay, loadAgentConfig } from "./agent/config.js";
6
+ import { loadAgentConfig, getConfigSetupMessage } from "./agent/config.js";
7
7
  import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "./session/session-store.js";
8
8
  import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "./indexer/cache.js";
9
9
  import { buildProjectIndex } from "./indexer/project-index.js";
10
+ import { sortModelsAlphabetically } from "./model-utils.js";
10
11
  import { createToolRegistry } from "./tools/registry.js";
11
12
  import { CliUsageError, parseCliArgs, validateCliArgs, } from "./cli/args.js";
13
+ import { handleConfigSlashCommand } from "./cli/config-slash-command.js";
12
14
  const EXIT_CODE_SUCCESS = 0;
13
15
  const EXIT_CODE_RUNTIME_ERROR = 1;
14
16
  const EXIT_CODE_USAGE_ERROR = 2;
@@ -18,6 +20,11 @@ function printBanner() {
18
20
  }
19
21
  async function createAgentRuntime(verbose, onProgress) {
20
22
  const config = await loadAgentConfig();
23
+ const setupMessage = getConfigSetupMessage(config);
24
+ if (setupMessage) {
25
+ console.error(setupMessage);
26
+ process.exit(EXIT_CODE_USAGE_ERROR);
27
+ }
21
28
  const modelClient = createModelClient(config);
22
29
  let projectIndex;
23
30
  try {
@@ -49,12 +56,12 @@ async function createAgentRuntime(verbose, onProgress) {
49
56
  ...(onProgress ? { onProgress } : {}),
50
57
  });
51
58
  }
52
- return { agent: buildAgent(), config, toolRegistry, projectIndex, buildAgent };
59
+ return { agent: buildAgent(), config, modelClient, toolRegistry, projectIndex, buildAgent };
53
60
  }
54
61
  async function runInteractive(verbose, initialTask) {
55
62
  const runtime = await createAgentRuntime(verbose, (msg) => console.error(` ${msg}`));
56
63
  let { agent } = runtime;
57
- const { config, buildAgent } = runtime;
64
+ const { config, modelClient, buildAgent } = runtime;
58
65
  printBanner();
59
66
  console.log(`Workspace: ${config.workspaceRoot}`);
60
67
  console.log(`Provider: ${config.modelProvider}`);
@@ -98,12 +105,13 @@ async function runInteractive(verbose, initialTask) {
98
105
  break;
99
106
  }
100
107
  if (trimmed === "/help") {
101
- console.log('Commands: "/help", "/config", "/compact", "/save [label]", "/load [label]", "/sessions", "/exit"');
108
+ console.log('Commands: "/help", "/config [keys|get|set|unset]", "/compact", "/reasoning [level]", "/models", "/model [name]", "/save [label]", "/load [label]", "/sessions", "/exit"');
102
109
  console.log("Start with --verbose or -v to log prompts, responses, and tool calls.");
103
110
  continue;
104
111
  }
105
- if (trimmed === "/config") {
106
- console.log("\n" + formatConfigForDisplay(config) + "\n");
112
+ const configCommand = await handleConfigSlashCommand(trimmed, { config });
113
+ if (configCommand.handled) {
114
+ console.log("\n" + (configCommand.message ?? "") + "\n");
107
115
  continue;
108
116
  }
109
117
  if (trimmed === "/compact") {
@@ -120,6 +128,63 @@ async function runInteractive(verbose, initialTask) {
120
128
  }
121
129
  continue;
122
130
  }
131
+ if (trimmed === "/reasoning" || trimmed.startsWith("/reasoning ")) {
132
+ const VALID_LEVELS = ["xhigh", "high", "medium", "low", "minimal", "none"];
133
+ const arg = trimmed.slice("/reasoning".length).trim().toLowerCase();
134
+ if (arg.length === 0) {
135
+ const current = agent.getReasoningEffort() ?? "(unset)";
136
+ console.log(`Current reasoning effort: ${current}`);
137
+ console.log(`Valid levels: ${VALID_LEVELS.join(", ")}, off`);
138
+ continue;
139
+ }
140
+ if (arg === "off") {
141
+ agent.setReasoningEffort(undefined);
142
+ console.log("Reasoning effort disabled.");
143
+ continue;
144
+ }
145
+ if (VALID_LEVELS.includes(arg)) {
146
+ agent.setReasoningEffort(arg);
147
+ console.log(`Reasoning effort set to: ${arg}`);
148
+ }
149
+ else {
150
+ console.log(`Invalid reasoning level "${arg}". Valid levels: ${VALID_LEVELS.join(", ")}, off`);
151
+ }
152
+ continue;
153
+ }
154
+ if (trimmed === "/models") {
155
+ if (modelClient.listModels) {
156
+ console.log("Fetching models...");
157
+ const models = sortModelsAlphabetically(await modelClient.listModels());
158
+ if (models.length === 0) {
159
+ console.log("No models found (provider may not support listing).");
160
+ }
161
+ else {
162
+ console.log(`Available models (${models.length}):`);
163
+ for (const m of models) {
164
+ const marker = m.id === config.model ? " (active)" : "";
165
+ console.log(` ${m.id}${marker}`);
166
+ }
167
+ }
168
+ }
169
+ else {
170
+ console.log("Current provider does not support listing models.");
171
+ }
172
+ continue;
173
+ }
174
+ if (trimmed.startsWith("/model ")) {
175
+ const newModel = trimmed.slice("/model ".length).trim();
176
+ if (newModel.length === 0) {
177
+ console.log(`Current model: ${config.model}`);
178
+ continue;
179
+ }
180
+ config.model = newModel;
181
+ console.log(`Model switched to: ${newModel}`);
182
+ continue;
183
+ }
184
+ if (trimmed === "/model") {
185
+ console.log(`Current model: ${config.model}`);
186
+ continue;
187
+ }
123
188
  if (trimmed === "/save" || trimmed.startsWith("/save ")) {
124
189
  const label = trimmed.slice("/save".length).trim() || undefined;
125
190
  try {
@@ -210,6 +275,11 @@ async function runOneshot(params) {
210
275
  async function main() {
211
276
  const cliArgs = parseCliArgs(process.argv);
212
277
  validateCliArgs(cliArgs);
278
+ if (cliArgs.pluginInstall) {
279
+ const { installPlugin } = await import("./cli/plugin-install.js");
280
+ await installPlugin();
281
+ return;
282
+ }
213
283
  if (cliArgs.serve) {
214
284
  const { runServe } = await import("./serve/server.js");
215
285
  await runServe(cliArgs.verbose, cliArgs.port);
@@ -20,19 +20,19 @@ export function getWorkspaceCacheDir(workspaceRoot) {
20
20
  function hashContent(content) {
21
21
  return createHash("sha256").update(content, "utf8").digest("hex");
22
22
  }
23
- async function collectSourceFiles(dir, root, files) {
23
+ async function collectSourceFiles(dir, root, files, validExtensions) {
24
24
  const entries = await readdir(dir, { withFileTypes: true });
25
25
  for (const entry of entries) {
26
26
  const fullPath = path.join(dir, entry.name);
27
27
  if (entry.isDirectory()) {
28
28
  if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
29
- await collectSourceFiles(fullPath, root, files);
29
+ await collectSourceFiles(fullPath, root, files, validExtensions);
30
30
  }
31
31
  continue;
32
32
  }
33
33
  if (entry.isFile()) {
34
34
  const ext = path.extname(entry.name).toLowerCase();
35
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
35
+ if (validExtensions.has(ext)) {
36
36
  files.push(path.relative(root, fullPath));
37
37
  }
38
38
  }
@@ -43,8 +43,10 @@ async function collectSourceFiles(dir, root, files) {
43
43
  */
44
44
  export async function computeFileHashes(workspaceRoot) {
45
45
  const root = path.resolve(workspaceRoot);
46
+ const plugins = await loadPlugins(root);
47
+ const validExtensions = new Set(plugins.flatMap((p) => p.extensions));
46
48
  const sourceFiles = [];
47
- await collectSourceFiles(root, root, sourceFiles);
49
+ await collectSourceFiles(root, root, sourceFiles, validExtensions);
48
50
  const hashes = new Map();
49
51
  for (const relPath of sourceFiles) {
50
52
  const absPath = path.join(root, relPath);
@@ -1,3 +1,4 @@
1
+ import { getSymbolDisplayName } from "./symbol-names.js";
1
2
  const DEFAULT_TOKEN_BUDGET = 1500;
2
3
  const APPROX_CHARS_PER_TOKEN = 4;
3
4
  function estimateTokens(text) {
@@ -7,39 +8,63 @@ function formatSymbol(symbol, indent, isMethod) {
7
8
  if (isMethod) {
8
9
  return `${indent} ${symbol.signature}`;
9
10
  }
10
- return `${indent}${symbol.kind} ${symbol.qualifiedName}\n${indent} ${symbol.signature}`;
11
+ return `${indent}${symbol.kind} ${getSymbolDisplayName(symbol)}\n${indent} ${symbol.signature}`;
11
12
  }
12
13
  function isEntryPointFile(filePath) {
13
14
  const name = filePath.replace(/\\/g, "/");
14
15
  return /(?:^|\/)index\.[jt]sx?$/.test(name);
15
16
  }
17
+ /**
18
+ * Build adjacency maps from edges for O(1) lookups per symbol.
19
+ */
20
+ function buildAdjacencyMaps(edges) {
21
+ const byFrom = new Map();
22
+ const byTo = new Map();
23
+ for (const edge of edges) {
24
+ const fromList = byFrom.get(edge.from);
25
+ if (fromList)
26
+ fromList.push(edge);
27
+ else
28
+ byFrom.set(edge.from, [edge]);
29
+ const toList = byTo.get(edge.to);
30
+ if (toList)
31
+ toList.push(edge);
32
+ else
33
+ byTo.set(edge.to, [edge]);
34
+ }
35
+ return { byFrom, byTo };
36
+ }
16
37
  /**
17
38
  * Build the set of symbols related to focus symbols via dependency edges.
18
39
  * Expands 1 hop outbound (what focus symbols depend on) and 1 hop inbound
19
40
  * (what depends on focus symbols).
20
41
  */
21
- function expandFocusSet(focusSymbols, edges) {
42
+ function expandFocusSet(focusSymbols, adjacency) {
22
43
  const expanded = new Set(focusSymbols);
23
- for (const edge of edges) {
44
+ for (const sym of focusSymbols) {
24
45
  // Outbound: focus symbol depends on something
25
- if (focusSymbols.has(edge.from)) {
26
- expanded.add(edge.to);
46
+ const outEdges = adjacency.byFrom.get(sym);
47
+ if (outEdges) {
48
+ for (const edge of outEdges)
49
+ expanded.add(edge.to);
27
50
  }
28
51
  // Inbound: something depends on focus symbol
29
- if (focusSymbols.has(edge.to)) {
30
- expanded.add(edge.from);
52
+ const inEdges = adjacency.byTo.get(sym);
53
+ if (inEdges) {
54
+ for (const edge of inEdges)
55
+ expanded.add(edge.from);
31
56
  }
32
57
  }
33
58
  return expanded;
34
59
  }
35
- function createSymbolRanker(edges, focusSymbols) {
60
+ function createSymbolRanker(adjacency, focusSymbols) {
36
61
  const refCount = new Map();
37
- for (const e of edges) {
38
- refCount.set(e.to, (refCount.get(e.to) ?? 0) + 1);
62
+ for (const [target, edges] of adjacency.byTo) {
63
+ refCount.set(target, edges.length);
39
64
  }
40
65
  // Expand focus set to include 1-hop neighbors in the dependency graph
41
66
  const boosted = focusSymbols?.size
42
- ? expandFocusSet(focusSymbols, edges)
67
+ ? expandFocusSet(focusSymbols, adjacency)
43
68
  : undefined;
44
69
  return (a, b) => {
45
70
  // Focus-boosted symbols always sort first
@@ -72,8 +97,11 @@ function createSymbolRanker(edges, focusSymbols) {
72
97
  export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGET, dependencyEdges, focusSymbols) {
73
98
  const totalCount = [...symbolsByFile.values()].reduce((sum, syms) => sum + syms.length, 0);
74
99
  const lines = ["# Project Code Map", ""];
100
+ const adjacency = dependencyEdges
101
+ ? buildAdjacencyMaps(dependencyEdges)
102
+ : { byFrom: new Map(), byTo: new Map() };
75
103
  const rank = dependencyEdges
76
- ? createSymbolRanker(dependencyEdges, focusSymbols)
104
+ ? createSymbolRanker(adjacency, focusSymbols)
77
105
  : (a, b) => (a.exported === b.exported ? 0 : a.exported ? -1 : 1);
78
106
  let totalTokens = estimateTokens(lines.join("\n"));
79
107
  let truncatedSymbols = 0;
@@ -82,7 +110,7 @@ export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGE
82
110
  // When we have focus symbols, sort files so that files containing
83
111
  // focused symbols come first in the code map.
84
112
  const boosted = focusSymbols?.size && dependencyEdges
85
- ? expandFocusSet(focusSymbols, dependencyEdges)
113
+ ? expandFocusSet(focusSymbols, adjacency)
86
114
  : undefined;
87
115
  const sortedFiles = [...symbolsByFile.keys()].sort((a, b) => {
88
116
  if (boosted) {
@@ -54,6 +54,7 @@ function extractJSDoc(node, sourceFile) {
54
54
  return cleaned.length > 0 ? cleaned : undefined;
55
55
  }
56
56
  function createPlugin() {
57
+ const astCache = new Map();
57
58
  return {
58
59
  name: "typescript",
59
60
  extensions: EXTENSIONS,
@@ -63,6 +64,7 @@ function createPlugin() {
63
64
  },
64
65
  indexFile(filePath, content) {
65
66
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
67
+ astCache.set(filePath, sourceFile);
66
68
  const symbols = [];
67
69
  let currentClass = null;
68
70
  function visit(node) {
@@ -230,44 +232,88 @@ function createPlugin() {
230
232
  },
231
233
  resolveDependencies(symbols, projectFiles) {
232
234
  const symbolSet = new Set(symbols.map((s) => s.qualifiedName));
235
+ const symbolsByLookup = new Map();
233
236
  const edges = [];
237
+ const edgeKeys = new Set();
234
238
  const rootDir = "/project";
239
+ function addLookup(key, symbol) {
240
+ if (key.length === 0)
241
+ return;
242
+ const existing = symbolsByLookup.get(key);
243
+ if (existing) {
244
+ existing.push(symbol);
245
+ }
246
+ else {
247
+ symbolsByLookup.set(key, [symbol]);
248
+ }
249
+ }
250
+ for (const symbol of symbols) {
251
+ const lookupKeys = new Set([
252
+ symbol.qualifiedName,
253
+ symbol.name,
254
+ symbol.originalQualifiedName ?? "",
255
+ symbol.displayName ?? "",
256
+ ...(symbol.aliases ?? []),
257
+ ]);
258
+ for (const key of lookupKeys) {
259
+ addLookup(key, symbol);
260
+ }
261
+ }
235
262
  function addEdge(from, to, kind) {
236
- if (symbolSet.has(to)) {
263
+ const edgeKey = `${from}->${to}:${kind}`;
264
+ if (symbolSet.has(from) && symbolSet.has(to) && !edgeKeys.has(edgeKey)) {
265
+ edgeKeys.add(edgeKey);
237
266
  edges.push({ from, to, kind });
238
267
  }
239
268
  }
240
- function collectTypeRefs(node, from) {
269
+ function resolveCandidates(rawName, filePath, kinds) {
270
+ const matches = symbolsByLookup.get(rawName) ?? [];
271
+ return matches.filter((symbol) => (filePath === undefined || symbol.filePath === filePath) &&
272
+ (kinds === undefined || kinds.includes(symbol.kind)));
273
+ }
274
+ function addResolvedEdges(rawFrom, rawTo, kind, filePath, targetKinds) {
275
+ const fromMatches = resolveCandidates(rawFrom, filePath);
276
+ const sameFileTargets = resolveCandidates(rawTo, filePath, targetKinds);
277
+ const toMatches = sameFileTargets.length > 0
278
+ ? sameFileTargets
279
+ : resolveCandidates(rawTo, undefined, targetKinds);
280
+ for (const fromSymbol of fromMatches) {
281
+ for (const toSymbol of toMatches) {
282
+ addEdge(fromSymbol.qualifiedName, toSymbol.qualifiedName, kind);
283
+ }
284
+ }
285
+ }
286
+ function collectTypeRefs(node, from, filePath) {
241
287
  if (ts.isTypeReferenceNode(node)) {
242
288
  const name = node.typeName.getText();
243
- addEdge(from, name, "references");
289
+ addResolvedEdges(from, name, "references", filePath, ["type", "interface", "class"]);
244
290
  if (ts.isQualifiedName(node.typeName)) {
245
291
  const left = node.typeName.left;
246
292
  if (ts.isIdentifier(left)) {
247
- addEdge(from, left.getText(), "references");
293
+ addResolvedEdges(from, left.getText(), "references", filePath, ["type", "interface", "class"]);
248
294
  }
249
295
  }
250
296
  }
251
- ts.forEachChild(node, (n) => collectTypeRefs(n, from));
297
+ ts.forEachChild(node, (n) => collectTypeRefs(n, from, filePath));
252
298
  }
253
- function collectCalls(node, from) {
299
+ function collectCalls(node, from, filePath) {
254
300
  if (ts.isCallExpression(node)) {
255
301
  const expr = node.expression;
256
302
  if (ts.isIdentifier(expr)) {
257
- addEdge(from, expr.getText(), "calls");
303
+ addResolvedEdges(from, expr.getText(), "calls", filePath, ["function", "class", "variable"]);
258
304
  }
259
305
  }
260
306
  if (ts.isNewExpression(node)) {
261
307
  const expr = node.expression;
262
308
  if (ts.isIdentifier(expr)) {
263
- addEdge(from, expr.getText(), "calls");
309
+ addResolvedEdges(from, expr.getText(), "calls", filePath, ["class", "function"]);
264
310
  }
265
311
  }
266
- ts.forEachChild(node, (n) => collectCalls(n, from));
312
+ ts.forEachChild(node, (n) => collectCalls(n, from, filePath));
267
313
  }
268
314
  for (const [filePath, content] of projectFiles) {
269
315
  const fullPath = path.join(rootDir, filePath);
270
- const sourceFile = ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true);
316
+ const sourceFile = astCache.get(filePath) ?? ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true);
271
317
  let currentClass = null;
272
318
  function visitClassNode(node, name) {
273
319
  const prevClass = currentClass;
@@ -284,7 +330,7 @@ function createPlugin() {
284
330
  const kind = clause.token === ts.SyntaxKind.ExtendsKeyword
285
331
  ? "extends"
286
332
  : "implements";
287
- addEdge(name, target, kind);
333
+ addResolvedEdges(name, target, kind, filePath, ["class", "interface"]);
288
334
  }
289
335
  }
290
336
  }
@@ -300,9 +346,9 @@ function createPlugin() {
300
346
  }
301
347
  if (ts.isConstructorDeclaration(node)) {
302
348
  const from = currentClass ? `${currentClass}.constructor` : "constructor";
303
- if (symbolSet.has(from)) {
304
- collectTypeRefs(node, from);
305
- collectCalls(node, from);
349
+ if (resolveCandidates(from, filePath).length > 0) {
350
+ collectTypeRefs(node, from, filePath);
351
+ collectCalls(node, from, filePath);
306
352
  }
307
353
  return;
308
354
  }
@@ -311,18 +357,18 @@ function createPlugin() {
311
357
  ? "[computed]"
312
358
  : node.name.getText(sourceFile);
313
359
  const from = currentClass ? `${currentClass}.${name}` : name;
314
- if (symbolSet.has(from)) {
315
- collectTypeRefs(node, from);
316
- collectCalls(node, from);
360
+ if (resolveCandidates(from, filePath).length > 0) {
361
+ collectTypeRefs(node, from, filePath);
362
+ collectCalls(node, from, filePath);
317
363
  }
318
364
  return;
319
365
  }
320
366
  if (ts.isFunctionDeclaration(node) && node.name) {
321
367
  const name = node.name.getText(sourceFile);
322
368
  const from = currentClass ? `${currentClass}.${name}` : name;
323
- if (symbolSet.has(from)) {
324
- collectTypeRefs(node, from);
325
- collectCalls(node, from);
369
+ if (resolveCandidates(from, filePath).length > 0) {
370
+ collectTypeRefs(node, from, filePath);
371
+ collectCalls(node, from, filePath);
326
372
  }
327
373
  return;
328
374
  }
@@ -333,9 +379,9 @@ function createPlugin() {
333
379
  continue;
334
380
  if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
335
381
  const name = decl.name.getText(sourceFile);
336
- if (symbolSet.has(name)) {
337
- collectTypeRefs(decl, name);
338
- collectCalls(decl, name);
382
+ if (resolveCandidates(name, filePath).length > 0) {
383
+ collectTypeRefs(decl, name, filePath);
384
+ collectCalls(decl, name, filePath);
339
385
  }
340
386
  }
341
387
  else if (ts.isClassExpression(init)) {
@@ -349,6 +395,7 @@ function createPlugin() {
349
395
  }
350
396
  visit(sourceFile);
351
397
  }
398
+ astCache.clear();
352
399
  return edges;
353
400
  },
354
401
  };