micro-models-agent 0.63.0 → 1.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 (186) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +8 -4
  41. package/dist/i18n/ru.json +8 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1755 -841
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/bridge-server.mjs +37 -4
  59. package/dist/modules/browser/driver.js +46 -4
  60. package/dist/modules/certification/cli.js +85 -42
  61. package/dist/modules/certification/loader.js +15 -1
  62. package/dist/modules/certification/manifest.js +126 -15
  63. package/dist/modules/certification/runner.js +4 -26
  64. package/dist/modules/certification/scenarios.js +184 -5
  65. package/dist/modules/certification/syntax-scenarios.js +51 -0
  66. package/dist/modules/context/chunk-query.js +25 -5
  67. package/dist/modules/context/fact-extractor.js +6 -2
  68. package/dist/modules/context/manager.js +23 -7
  69. package/dist/modules/execution/audit-runners.js +7 -1
  70. package/dist/modules/execution/auditor.js +3 -3
  71. package/dist/modules/execution/execution-plugin.js +22 -15
  72. package/dist/modules/execution/input-from.js +46 -0
  73. package/dist/modules/execution/module.js +107 -18
  74. package/dist/modules/execution/moe-executor.js +166 -54
  75. package/dist/modules/execution/plan-actions.js +524 -0
  76. package/dist/modules/execution/plan-steps.js +23 -0
  77. package/dist/modules/execution/plan-store.js +15 -3
  78. package/dist/modules/execution/plan-tool.js +6 -488
  79. package/dist/modules/execution/plan-validator.js +24 -0
  80. package/dist/modules/execution/stuck-detector.js +3 -18
  81. package/dist/modules/execution/tracker.js +14 -5
  82. package/dist/modules/execution/transient-error.js +30 -0
  83. package/dist/modules/execution/verifier.js +94 -7
  84. package/dist/modules/execution/windows-commands.js +11 -0
  85. package/dist/modules/hallucination/confidence.js +36 -23
  86. package/dist/modules/hallucination/consistency.js +3 -0
  87. package/dist/modules/hallucination/detector.js +8 -3
  88. package/dist/modules/hallucination/factual.js +26 -7
  89. package/dist/modules/hallucination/llm-judge.js +12 -2
  90. package/dist/modules/indexer/map-command.js +35 -0
  91. package/dist/modules/indexer/map-select.js +87 -0
  92. package/dist/modules/indexer/module.js +34 -22
  93. package/dist/modules/indexer/symbols.js +189 -0
  94. package/dist/modules/indexer/walker.js +96 -42
  95. package/dist/modules/lsp/check-tool.js +2 -1
  96. package/dist/modules/lsp/client.js +49 -32
  97. package/dist/modules/lsp/config.js +55 -2
  98. package/dist/modules/lsp/module.js +38 -5
  99. package/dist/modules/lsp/probe.js +4 -3
  100. package/dist/modules/lsp/project-root.js +41 -1
  101. package/dist/modules/lsp/startup-check.js +12 -4
  102. package/dist/modules/mcp/client.js +153 -104
  103. package/dist/modules/mcp/module.js +165 -41
  104. package/dist/modules/memory/module.js +4 -3
  105. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  106. package/dist/modules/plugins/manager.js +47 -84
  107. package/dist/modules/pricing/index.js +17 -7
  108. package/dist/modules/pricing/prices.js +30 -12
  109. package/dist/modules/processes/index.js +1 -0
  110. package/dist/modules/processes/kill-tree.js +56 -0
  111. package/dist/modules/processes/registry.js +2 -54
  112. package/dist/modules/providers/cache.js +23 -0
  113. package/dist/modules/providers/factory.js +28 -0
  114. package/dist/modules/providers/fallback.js +7 -5
  115. package/dist/modules/providers/health.js +2 -1
  116. package/dist/modules/providers/index.js +1 -0
  117. package/dist/modules/providers/manager.js +17 -2
  118. package/dist/modules/providers/presets.js +79 -6
  119. package/dist/modules/reasoning/policy.js +40 -0
  120. package/dist/modules/reasoning/probe.js +111 -0
  121. package/dist/modules/security/audit-notifier.js +42 -27
  122. package/dist/modules/security/command-validator.js +25 -20
  123. package/dist/modules/security/encryption.js +6 -12
  124. package/dist/modules/security/network-validator.js +76 -5
  125. package/dist/modules/security/path-validator.js +77 -34
  126. package/dist/modules/security/rate-limiter.js +11 -0
  127. package/dist/modules/security/security-policies.js +1 -1
  128. package/dist/modules/security/session-encryption.js +13 -2
  129. package/dist/modules/security/session-isolation.js +2 -9
  130. package/dist/modules/session/manager.js +11 -0
  131. package/dist/modules/session/module.js +11 -3
  132. package/dist/modules/session/store.js +41 -5
  133. package/dist/modules/skills/loader.js +7 -1
  134. package/dist/modules/skills/module.js +2 -1
  135. package/dist/modules/updater/changelog-reader.js +94 -0
  136. package/dist/modules/updater/dev-detect.js +17 -0
  137. package/dist/modules/updater/index.js +1 -0
  138. package/dist/modules/updater/module.js +14 -3
  139. package/dist/output/bus.js +32 -0
  140. package/dist/output/channel.js +233 -0
  141. package/dist/output/format.js +14 -0
  142. package/dist/output/index.js +7 -0
  143. package/dist/output/json-sink.js +22 -0
  144. package/dist/output/machine.js +8 -0
  145. package/dist/output/session-sink.js +27 -0
  146. package/dist/output/types.js +1 -0
  147. package/dist/tools/approve.js +6 -2
  148. package/dist/tools/attach-image.js +11 -11
  149. package/dist/tools/auto-fixer.js +198 -0
  150. package/dist/tools/bash.js +142 -89
  151. package/dist/tools/chunk-query.js +10 -6
  152. package/dist/tools/download-file.js +1 -1
  153. package/dist/tools/edit-file.js +20 -2
  154. package/dist/tools/executor.js +54 -9
  155. package/dist/tools/glob-tool.js +7 -0
  156. package/dist/tools/grep-tool.js +15 -1
  157. package/dist/tools/index.js +3 -1
  158. package/dist/tools/list-dir.js +3 -1
  159. package/dist/tools/load-skill.js +2 -1
  160. package/dist/tools/mcp-call.js +1 -1
  161. package/dist/tools/move-file.js +5 -4
  162. package/dist/tools/path-utils.js +7 -0
  163. package/dist/tools/pipeline-run.js +1 -1
  164. package/dist/tools/prompt-io.js +28 -0
  165. package/dist/tools/question.js +12 -12
  166. package/dist/tools/scope-request.js +91 -0
  167. package/dist/tools/session-info.js +44 -0
  168. package/dist/tools/set-thinking.js +71 -0
  169. package/dist/tools/subagent.js +50 -9
  170. package/dist/tools/syntax-validator.js +177 -0
  171. package/dist/tools/user-input.js +16 -9
  172. package/dist/tools/write-file.js +17 -1
  173. package/dist/ui/diff.js +10 -0
  174. package/dist/ui/line-editor.js +179 -26
  175. package/dist/ui/line-math.js +20 -3
  176. package/dist/ui/md-formatter.js +100 -10
  177. package/dist/ui/output.js +5 -4
  178. package/dist/ui/plan-view.js +2 -7
  179. package/dist/ui/renderer.js +89 -85
  180. package/dist/ui/spinner.js +14 -4
  181. package/dist/utils/error.js +4 -0
  182. package/dist/utils/index.js +4 -0
  183. package/dist/utils/retry.js +17 -0
  184. package/dist/utils/sleep.js +23 -0
  185. package/dist/utils/truncate.js +9 -0
  186. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { MCPClient } from "./client";
2
2
  import { MCPRegistry } from "./registry";
3
+ import { Logger } from "../../logger/app-logger";
4
+ const logger = new Logger("warn", "mcp");
3
5
  function sanitizeToolName(server, tool) {
4
6
  const safeServer = server.replace(/[^a-zA-Z0-9_-]/g, "_");
5
7
  const safeTool = tool.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -16,16 +18,27 @@ function convertInputSchema(schema) {
16
18
  }
17
19
  return { type: "object", properties: {} };
18
20
  }
21
+ /** Timeout for the initial tool discovery call (avoids hanging startup). */
22
+ const DISCOVERY_TIMEOUT_MS = 5_000;
19
23
  export class MCPModule {
20
24
  config;
21
25
  name = "mcp";
22
26
  connections = new Map();
27
+ /** Server configs registered at init, not yet connected. */
28
+ serverConfigs = new Map();
23
29
  discovered = [];
24
30
  initialized = false;
25
31
  initError = null;
32
+ /** Servers that failed initial discovery — will not be retried. */
33
+ failedServers = new Set();
26
34
  constructor(config) {
27
35
  this.config = config;
28
36
  }
37
+ /**
38
+ * Register MCP server configs WITHOUT connecting. Actual connection and tool
39
+ * discovery happens lazily on first tool call — keeps startup fast and
40
+ * offline-safe.
41
+ */
29
42
  async initialize() {
30
43
  if (this.initialized)
31
44
  return;
@@ -48,14 +61,76 @@ export class MCPModule {
48
61
  };
49
62
  registry.register(serverConfig);
50
63
  }
64
+ // Store configs for lazy connection — no network calls here.
51
65
  for (const serverName of registry.list()) {
52
- const serverConfig = registry.get(serverName);
66
+ this.serverConfigs.set(serverName, registry.get(serverName));
67
+ }
68
+ // Attempt fast discovery in background (best-effort, timeout-bounded).
69
+ // If it fails, tools are still registered — they'll connect on first call.
70
+ this.discoverAllTools();
71
+ }
72
+ /**
73
+ * Try to connect and discover tools for all registered servers.
74
+ * Runs in background, never blocks the caller. Failures are silent —
75
+ * tools still get registered and will retry connection on first call.
76
+ */
77
+ async discoverAllTools() {
78
+ for (const [serverName, serverConfig] of this.serverConfigs) {
79
+ if (this.failedServers.has(serverName))
80
+ continue;
53
81
  const client = new MCPClient(serverConfig);
54
82
  try {
55
- await client.connect();
56
- const tools = await client.listTools();
57
- this.connections.set(serverName, client);
58
- for (const tool of tools) {
83
+ // Race discovery against timeout — never let a dead server block startup.
84
+ await Promise.race([
85
+ (async () => {
86
+ await client.connect();
87
+ const tools = await client.listTools();
88
+ this.connections.set(serverName, client);
89
+ for (const tool of tools) {
90
+ this.discovered.push({
91
+ serverName,
92
+ toolName: tool.name,
93
+ description: tool.description || `Tool on MCP server "${serverName}"`,
94
+ inputSchema: tool.inputSchema || {},
95
+ });
96
+ }
97
+ })(),
98
+ new Promise((_, reject) => setTimeout(() => reject(new Error("discovery timeout")), DISCOVERY_TIMEOUT_MS)),
99
+ ]);
100
+ }
101
+ catch (e) {
102
+ this.failedServers.add(serverName);
103
+ this.initError = `MCP server "${serverName}" init failed: ${e.message}`;
104
+ await client.disconnect().catch((err) => {
105
+ logger.warn(`cleanup disconnect for "${serverName}" failed`, { error: String(err) });
106
+ });
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * Ensure a specific server is connected and its tools are discovered.
112
+ * Called lazily on first tool invocation for that server.
113
+ */
114
+ async ensureConnected(serverName) {
115
+ // Already connected?
116
+ const existing = this.connections.get(serverName);
117
+ if (existing?.isConnected())
118
+ return existing;
119
+ // Failed during init — don't retry.
120
+ if (this.failedServers.has(serverName))
121
+ return null;
122
+ const serverConfig = this.serverConfigs.get(serverName);
123
+ if (!serverConfig)
124
+ return null;
125
+ const client = new MCPClient(serverConfig);
126
+ try {
127
+ await client.connect();
128
+ const tools = await client.listTools();
129
+ this.connections.set(serverName, client);
130
+ // Add newly discovered tools (if not already present).
131
+ for (const tool of tools) {
132
+ const alreadyDiscovered = this.discovered.some((d) => d.serverName === serverName && d.toolName === tool.name);
133
+ if (!alreadyDiscovered) {
59
134
  this.discovered.push({
60
135
  serverName,
61
136
  toolName: tool.name,
@@ -64,14 +139,18 @@ export class MCPModule {
64
139
  });
65
140
  }
66
141
  }
67
- catch (e) {
68
- this.initError = `MCP server "${serverName}" init failed: ${e.message}`;
69
- await client.disconnect().catch(() => { });
70
- }
142
+ return client;
143
+ }
144
+ catch (e) {
145
+ this.failedServers.add(serverName);
146
+ logger.warn(`MCP connect failed for "${serverName}": ${e.message}`);
147
+ await client.disconnect().catch(() => { });
148
+ return null;
71
149
  }
72
150
  }
73
151
  getSystemPromptBlock() {
74
- if (this.discovered.length === 0)
152
+ const serverNames = [...this.serverConfigs.keys()];
153
+ if (serverNames.length === 0)
75
154
  return null;
76
155
  const lines = ["---", "MCP servers available:"];
77
156
  const byServer = new Map();
@@ -80,11 +159,17 @@ export class MCPModule {
80
159
  list.push(d);
81
160
  byServer.set(d.serverName, list);
82
161
  }
83
- for (const [server, tools] of byServer) {
84
- lines.push(` [${server}]`);
85
- for (const t of tools) {
86
- const toolRef = sanitizeToolName(server, t.toolName);
87
- lines.push(` - ${toolRef}: ${t.description}`);
162
+ for (const serverName of serverNames) {
163
+ lines.push(` [${serverName}]`);
164
+ const tools = byServer.get(serverName);
165
+ if (tools && tools.length > 0) {
166
+ for (const t of tools) {
167
+ const toolRef = sanitizeToolName(serverName, t.toolName);
168
+ lines.push(` - ${toolRef}: ${t.description}`);
169
+ }
170
+ }
171
+ else {
172
+ lines.push(` (not yet connected — use mcp__${serverName}__connect to discover tools)`);
88
173
  }
89
174
  }
90
175
  lines.push("---");
@@ -96,45 +181,84 @@ export class MCPModule {
96
181
  };
97
182
  }
98
183
  getToolDefinitions() {
99
- return this.discovered.map((d) => ({
100
- name: sanitizeToolName(d.serverName, d.toolName),
101
- description: `${d.description} [MCP server: ${d.serverName}]`,
102
- parameters: convertInputSchema(d.inputSchema),
103
- tags: ["code", "research"],
104
- handler: async (_ctx, args) => {
105
- const client = this.connections.get(d.serverName);
106
- if (!client) {
107
- return {
108
- success: false,
109
- output: `MCP server "${d.serverName}" not connected`,
110
- };
111
- }
112
- try {
113
- if (!client.isConnected()) {
114
- await client.connect();
184
+ // Build definitions from discovered tools AND from registered-but-not-yet-discovered
185
+ // servers (so the LLM sees all configured MCP tools even if discovery failed at startup).
186
+ const defs = [];
187
+ // Tools already discovered (connected at startup).
188
+ for (const d of this.discovered) {
189
+ defs.push({
190
+ name: sanitizeToolName(d.serverName, d.toolName),
191
+ description: `${d.description} [MCP server: ${d.serverName}]`,
192
+ parameters: convertInputSchema(d.inputSchema),
193
+ tags: ["code", "research"],
194
+ handler: async (_ctx, args) => {
195
+ const client = this.connections.get(d.serverName);
196
+ if (!client?.isConnected()) {
197
+ return {
198
+ success: false,
199
+ output: `MCP server "${d.serverName}" not connected`,
200
+ };
201
+ }
202
+ try {
203
+ const result = await client.callTool(d.toolName, args);
204
+ return {
205
+ success: true,
206
+ output: typeof result === "string" ? result : JSON.stringify(result, null, 2),
207
+ };
115
208
  }
116
- const result = await client.callTool(d.toolName, args);
209
+ catch (e) {
210
+ return { success: false, output: `MCP call failed: ${e.message}` };
211
+ }
212
+ },
213
+ });
214
+ }
215
+ // Servers that haven't been discovered yet — register placeholder tools
216
+ // that will connect lazily on first call.
217
+ for (const [serverName] of this.serverConfigs) {
218
+ if (this.failedServers.has(serverName))
219
+ continue;
220
+ const alreadyDiscovered = this.discovered.some((d) => d.serverName === serverName);
221
+ if (alreadyDiscovered)
222
+ continue;
223
+ // Register a single "connect" tool so the LLM can trigger discovery.
224
+ defs.push({
225
+ name: sanitizeToolName(serverName, "connect"),
226
+ description: `Connect to MCP server "${serverName}" and discover its tools`,
227
+ parameters: { type: "object", properties: {} },
228
+ tags: ["code", "research"],
229
+ handler: async (_ctx, _args) => {
230
+ const client = await this.ensureConnected(serverName);
231
+ if (!client) {
232
+ return { success: false, output: `MCP server "${serverName}" connection failed` };
233
+ }
234
+ const tools = this.discovered
235
+ .filter((d) => d.serverName === serverName)
236
+ .map((d) => ` - ${sanitizeToolName(serverName, d.toolName)}: ${d.description}`);
117
237
  return {
118
238
  success: true,
119
- output: typeof result === "string" ? result : JSON.stringify(result, null, 2),
239
+ output: tools.length > 0
240
+ ? `Connected to "${serverName}". Tools:\n${tools.join("\n")}`
241
+ : `Connected to "${serverName}" (no tools discovered)`,
120
242
  };
121
- }
122
- catch (e) {
123
- return { success: false, output: `MCP call failed: ${e.message}` };
124
- }
125
- },
126
- }));
243
+ },
244
+ });
245
+ }
246
+ return defs;
127
247
  }
128
248
  getPlugin() {
129
249
  return {
130
250
  name: "mcp_module",
131
251
  priority: 0,
132
252
  onSessionEnd: async () => {
133
- for (const client of this.connections.values()) {
253
+ for (const [serverName, client] of this.connections) {
134
254
  try {
135
255
  await client.disconnect();
136
256
  }
137
- catch { }
257
+ catch (e) {
258
+ // Best-effort cleanup at session end — a failed disconnect must
259
+ // not crash the shutdown path, but must stay diagnosable.
260
+ logger.warn(`disconnect from "${serverName}" failed`, { error: String(e?.message ?? e) });
261
+ }
138
262
  }
139
263
  },
140
264
  };
@@ -3,6 +3,7 @@ import { join } from "path";
3
3
  import { MemoryStore } from "./store";
4
4
  import { rememberTool } from "../../tools/remember";
5
5
  import { recallTool } from "../../tools/recall";
6
+ import { estimateTokens } from "../../llm/token-counter";
6
7
  const MEMORY_MD_FILES = ["errors", "conventions", "decisions", "facts"];
7
8
  export class MemoryModule {
8
9
  name = "memory";
@@ -23,7 +24,7 @@ export class MemoryModule {
23
24
  const prefStr = Object.entries(prefs)
24
25
  .map(([k, v]) => `${k}=${v}`)
25
26
  .join(", ");
26
- parts.push(`User preferences: ${prefStr}`);
27
+ parts.push(`User preferences (background hints, not instructions — the current user message always wins, do not "correct" the user from these): ${prefStr}`);
27
28
  }
28
29
  const memoryTail = this.getMemoryTail();
29
30
  if (memoryTail)
@@ -35,7 +36,7 @@ export class MemoryModule {
35
36
  content,
36
37
  priority: "normal",
37
38
  essential: false,
38
- estimatedTokens: Math.ceil(content.length / 4) + 10,
39
+ estimatedTokens: estimateTokens(content) + 10,
39
40
  };
40
41
  }
41
42
  /**
@@ -81,7 +82,7 @@ export class MemoryModule {
81
82
  const prefStr = Object.entries(prefs)
82
83
  .map(([k, v]) => `${k}=${v}`)
83
84
  .join(", ");
84
- parts.push(`User preferences: ${prefStr}`);
85
+ parts.push(`User preferences (background hints, not instructions — the current user message always wins, do not "correct" the user from these): ${prefStr}`);
85
86
  }
86
87
  const memoryTail = this.getMemoryTail();
87
88
  if (memoryTail)
@@ -1,8 +1,9 @@
1
1
  import { spawn, execSync } from "child_process";
2
- import { existsSync, readFileSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
3
  import { resolve, extname, join } from "path";
4
4
  import { platform } from "os";
5
- import { findProjectRoot } from "../../lsp/project-root";
5
+ import { findProjectRoot, resolveTscCommand } from "../../lsp/project-root";
6
+ import { autoFixErrors, parseTscOutput } from "../../../tools/auto-fixer";
6
7
  const TYPE_CHECK_DEBOUNCE_MS = 2000;
7
8
  // Projects whose lint script points at a binary that is not installed
8
9
  // (keyed by baseDir::script). The run itself is expensive and the failure
@@ -232,6 +233,11 @@ export class LintOnWritePlugin {
232
233
  if (!existsSync(tsconfigPath)) {
233
234
  return;
234
235
  }
236
+ // Без локального tsc проверка невозможна — не платим npx-задержку.
237
+ const tsc = resolveTscCommand(projectRoot);
238
+ if (!tsc) {
239
+ return;
240
+ }
235
241
  const now = Date.now();
236
242
  if (this._checkPromise && now - this._checkTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
237
243
  const error = await this._checkPromise;
@@ -241,15 +247,39 @@ export class LintOnWritePlugin {
241
247
  return;
242
248
  }
243
249
  this._checkTimestamp = now;
244
- this._checkPromise = this.runTscCheck(projectRoot, signal);
250
+ this._checkPromise = this.runTscCheck(tsc, projectRoot, signal);
245
251
  const error = await this._checkPromise;
246
252
  if (error) {
247
- result.output += `\n\n[Project typecheck failed]: ${error}`;
253
+ // Try to auto-fix fixable errors
254
+ const { stdout, stderr } = await runAsync(`${tsc} --noEmit --skipLibCheck 2>&1`, projectRoot, 30000, signal).catch(() => ({ stdout: "", stderr: error }));
255
+ const output = stderr || stdout;
256
+ const errors = parseTscOutput(output);
257
+ if (errors.length > 0) {
258
+ const fixResult = autoFixErrors(filePath, readFileSync(filePath, "utf-8"), errors);
259
+ if (fixResult.fixed) {
260
+ // Write the fixed content
261
+ writeFileSync(filePath, fixResult.newContent, "utf-8");
262
+ result.output += `\n\n[Auto-fixed]: ${fixResult.applied.join("; ")}`;
263
+ // If there are remaining errors, show them
264
+ if (fixResult.remaining.length > 0) {
265
+ const remainingMsg = fixResult.remaining
266
+ .map((e) => `TS${e.code}: ${e.message}`)
267
+ .join("\n");
268
+ result.output += `\n\n[Remaining errors]: ${remainingMsg}`;
269
+ }
270
+ }
271
+ else {
272
+ result.output += `\n\n[Project typecheck failed]: ${error}`;
273
+ }
274
+ }
275
+ else {
276
+ result.output += `\n\n[Project typecheck failed]: ${error}`;
277
+ }
248
278
  }
249
279
  }
250
- async runTscCheck(baseDir, signal) {
280
+ async runTscCheck(tsc, baseDir, signal) {
251
281
  try {
252
- await runAsync(`npx tsc --noEmit --skipLibCheck`, baseDir, 30000, signal);
282
+ const { stdout, stderr } = await runAsync(`${tsc} --noEmit --skipLibCheck`, baseDir, 30000, signal);
253
283
  return null;
254
284
  }
255
285
  catch (err) {
@@ -1,4 +1,29 @@
1
1
  import { semverGt } from "../updater/checker";
2
+ import { Logger } from "../../logger/app-logger";
3
+ const logger = new Logger("warn", "plugins");
4
+ /**
5
+ * Run a (possibly async) plugin hook with error isolation. Sync throws and
6
+ * async rejections are both logged and swallowed — one broken plugin must
7
+ * never crash the agent or other plugins.
8
+ */
9
+ function safeInvoke(plugin, hookName, fn) {
10
+ try {
11
+ const out = fn();
12
+ if (out instanceof Promise) {
13
+ // The promise rejects AFTER this try/catch has exited — attach a catch
14
+ // now, otherwise isolation is broken by an unhandledRejection.
15
+ out.catch((e) => {
16
+ logger.warn(`plugin "${plugin.name}" ${hookName} rejected`, { error: String(e) });
17
+ });
18
+ return undefined;
19
+ }
20
+ return out;
21
+ }
22
+ catch (e) {
23
+ logger.warn(`plugin "${plugin.name}" ${hookName} threw`, { error: String(e) });
24
+ return undefined;
25
+ }
26
+ }
2
27
  export class PluginManager {
3
28
  plugins = [];
4
29
  hostBridge = null;
@@ -55,14 +80,7 @@ export class PluginManager {
55
80
  return this.plugins.find((p) => p.name === name) ?? null;
56
81
  }
57
82
  runOnBuildPrompt() {
58
- return this.plugins.map((p) => {
59
- try {
60
- return p.onBuildPrompt?.() ?? null;
61
- }
62
- catch {
63
- return null;
64
- }
65
- });
83
+ return this.plugins.map((p) => safeInvoke(p, "onBuildPrompt", () => p.onBuildPrompt?.() ?? null) ?? null);
66
84
  }
67
85
  async runOnBeforeTool(rawCtx, call) {
68
86
  const ctx = this.withHost(rawCtx);
@@ -73,8 +91,8 @@ export class PluginManager {
73
91
  if (result === false)
74
92
  return false;
75
93
  }
76
- catch {
77
- /* error isolation */
94
+ catch (e) {
95
+ logger.warn(`plugin "${plugin.name}" onBeforeTool failed`, { error: String(e) });
78
96
  }
79
97
  }
80
98
  }
@@ -87,8 +105,8 @@ export class PluginManager {
87
105
  try {
88
106
  await plugin.onAfterTool(ctx, call, result);
89
107
  }
90
- catch {
91
- /* error isolation */
108
+ catch (e) {
109
+ logger.warn(`plugin "${plugin.name}" onAfterTool failed`, { error: String(e) });
92
110
  }
93
111
  }
94
112
  }
@@ -100,8 +118,8 @@ export class PluginManager {
100
118
  try {
101
119
  await plugin.onError(ctx, error);
102
120
  }
103
- catch {
104
- /* error isolation */
121
+ catch (e) {
122
+ logger.warn(`plugin "${plugin.name}" onError failed`, { error: String(e) });
105
123
  }
106
124
  }
107
125
  }
@@ -110,12 +128,7 @@ export class PluginManager {
110
128
  const ctx = this.withHost(rawCtx);
111
129
  for (const plugin of this.plugins) {
112
130
  if (plugin.onSessionStart) {
113
- try {
114
- plugin.onSessionStart(ctx);
115
- }
116
- catch {
117
- /* error isolation */
118
- }
131
+ safeInvoke(plugin, "onSessionStart", () => plugin.onSessionStart(ctx));
119
132
  }
120
133
  }
121
134
  }
@@ -123,12 +136,7 @@ export class PluginManager {
123
136
  const ctx = this.withHost(rawCtx);
124
137
  for (const plugin of this.plugins) {
125
138
  if (plugin.onBeforeThink) {
126
- try {
127
- plugin.onBeforeThink(ctx);
128
- }
129
- catch {
130
- /* error isolation */
131
- }
139
+ safeInvoke(plugin, "onBeforeThink", () => plugin.onBeforeThink(ctx));
132
140
  }
133
141
  }
134
142
  }
@@ -136,12 +144,7 @@ export class PluginManager {
136
144
  const ctx = this.withHost(rawCtx);
137
145
  for (const plugin of this.plugins) {
138
146
  if (plugin.onAfterThink) {
139
- try {
140
- plugin.onAfterThink(ctx, response);
141
- }
142
- catch {
143
- /* error isolation */
144
- }
147
+ safeInvoke(plugin, "onAfterThink", () => plugin.onAfterThink(ctx, response));
145
148
  }
146
149
  }
147
150
  }
@@ -149,12 +152,7 @@ export class PluginManager {
149
152
  const ctx = this.withHost(rawCtx);
150
153
  for (const plugin of this.plugins) {
151
154
  if (plugin.onSessionEnd) {
152
- try {
153
- plugin.onSessionEnd(ctx);
154
- }
155
- catch {
156
- /* error isolation */
157
- }
155
+ safeInvoke(plugin, "onSessionEnd", () => plugin.onSessionEnd(ctx));
158
156
  }
159
157
  }
160
158
  }
@@ -162,12 +160,7 @@ export class PluginManager {
162
160
  const ctx = this.withHost(rawCtx);
163
161
  for (const plugin of this.plugins) {
164
162
  if (plugin.onToolCall) {
165
- try {
166
- plugin.onToolCall(ctx);
167
- }
168
- catch {
169
- /* error isolation */
170
- }
163
+ safeInvoke(plugin, "onToolCall", () => plugin.onToolCall(ctx));
171
164
  }
172
165
  }
173
166
  }
@@ -175,12 +168,7 @@ export class PluginManager {
175
168
  const ctx = this.withHost(rawCtx);
176
169
  for (const plugin of this.plugins) {
177
170
  if (plugin.onPhase) {
178
- try {
179
- plugin.onPhase(ctx, phase);
180
- }
181
- catch {
182
- /* error isolation */
183
- }
171
+ safeInvoke(plugin, "onPhase", () => plugin.onPhase(ctx, phase));
184
172
  }
185
173
  }
186
174
  }
@@ -188,12 +176,7 @@ export class PluginManager {
188
176
  const ctx = this.withHost(rawCtx);
189
177
  for (const plugin of this.plugins) {
190
178
  if (plugin.onToolStart) {
191
- try {
192
- plugin.onToolStart(ctx, call);
193
- }
194
- catch {
195
- /* error isolation */
196
- }
179
+ safeInvoke(plugin, "onToolStart", () => plugin.onToolStart(ctx, call));
197
180
  }
198
181
  }
199
182
  }
@@ -201,12 +184,7 @@ export class PluginManager {
201
184
  const ctx = this.withHost(rawCtx);
202
185
  for (const plugin of this.plugins) {
203
186
  if (plugin.onToolEnd) {
204
- try {
205
- plugin.onToolEnd(ctx, call, result, durationMs);
206
- }
207
- catch {
208
- /* error isolation */
209
- }
187
+ safeInvoke(plugin, "onToolEnd", () => plugin.onToolEnd(ctx, call, result, durationMs));
210
188
  }
211
189
  }
212
190
  }
@@ -215,14 +193,9 @@ export class PluginManager {
215
193
  const ctx = this.withHost(rawCtx);
216
194
  for (const plugin of this.plugins) {
217
195
  if (plugin.onText) {
218
- try {
219
- const out = plugin.onText(ctx, chunk);
220
- if (typeof out === "string")
221
- chunk = out;
222
- }
223
- catch {
224
- /* error isolation */
225
- }
196
+ const out = safeInvoke(plugin, "onText", () => plugin.onText(ctx, chunk));
197
+ if (typeof out === "string")
198
+ chunk = out;
226
199
  }
227
200
  }
228
201
  return chunk;
@@ -232,14 +205,9 @@ export class PluginManager {
232
205
  const ctx = this.withHost(rawCtx);
233
206
  for (const plugin of this.plugins) {
234
207
  if (plugin.onMeta) {
235
- try {
236
- const out = plugin.onMeta(ctx, chunk);
237
- if (typeof out === "string")
238
- chunk = out;
239
- }
240
- catch {
241
- /* error isolation */
242
- }
208
+ const out = safeInvoke(plugin, "onMeta", () => plugin.onMeta(ctx, chunk));
209
+ if (typeof out === "string")
210
+ chunk = out;
243
211
  }
244
212
  }
245
213
  return chunk;
@@ -249,12 +217,7 @@ export class PluginManager {
249
217
  const ctx = this.withHost(rawCtx);
250
218
  for (const plugin of this.plugins) {
251
219
  if (plugin.onTurnEnd) {
252
- try {
253
- plugin.onTurnEnd(ctx, summary);
254
- }
255
- catch {
256
- /* error isolation */
257
- }
220
+ safeInvoke(plugin, "onTurnEnd", () => plugin.onTurnEnd(ctx, summary));
258
221
  }
259
222
  }
260
223
  }
@@ -1,4 +1,4 @@
1
- import { calculateCost, formatCost } from "./prices";
1
+ import { calculateCostDetailed, formatCost } from "./prices";
2
2
  /**
3
3
  * Accumulates per-call LLM cost for a session, attributed to the provider
4
4
  * that served the call. Call `record()` after each LLM call with the
@@ -10,6 +10,7 @@ export class CostTracker {
10
10
  config;
11
11
  model;
12
12
  _total = 0;
13
+ _saved = 0;
13
14
  _known = false;
14
15
  byProvider = new Map();
15
16
  constructor(model, config) {
@@ -20,26 +21,35 @@ export class CostTracker {
20
21
  setModel(model) {
21
22
  this.model = model;
22
23
  }
23
- /** Record a single LLM call's token usage, attributed to `provider`. */
24
- record(promptTokens, completionTokens, provider) {
24
+ /**
25
+ * Записывает usage одного LLM-вызова с атрибуцией провайдеру. `cache`
26
+ * разделяет prompt-токены на кешированные/некешированные и накапливает
27
+ * экономию.
28
+ */
29
+ record(promptTokens, completionTokens, provider, cache) {
25
30
  const key = provider || "unknown";
26
- const cost = calculateCost(this.model, promptTokens, completionTokens, this.config);
27
- if (cost === undefined)
31
+ const result = calculateCostDetailed(this.model, promptTokens, completionTokens, this.config, cache);
32
+ if (!result)
28
33
  return;
29
34
  this._known = true;
30
- this._total += cost;
35
+ this._total += result.cost;
36
+ this._saved += result.saved;
31
37
  let entry = this.byProvider.get(key);
32
38
  if (!entry) {
33
39
  entry = { total: 0, known: false };
34
40
  this.byProvider.set(key, entry);
35
41
  }
36
42
  entry.known = true;
37
- entry.total += cost;
43
+ entry.total += result.cost;
38
44
  }
39
45
  /** Running total cost in USD, or undefined if no model price is known. */
40
46
  get total() {
41
47
  return this._known ? this._total : undefined;
42
48
  }
49
+ /** Накопленная экономия на cache read (USD), undefined без известной цены. */
50
+ get saved() {
51
+ return this._known ? this._saved : undefined;
52
+ }
43
53
  /** Total for one provider, or undefined when nothing was priced for it. */
44
54
  totalFor(provider) {
45
55
  const entry = this.byProvider.get(provider);