@sean.holung/minicode 0.3.2 → 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 +48 -43
  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
@@ -1,12 +1,19 @@
1
1
  import { CodingAgent, createModelClient } from "@minicode/agent-sdk";
2
+ import { readFile } from "node:fs/promises";
3
+ import { watch } from "node:fs";
4
+ import path from "node:path";
5
+ import { analyzeProjectStructure } from "../analysis/structural-analysis.js";
2
6
  import { loadAgentConfig } from "../agent/config.js";
3
7
  import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "../indexer/cache.js";
4
8
  import { buildProjectIndex } from "../indexer/project-index.js";
9
+ import { sortModelsAlphabetically } from "../model-utils.js";
5
10
  import { createToolRegistry } from "../tools/registry.js";
6
11
  import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "../session/session-store.js";
12
+ import { getSymbolDisplayName } from "../indexer/symbol-names.js";
7
13
  export class AgentBridge {
8
14
  agent;
9
15
  config;
16
+ modelClient;
10
17
  projectIndex;
11
18
  buildAgent;
12
19
  busy = false;
@@ -16,6 +23,8 @@ export class AgentBridge {
16
23
  listeners = new Set();
17
24
  pinnedSymbols = new Set();
18
25
  annotations = new Map();
26
+ fileWatcher = null;
27
+ reindexTimers = new Map();
19
28
  constructor(broadcast, verbose) {
20
29
  this.broadcast = broadcast;
21
30
  this.verbose = verbose;
@@ -34,7 +43,14 @@ export class AgentBridge {
34
43
  }
35
44
  async init() {
36
45
  const config = await loadAgentConfig();
37
- const modelClient = createModelClient(config);
46
+ let modelClient;
47
+ try {
48
+ modelClient = createModelClient(config);
49
+ }
50
+ catch {
51
+ // Model client may fail to initialize if API keys are missing.
52
+ // Serve mode continues so the web UI can show setup instructions.
53
+ }
38
54
  let projectIndex;
39
55
  try {
40
56
  const cacheDir = getWorkspaceCacheDir(config.workspaceRoot);
@@ -60,23 +76,94 @@ export class AgentBridge {
60
76
  };
61
77
  this.config = config;
62
78
  this.projectIndex = projectIndex;
63
- this.buildAgent = (session, onUiUpdate) => {
64
- return new CodingAgent({
65
- config,
66
- modelClient,
67
- toolRegistry,
68
- verbose: this.verbose,
69
- ...(session ? { session } : {}),
70
- ...(projectIndex !== undefined
71
- ? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
72
- : {}),
73
- onUiUpdate: onUiUpdate ?? ((event) => {
74
- this.emit(event);
75
- }),
76
- getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
79
+ if (modelClient) {
80
+ this.modelClient = modelClient;
81
+ this.buildAgent = (session, onUiUpdate) => {
82
+ return new CodingAgent({
83
+ config,
84
+ modelClient,
85
+ toolRegistry,
86
+ verbose: this.verbose,
87
+ ...(session ? { session } : {}),
88
+ ...(projectIndex !== undefined
89
+ ? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
90
+ : {}),
91
+ onUiUpdate: onUiUpdate ?? ((event) => {
92
+ this.emit(event);
93
+ }),
94
+ getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
95
+ });
96
+ };
97
+ this.agent = this.buildAgent();
98
+ }
99
+ }
100
+ // ── File watcher for automatic reindexing ──
101
+ static WATCH_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
102
+ static SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage"]);
103
+ static REINDEX_DEBOUNCE_MS = 300;
104
+ /**
105
+ * Start watching the workspace for file changes and reindex automatically.
106
+ * Call after init(). Safe to call if no project index exists (no-ops).
107
+ */
108
+ startFileWatcher() {
109
+ if (!this.projectIndex || this.fileWatcher)
110
+ return;
111
+ const workspaceRoot = this.config.workspaceRoot;
112
+ try {
113
+ this.fileWatcher = watch(workspaceRoot, { recursive: true }, (_event, filename) => {
114
+ if (!filename || !this.projectIndex)
115
+ return;
116
+ const normalized = filename.replace(/\\/g, "/");
117
+ // Skip ignored directories
118
+ const parts = normalized.split("/");
119
+ if (parts.some((p) => AgentBridge.SKIP_DIRS.has(p) || p.startsWith(".")))
120
+ return;
121
+ // Skip non-indexable extensions
122
+ const ext = path.extname(normalized).toLowerCase();
123
+ if (!AgentBridge.WATCH_EXTENSIONS.has(ext))
124
+ return;
125
+ // Debounce: if same file changes rapidly, only reindex once
126
+ const existing = this.reindexTimers.get(normalized);
127
+ if (existing)
128
+ clearTimeout(existing);
129
+ this.reindexTimers.set(normalized, setTimeout(() => {
130
+ this.reindexTimers.delete(normalized);
131
+ void this.reindexChangedFile(normalized);
132
+ }, AgentBridge.REINDEX_DEBOUNCE_MS));
77
133
  });
78
- };
79
- this.agent = this.buildAgent();
134
+ }
135
+ catch {
136
+ console.warn("[warn] File watcher could not start (recursive fs.watch may not be supported on this platform).\n" +
137
+ " File changes will not trigger automatic reindexing. Restart minicode serve to pick up changes.");
138
+ }
139
+ }
140
+ stopFileWatcher() {
141
+ if (this.fileWatcher) {
142
+ this.fileWatcher.close();
143
+ this.fileWatcher = null;
144
+ }
145
+ for (const timer of this.reindexTimers.values()) {
146
+ clearTimeout(timer);
147
+ }
148
+ this.reindexTimers.clear();
149
+ }
150
+ async reindexChangedFile(relPath) {
151
+ if (!this.projectIndex)
152
+ return;
153
+ const absPath = path.resolve(this.config.workspaceRoot, relPath);
154
+ try {
155
+ const content = await readFile(absPath, "utf-8");
156
+ this.projectIndex.reindexFile(relPath, content);
157
+ if (this.verbose) {
158
+ console.error(`[watch] Reindexed: ${relPath}`);
159
+ }
160
+ }
161
+ catch {
162
+ // File may have been deleted — ignore
163
+ }
164
+ }
165
+ isReady() {
166
+ return this.agent !== undefined;
80
167
  }
81
168
  isBusy() {
82
169
  return this.busy;
@@ -84,10 +171,20 @@ export class AgentBridge {
84
171
  getConfig() {
85
172
  return this.config;
86
173
  }
87
- getAgent() {
174
+ requireAgent() {
175
+ if (!this.agent) {
176
+ throw new Error("Agent is not configured. Set MODEL and provider settings in ~/.minicode/.env");
177
+ }
88
178
  return this.agent;
89
179
  }
180
+ getAgent() {
181
+ return this.requireAgent();
182
+ }
183
+ getCurrentSessionId() {
184
+ return this.requireAgent().getSession().id;
185
+ }
90
186
  async runTurn(message) {
187
+ const agent = this.requireAgent();
91
188
  if (this.busy) {
92
189
  throw new Error("busy");
93
190
  }
@@ -95,7 +192,7 @@ export class AgentBridge {
95
192
  this.abortController = new AbortController();
96
193
  try {
97
194
  this.emit({ type: "turn_start" });
98
- const result = await this.agent.runTurn(message, {
195
+ const result = await agent.runTurn(message, {
99
196
  signal: this.abortController.signal,
100
197
  });
101
198
  this.emit({
@@ -126,12 +223,16 @@ export class AgentBridge {
126
223
  }
127
224
  // Session operations
128
225
  async saveSess(label) {
226
+ const agent = this.requireAgent();
129
227
  const annotationsObj = this.annotations.size > 0
130
228
  ? Object.fromEntries(this.annotations)
131
229
  : undefined;
132
- return saveSession(this.agent.getSession(), label, annotationsObj);
230
+ return saveSession(agent.getSession(), label, annotationsObj);
133
231
  }
134
232
  async loadSess(label) {
233
+ if (!this.buildAgent) {
234
+ throw new Error("Agent is not configured.");
235
+ }
135
236
  const result = (await loadSessionByLabel(label)) ?? (await loadSession(label));
136
237
  if (!result)
137
238
  return null;
@@ -158,7 +259,7 @@ export class AgentBridge {
158
259
  const symbols = [];
159
260
  for (const sym of this.projectIndex.symbols.values()) {
160
261
  symbols.push({
161
- name: sym.name,
262
+ name: getSymbolDisplayName(sym),
162
263
  qualifiedName: sym.qualifiedName,
163
264
  kind: sym.kind,
164
265
  filePath: sym.filePath,
@@ -182,7 +283,7 @@ export class AgentBridge {
182
283
  if (cone.length === 0)
183
284
  return undefined;
184
285
  return cone.map((sym) => ({
185
- name: sym.name,
286
+ name: getSymbolDisplayName(sym),
186
287
  qualifiedName: sym.qualifiedName,
187
288
  kind: sym.kind,
188
289
  filePath: sym.filePath,
@@ -198,7 +299,14 @@ export class AgentBridge {
198
299
  // Find all edges pointing TO this symbol
199
300
  const refs = this.projectIndex.dependencyEdges
200
301
  .filter((e) => e.to === sym.qualifiedName || e.to === sym.name)
201
- .map((e) => ({ from: e.from, kind: e.kind }));
302
+ .map((e) => {
303
+ const fromSymbol = this.projectIndex.getSymbol(e.from);
304
+ return {
305
+ from: e.from,
306
+ kind: e.kind,
307
+ ...(fromSymbol ? { name: getSymbolDisplayName(fromSymbol) } : {}),
308
+ };
309
+ });
202
310
  return refs;
203
311
  }
204
312
  getCodeMap(tokenBudget) {
@@ -214,7 +322,7 @@ export class AgentBridge {
214
322
  for (const sym of this.projectIndex.symbols.values()) {
215
323
  nodes.push({
216
324
  id: sym.qualifiedName,
217
- name: sym.name,
325
+ name: getSymbolDisplayName(sym),
218
326
  kind: sym.kind,
219
327
  filePath: sym.filePath,
220
328
  exported: sym.exported,
@@ -227,6 +335,11 @@ export class AgentBridge {
227
335
  }));
228
336
  return { nodes, edges };
229
337
  }
338
+ getStructuralAnalysis() {
339
+ if (!this.projectIndex)
340
+ return undefined;
341
+ return analyzeProjectStructure(this.projectIndex);
342
+ }
230
343
  getPinnedSymbols() {
231
344
  return [...this.pinnedSymbols];
232
345
  }
@@ -337,10 +450,22 @@ export class AgentBridge {
337
450
  }
338
451
  return result;
339
452
  }
453
+ // ── Model selection ──
454
+ async listModels() {
455
+ if (this.modelClient?.listModels) {
456
+ return sortModelsAlphabetically(await this.modelClient.listModels());
457
+ }
458
+ return [];
459
+ }
460
+ switchModel(modelId) {
461
+ this.config.model = modelId;
462
+ }
340
463
  // ── Explain ──
341
464
  async explainSymbol(name, onEvent, signal) {
342
465
  if (!this.projectIndex)
343
466
  throw new Error("No project index");
467
+ if (!this.buildAgent)
468
+ throw new Error("Agent is not configured.");
344
469
  const sym = this.projectIndex.getSymbol(name);
345
470
  if (!sym)
346
471
  throw new Error(`Symbol "${name}" not found`);
@@ -353,4 +478,58 @@ Be concise but thorough.`;
353
478
  const result = await explainAgent.runTurn(prompt, opts);
354
479
  return result.text;
355
480
  }
481
+ async explainStructuralFinding(findingId, onEvent, signal) {
482
+ const report = this.getStructuralAnalysis();
483
+ if (!report)
484
+ throw new Error("No project index");
485
+ if (!this.buildAgent)
486
+ throw new Error("Agent is not configured.");
487
+ const finding = report.findings.find((item) => item.id === findingId);
488
+ if (!finding)
489
+ throw new Error(`Structural finding "${findingId}" not found`);
490
+ const explainAgent = this.buildAgent(undefined, onEvent);
491
+ const affectedSymbols = finding.symbols.length > 0
492
+ ? finding.symbols.slice(0, 8).join(", ")
493
+ : "(none)";
494
+ const affectedFiles = finding.files.length > 0
495
+ ? finding.files.slice(0, 6).join(", ")
496
+ : "(none)";
497
+ const rationale = finding.rationale.map((item) => `- ${item}`).join("\n");
498
+ const metrics = Object.entries(finding.metrics)
499
+ .map(([key, value]) => `- ${key}: ${String(value)}`)
500
+ .join("\n");
501
+ const prompt = `Interpret this deterministic structural analysis finding for the current codebase.
502
+
503
+ Finding type: ${finding.type}
504
+ Severity: ${finding.severity}
505
+ Title: ${finding.title}
506
+ Summary: ${finding.summary}
507
+
508
+ Affected symbols:
509
+ ${affectedSymbols}
510
+
511
+ Affected files:
512
+ ${affectedFiles}
513
+
514
+ Deterministic rationale:
515
+ ${rationale || "- No extra rationale provided."}
516
+
517
+ Metrics:
518
+ ${metrics || "- No metrics provided."}
519
+
520
+ Important constraints:
521
+ - This finding came from deterministic graph analysis, not from the model.
522
+ - Your job is to interpret the finding, not to replace it.
523
+ - Keep the explanation advisory and note where the signal may be noisy or incomplete.
524
+ - If useful, inspect a few affected symbols with read_symbol, get_dependencies, and find_references before explaining.
525
+
526
+ Respond with short sections for:
527
+ 1. What this likely means
528
+ 2. Why it may matter
529
+ 3. Caveats / false-positive risk
530
+ 4. Potential next steps`;
531
+ const opts = signal ? { signal } : undefined;
532
+ const result = await explainAgent.runTurn(prompt, opts);
533
+ return result.text;
534
+ }
356
535
  }