micro-models-agent 0.63.3 → 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.
- package/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -5,9 +5,12 @@ import { getServerForFile, DEFAULT_LSP_CONFIG } from "./config";
|
|
|
5
5
|
import { findProjectRoot } from "./project-root";
|
|
6
6
|
import { isPathInScope } from "../security/path-validator";
|
|
7
7
|
import { DEFAULT_SECURITY_CONFIG } from "../../config/security";
|
|
8
|
+
import { toForwardSlash } from "../../tools/path-utils";
|
|
8
9
|
import { safeResolvePath } from "../../tools/path-utils";
|
|
9
10
|
import { t } from "../../i18n/index";
|
|
10
11
|
import { severityLabels, collectCheckFiles, formatCheckDiagnostics } from "./check-tool";
|
|
12
|
+
import { errMsg } from "../../utils";
|
|
13
|
+
import { join } from "path";
|
|
11
14
|
/**
|
|
12
15
|
* Consecutive LSP failures before a server is disabled for the session. A dead
|
|
13
16
|
* server (not installed, npx auto-install failed, path missing) otherwise
|
|
@@ -17,6 +20,22 @@ import { severityLabels, collectCheckFiles, formatCheckDiagnostics } from "./che
|
|
|
17
20
|
* for that server — a CSS timeout must not take TypeScript checks down too.
|
|
18
21
|
*/
|
|
19
22
|
const MAX_CONSECUTIVE_LSP_FAILURES = 3;
|
|
23
|
+
/**
|
|
24
|
+
* Module-resolution diagnostics that are meaningless without a type
|
|
25
|
+
* environment. In a bare directory (no tsconfig.json / node_modules) the
|
|
26
|
+
* TypeScript server reports `Cannot find module 'fs'` and
|
|
27
|
+
* `Cannot find name '__dirname'` for perfectly valid Node code — the model
|
|
28
|
+
* then abandons a working approach because of phantom errors (observed
|
|
29
|
+
* ses_mtho2oab: LSP "errors" pushed the model off a planned .ts file to .js
|
|
30
|
+
* and into a plan-drift spiral). Suppressed with an explicit inconclusive
|
|
31
|
+
* note when no type env exists near the project root.
|
|
32
|
+
*/
|
|
33
|
+
const TYPE_ENV_DIAG_PATTERN = /Cannot find module ['"][^'"]+['"] or its corresponding type declarations|Cannot find name '(__dirname|__filename|require|process|Buffer|console)'|Do you need to install type definitions/;
|
|
34
|
+
function hasTypeEnvironment(projectRoot) {
|
|
35
|
+
return (existsSync(join(projectRoot, "tsconfig.json")) ||
|
|
36
|
+
existsSync(join(projectRoot, "jsconfig.json")) ||
|
|
37
|
+
existsSync(join(projectRoot, "node_modules")));
|
|
38
|
+
}
|
|
20
39
|
export class LspModule {
|
|
21
40
|
name = "lsp";
|
|
22
41
|
config;
|
|
@@ -103,7 +122,7 @@ export class LspModule {
|
|
|
103
122
|
// Surface failures instead of silently returning [] — a dead LSP
|
|
104
123
|
// previously produced ZERO diagnostics across whole sessions and the
|
|
105
124
|
// model only found type errors reactively via `bun run build`.
|
|
106
|
-
const msg =
|
|
125
|
+
const msg = errMsg(e);
|
|
107
126
|
const failures = (self.failuresByServer.get(key) ?? 0) + 1;
|
|
108
127
|
self.failuresByServer.set(key, failures);
|
|
109
128
|
try {
|
|
@@ -183,6 +202,7 @@ export class LspModule {
|
|
|
183
202
|
const notes = [];
|
|
184
203
|
const checkedFiles = [];
|
|
185
204
|
let checked = 0;
|
|
205
|
+
let suppressedTypeEnvDiags = 0;
|
|
186
206
|
for (const file of files) {
|
|
187
207
|
const serverConfig = getServerForFile(file, this.config);
|
|
188
208
|
if (!serverConfig)
|
|
@@ -192,10 +212,20 @@ export class LspModule {
|
|
|
192
212
|
continue;
|
|
193
213
|
const projectRoot = findProjectRoot(file, ctx.baseDir, serverConfig.workspaceMarkers ?? []);
|
|
194
214
|
try {
|
|
195
|
-
|
|
215
|
+
let diags = await this.client.checkFile(file, ctx.baseDir, serverConfig, projectRoot);
|
|
196
216
|
this.failuresByServer.set(key, 0);
|
|
197
217
|
checked++;
|
|
198
|
-
checkedFiles.push(relative(ctx.baseDir, file)
|
|
218
|
+
checkedFiles.push(toForwardSlash(relative(ctx.baseDir, file)));
|
|
219
|
+
if (!hasTypeEnvironment(projectRoot)) {
|
|
220
|
+
const kept = diags.filter((d) => {
|
|
221
|
+
if (TYPE_ENV_DIAG_PATTERN.test(d.message)) {
|
|
222
|
+
suppressedTypeEnvDiags++;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
diags = kept;
|
|
228
|
+
}
|
|
199
229
|
for (const d of diags) {
|
|
200
230
|
if (d.severity === 1)
|
|
201
231
|
errors.push({ file, diag: d });
|
|
@@ -204,7 +234,7 @@ export class LspModule {
|
|
|
204
234
|
}
|
|
205
235
|
}
|
|
206
236
|
catch (e) {
|
|
207
|
-
const msg =
|
|
237
|
+
const msg = errMsg(e);
|
|
208
238
|
const failures = (this.failuresByServer.get(key) ?? 0) + 1;
|
|
209
239
|
this.failuresByServer.set(key, failures);
|
|
210
240
|
try {
|
|
@@ -236,6 +266,9 @@ export class LspModule {
|
|
|
236
266
|
if (checked > 0 && errors.length === 0 && warnings.length === 0) {
|
|
237
267
|
lines.push(t("lsp.check_clean", { count: String(checked) }));
|
|
238
268
|
}
|
|
269
|
+
if (suppressedTypeEnvDiags > 0) {
|
|
270
|
+
lines.push(`[lsp_check inconclusive: no tsconfig.json / node_modules near project root — ${suppressedTypeEnvDiags} module-resolution diagnostic(s) suppressed. Initialize the project (package.json + types) for accurate TypeScript checks.]`);
|
|
271
|
+
}
|
|
239
272
|
lines.push(...notes);
|
|
240
273
|
if (lines.length === 0) {
|
|
241
274
|
return {
|
|
@@ -247,7 +280,7 @@ export class LspModule {
|
|
|
247
280
|
}
|
|
248
281
|
}
|
|
249
282
|
function formatDiagnostics(diagnostics, filePath) {
|
|
250
|
-
const filename = filePath
|
|
283
|
+
const filename = toForwardSlash(filePath).split("/").pop() ?? filePath;
|
|
251
284
|
return diagnostics
|
|
252
285
|
.slice(0, 5)
|
|
253
286
|
.map((d) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { LspClient } from "./client";
|
|
2
|
-
import {
|
|
2
|
+
import { getServerKeyForFile } from "./config";
|
|
3
3
|
import { collectCheckFiles } from "./check-tool";
|
|
4
4
|
import { findProjectRoot } from "./project-root";
|
|
5
|
+
import { errMsg } from "../../utils";
|
|
5
6
|
const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
|
|
6
7
|
const DEFAULT_PROBE_DEADLINE_MS = 60_000;
|
|
7
8
|
const DEFAULT_MAX_PARALLEL = 3;
|
|
@@ -21,7 +22,7 @@ export async function probeLspServers(config, baseDir, deps = {}) {
|
|
|
21
22
|
const seen = new Set();
|
|
22
23
|
const servers = [];
|
|
23
24
|
for (const file of files) {
|
|
24
|
-
const language =
|
|
25
|
+
const language = getServerKeyForFile(file, config);
|
|
25
26
|
if (!language)
|
|
26
27
|
continue;
|
|
27
28
|
const server = config.servers[language];
|
|
@@ -56,7 +57,7 @@ export async function probeLspServers(config, baseDir, deps = {}) {
|
|
|
56
57
|
ok.push({ language, ok: true, durationMs: Date.now() - started });
|
|
57
58
|
}
|
|
58
59
|
catch (e) {
|
|
59
|
-
const errorMsg =
|
|
60
|
+
const errorMsg = errMsg(e);
|
|
60
61
|
deps.logger?.warn(`LSP probe failed for ${language}: ${errorMsg}`, {
|
|
61
62
|
command: server.command,
|
|
62
63
|
timeout: serverTimeout,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
-
import { dirname, join } from "path";
|
|
2
|
+
import { dirname, join, relative, isAbsolute } from "path";
|
|
3
|
+
import { platform } from "os";
|
|
3
4
|
/**
|
|
4
5
|
* Resolve the LSP workspace root for an edited file.
|
|
5
6
|
*
|
|
@@ -13,11 +14,50 @@ import { dirname, join } from "path";
|
|
|
13
14
|
* LSP server config (`LspServerConfig.workspaceMarkers`) — this function takes
|
|
14
15
|
* the markers as a parameter and performs no language detection itself.
|
|
15
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Находит локальный компилятор TypeScript, поднимаясь от `startDir` вверх по
|
|
19
|
+
* `node_modules`. Возвращает готовую команду или null, если tsc не установлен.
|
|
20
|
+
*
|
|
21
|
+
* Почему не `npx`: обёртка npx тратит 5–13 секунд даже на локальный пакет
|
|
22
|
+
* (Windows), а без локального typescript пытается лезть в сеть. Прямой вызов
|
|
23
|
+
* локального tsc — доли секунды и честная деградация (null → пропускаем).
|
|
24
|
+
*
|
|
25
|
+
* Кросс-платформенность:
|
|
26
|
+
* - предпочитаем JS-энтрипоинт `typescript/bin/tsc` и запускаем его ТЕКУЩИМ
|
|
27
|
+
* рантаймом (`process.execPath` — bun или node). Так не зависим от `node` в
|
|
28
|
+
* PATH (актуально для систем только с Bun) и одинаково работает на
|
|
29
|
+
* Windows/macOS/Linux;
|
|
30
|
+
* - если JS-файла нет, но есть `.bin`-стаб, берём его: на Windows `tsc.cmd`,
|
|
31
|
+
* на macOS/Linux `tsc` (обёртка сама найдёт интерпретатор).
|
|
32
|
+
*/
|
|
33
|
+
export function resolveTscCommand(startDir, maxLevels = 8) {
|
|
34
|
+
const runtime = process.execPath;
|
|
35
|
+
let dir = startDir;
|
|
36
|
+
for (let i = 0; i < maxLevels; i++) {
|
|
37
|
+
const js = join(dir, "node_modules", "typescript", "bin", "tsc");
|
|
38
|
+
if (existsSync(js))
|
|
39
|
+
return `"${runtime}" "${js}"`;
|
|
40
|
+
const stub = join(dir, "node_modules", ".bin", platform() === "win32" ? "tsc.cmd" : "tsc");
|
|
41
|
+
if (existsSync(stub))
|
|
42
|
+
return `"${stub}"`;
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir)
|
|
45
|
+
break;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
16
50
|
export function findProjectRoot(filePath, baseDir, markers) {
|
|
17
51
|
if (!markers || markers.length === 0)
|
|
18
52
|
return baseDir;
|
|
19
53
|
let dir = dirname(filePath);
|
|
20
54
|
const root = baseDir.replace(/[\\/]+$/, "");
|
|
55
|
+
// Clamp the walk to baseDir: callers that pass baseDir itself as filePath
|
|
56
|
+
// (probes) start at dirname(baseDir) — ABOVE the sandbox — and would pick up
|
|
57
|
+
// a marker directory from the parent (e.g. a package.json next to it).
|
|
58
|
+
const rel = relative(root, dir);
|
|
59
|
+
if ((rel && rel.startsWith("..")) || isAbsolute(rel))
|
|
60
|
+
dir = root;
|
|
21
61
|
while (true) {
|
|
22
62
|
if (markers.some((m) => existsSync(join(dir, m)))) {
|
|
23
63
|
return dir;
|
|
@@ -5,7 +5,9 @@ import { t } from "../../i18n/index";
|
|
|
5
5
|
import { LspClient } from "./client";
|
|
6
6
|
import { collectCheckFiles } from "./check-tool";
|
|
7
7
|
import { getServerForFile } from "./config";
|
|
8
|
-
import { findProjectRoot } from "./project-root";
|
|
8
|
+
import { findProjectRoot, resolveTscCommand } from "./project-root";
|
|
9
|
+
import { estimateTokens } from "../../llm/token-counter";
|
|
10
|
+
import { errMsg } from "../../utils";
|
|
9
11
|
/** Startup check never examines more than this many files via LSP. */
|
|
10
12
|
const STARTUP_CHECK_FILE_CAP = 5;
|
|
11
13
|
/** Overall cap for the whole startup check — never block session start longer. */
|
|
@@ -42,7 +44,7 @@ export async function runStartupHealthCheck(config, baseDir, deps = {}) {
|
|
|
42
44
|
content,
|
|
43
45
|
priority: "high",
|
|
44
46
|
essential: false,
|
|
45
|
-
estimatedTokens:
|
|
47
|
+
estimatedTokens: estimateTokens(content),
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
catch {
|
|
@@ -80,7 +82,7 @@ async function runCheck(config, baseDir, deps) {
|
|
|
80
82
|
}
|
|
81
83
|
catch (e) {
|
|
82
84
|
// Per-file LSP failure → log and skip (fail-open).
|
|
83
|
-
deps.logger?.debug(`Startup LSP check failed for ${file}: ${
|
|
85
|
+
deps.logger?.debug(`Startup LSP check failed for ${file}: ${errMsg(e)}`);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
if (checked === 0 || lines.length === 0)
|
|
@@ -88,8 +90,14 @@ async function runCheck(config, baseDir, deps) {
|
|
|
88
90
|
return { lines: lines.slice(0, STARTUP_CHECK_ERROR_CAP) };
|
|
89
91
|
}
|
|
90
92
|
async function runTscDefault(projectRoot, timeoutMs) {
|
|
93
|
+
// Локальный tsc, а не `npx`: npx-обёртка (cmd → node npm-prefix) медленная и
|
|
94
|
+
// на Windows переживает выход процесса, оставляя открытым stdout-пайп —
|
|
95
|
+
// команда «виснет». Нет локального tsc — молча пропускаем проверку.
|
|
96
|
+
const tsc = resolveTscCommand(projectRoot);
|
|
97
|
+
if (!tsc)
|
|
98
|
+
return [];
|
|
91
99
|
return new Promise((resolve) => {
|
|
92
|
-
const child = spawn(
|
|
100
|
+
const child = spawn(`${tsc} --noEmit --skipLibCheck`, {
|
|
93
101
|
cwd: projectRoot,
|
|
94
102
|
shell: true,
|
|
95
103
|
windowsHide: true,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import { defaultOutputBus } from "../../output";
|
|
3
|
+
/** Default timeout for MCP HTTP requests (connect + tool calls). */
|
|
4
|
+
const HTTP_TIMEOUT_MS = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* Accept header required by the MCP Streamable HTTP transport — the server
|
|
7
|
+
* responds with 406 if the client doesn't accept both media types.
|
|
8
|
+
*/
|
|
9
|
+
const MCP_ACCEPT_HEADER = "application/json, text/event-stream";
|
|
2
10
|
export class MCPClient {
|
|
3
11
|
serverName;
|
|
4
12
|
config;
|
|
@@ -31,22 +39,30 @@ export class MCPClient {
|
|
|
31
39
|
if (!url)
|
|
32
40
|
throw new Error("SSE transport requires a url");
|
|
33
41
|
this.abortController = new AbortController();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const timeout = setTimeout(() => this.abortController?.abort(), this.config.timeout || HTTP_TIMEOUT_MS);
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method: "GET",
|
|
46
|
+
headers: {
|
|
47
|
+
Accept: "text/event-stream",
|
|
48
|
+
...this.config.headers,
|
|
49
|
+
},
|
|
50
|
+
signal: this.abortController.signal,
|
|
51
|
+
});
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
this._connected = true;
|
|
57
|
+
const reader = response.body.getReader();
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let buffer = "";
|
|
60
|
+
this.readSSE(reader, decoder, buffer);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
throw err;
|
|
44
65
|
}
|
|
45
|
-
this._connected = true;
|
|
46
|
-
const reader = response.body.getReader();
|
|
47
|
-
const decoder = new TextDecoder();
|
|
48
|
-
let buffer = "";
|
|
49
|
-
this.readSSE(reader, decoder, buffer);
|
|
50
66
|
}
|
|
51
67
|
async readSSE(reader, decoder, buffer) {
|
|
52
68
|
try {
|
|
@@ -87,6 +103,7 @@ export class MCPClient {
|
|
|
87
103
|
try {
|
|
88
104
|
const msg = JSON.parse(data);
|
|
89
105
|
if (this.pendingResolve && msg.id) {
|
|
106
|
+
this.clearSseTimer();
|
|
90
107
|
if (msg.error) {
|
|
91
108
|
this.pendingReject?.(new Error(msg.error.message));
|
|
92
109
|
}
|
|
@@ -97,10 +114,15 @@ export class MCPClient {
|
|
|
97
114
|
this.pendingReject = null;
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
|
-
catch {
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Heartbeats/comments are not JSON — expected noise, but still logged
|
|
119
|
+
// so a genuinely broken stream is visible in diagnostics.
|
|
120
|
+
defaultOutputBus.log("error", "mcp", `[MCPClient] Unparseable SSE message: ${String(data).slice(0, 120)} ${e instanceof Error ? e.message : String(e)}`, { persist: true });
|
|
121
|
+
}
|
|
101
122
|
}
|
|
102
123
|
handleSSEError(err) {
|
|
103
124
|
if (this.pendingReject) {
|
|
125
|
+
this.clearSseTimer();
|
|
104
126
|
this.pendingReject(err);
|
|
105
127
|
this.pendingResolve = null;
|
|
106
128
|
this.pendingReject = null;
|
|
@@ -130,11 +152,102 @@ export class MCPClient {
|
|
|
130
152
|
child.on("spawn", () => {
|
|
131
153
|
this._connected = true;
|
|
132
154
|
this.process = child;
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
// MCP handshake: real servers require `initialize` before any
|
|
156
|
+
// tools/list or tools/call request.
|
|
157
|
+
this.stdioRequest("initialize", {
|
|
158
|
+
protocolVersion: "2024-11-05",
|
|
159
|
+
capabilities: {},
|
|
160
|
+
clientInfo: { name: "mma", version: "1.0.0" },
|
|
161
|
+
}, 10000)
|
|
162
|
+
.then(() => {
|
|
163
|
+
child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
|
164
|
+
})
|
|
165
|
+
.then(() => {
|
|
166
|
+
_resolved = true;
|
|
167
|
+
resolve();
|
|
168
|
+
})
|
|
169
|
+
.catch((err) => {
|
|
170
|
+
this._connected = false;
|
|
171
|
+
if (!_resolved) {
|
|
172
|
+
_resolved = true;
|
|
173
|
+
reject(err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
135
176
|
});
|
|
136
177
|
});
|
|
137
178
|
}
|
|
179
|
+
/** Unparsed trailing bytes from stdio responses (partial line). */
|
|
180
|
+
stdioBuffer = "";
|
|
181
|
+
/**
|
|
182
|
+
* JSON-RPC request over stdio with timeout, exit handling and
|
|
183
|
+
* buffer-aware line parsing (notifications and stale responses between
|
|
184
|
+
* our request and its response no longer break matching).
|
|
185
|
+
*/
|
|
186
|
+
stdioRequest(method, params, timeoutMs) {
|
|
187
|
+
const proc = this.process;
|
|
188
|
+
if (!proc || !proc.stdout || !proc.stdin) {
|
|
189
|
+
return Promise.reject(new Error("Not connected to MCP server"));
|
|
190
|
+
}
|
|
191
|
+
const stdout = proc.stdout;
|
|
192
|
+
const timeout = timeoutMs ?? this.config.timeout ?? 30000;
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const request = {
|
|
195
|
+
jsonrpc: "2.0",
|
|
196
|
+
id: Date.now(),
|
|
197
|
+
method,
|
|
198
|
+
...(params !== undefined ? { params } : {}),
|
|
199
|
+
};
|
|
200
|
+
let timer;
|
|
201
|
+
let settled = false;
|
|
202
|
+
const finish = (fn) => {
|
|
203
|
+
if (settled)
|
|
204
|
+
return;
|
|
205
|
+
settled = true;
|
|
206
|
+
if (timer !== undefined)
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
stdout.off("data", onData);
|
|
209
|
+
proc.off("exit", onExit);
|
|
210
|
+
fn();
|
|
211
|
+
};
|
|
212
|
+
const onData = (data) => {
|
|
213
|
+
this.stdioBuffer += data.toString();
|
|
214
|
+
let idx;
|
|
215
|
+
while ((idx = this.stdioBuffer.indexOf("\n")) !== -1 && !settled) {
|
|
216
|
+
const line = this.stdioBuffer.slice(0, idx).trim();
|
|
217
|
+
this.stdioBuffer = this.stdioBuffer.slice(idx + 1);
|
|
218
|
+
if (!line)
|
|
219
|
+
continue;
|
|
220
|
+
let msg;
|
|
221
|
+
try {
|
|
222
|
+
msg = JSON.parse(line);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (msg.id !== request.id)
|
|
228
|
+
continue; // notification or foreign response
|
|
229
|
+
finish(() => {
|
|
230
|
+
if (msg.error) {
|
|
231
|
+
reject(new Error(msg.error.message));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
resolve(msg.result);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const onExit = () => {
|
|
241
|
+
finish(() => reject(new Error(`MCP server exited before responding to ${method}`)));
|
|
242
|
+
};
|
|
243
|
+
stdout.on("data", onData);
|
|
244
|
+
proc.once("exit", onExit);
|
|
245
|
+
timer = setTimeout(() => {
|
|
246
|
+
finish(() => reject(new Error(`MCP ${method} timeout`)));
|
|
247
|
+
}, timeout);
|
|
248
|
+
proc.stdin.write(JSON.stringify(request) + "\n");
|
|
249
|
+
});
|
|
250
|
+
}
|
|
138
251
|
async listTools() {
|
|
139
252
|
if (!this._connected) {
|
|
140
253
|
await this.connect();
|
|
@@ -156,10 +269,12 @@ export class MCPClient {
|
|
|
156
269
|
const response = await fetch(this.config.url, {
|
|
157
270
|
method: "POST",
|
|
158
271
|
headers: {
|
|
272
|
+
Accept: MCP_ACCEPT_HEADER,
|
|
159
273
|
"Content-Type": "application/json",
|
|
160
274
|
...this.config.headers,
|
|
161
275
|
},
|
|
162
276
|
body: JSON.stringify(request),
|
|
277
|
+
signal: AbortSignal.timeout(this.config.timeout || HTTP_TIMEOUT_MS),
|
|
163
278
|
});
|
|
164
279
|
if (!response.ok) {
|
|
165
280
|
const text = await response.text().catch(() => "");
|
|
@@ -180,6 +295,7 @@ export class MCPClient {
|
|
|
180
295
|
const response = await fetch(this.config.url, {
|
|
181
296
|
method: "POST",
|
|
182
297
|
headers: {
|
|
298
|
+
Accept: MCP_ACCEPT_HEADER,
|
|
183
299
|
"Content-Type": "application/json",
|
|
184
300
|
...this.config.headers,
|
|
185
301
|
},
|
|
@@ -200,46 +316,8 @@ export class MCPClient {
|
|
|
200
316
|
if (!this.process) {
|
|
201
317
|
throw new Error("Not connected to MCP server");
|
|
202
318
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
jsonrpc: "2.0",
|
|
206
|
-
id: Date.now(),
|
|
207
|
-
method: "tools/list",
|
|
208
|
-
};
|
|
209
|
-
const requestStr = JSON.stringify(request) + "\n";
|
|
210
|
-
this.process.stdin.write(requestStr);
|
|
211
|
-
let responseData = "";
|
|
212
|
-
const onData = (data) => {
|
|
213
|
-
responseData += data.toString();
|
|
214
|
-
if (responseData.includes("\n")) {
|
|
215
|
-
this.process.stdout.off("data", onData);
|
|
216
|
-
try {
|
|
217
|
-
const lines = responseData.split("\n").filter((l) => l.trim());
|
|
218
|
-
for (const line of lines) {
|
|
219
|
-
const response = JSON.parse(line);
|
|
220
|
-
if (response.id === request.id) {
|
|
221
|
-
if (response.error) {
|
|
222
|
-
reject(new Error(response.error.message));
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
resolve(response.result?.tools || []);
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
reject(new Error("No matching response found"));
|
|
231
|
-
}
|
|
232
|
-
catch (e) {
|
|
233
|
-
reject(new Error(`Parse error: ${e.message}`));
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
this.process.stdout.on("data", onData);
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
this.process.stdout.off("data", onData);
|
|
240
|
-
reject(new Error("MCP listTools timeout"));
|
|
241
|
-
}, 30000);
|
|
242
|
-
});
|
|
319
|
+
const result = await this.stdioRequest("tools/list");
|
|
320
|
+
return result?.tools || [];
|
|
243
321
|
}
|
|
244
322
|
async disconnect() {
|
|
245
323
|
if (this.config.transport === "http") {
|
|
@@ -281,10 +359,12 @@ export class MCPClient {
|
|
|
281
359
|
const response = await fetch(this.config.url, {
|
|
282
360
|
method: "POST",
|
|
283
361
|
headers: {
|
|
362
|
+
Accept: MCP_ACCEPT_HEADER,
|
|
284
363
|
"Content-Type": "application/json",
|
|
285
364
|
...this.config.headers,
|
|
286
365
|
},
|
|
287
366
|
body: JSON.stringify(request),
|
|
367
|
+
signal: AbortSignal.timeout(this.config.timeout || HTTP_TIMEOUT_MS),
|
|
288
368
|
});
|
|
289
369
|
if (!response.ok) {
|
|
290
370
|
const text = await response.text().catch(() => "");
|
|
@@ -296,6 +376,8 @@ export class MCPClient {
|
|
|
296
376
|
}
|
|
297
377
|
return result.result || result;
|
|
298
378
|
}
|
|
379
|
+
/** Timer of the in-flight SSE request — cleared when the reply arrives via the stream. */
|
|
380
|
+
sseTimer;
|
|
299
381
|
async callToolSSE(toolName, args) {
|
|
300
382
|
const request = {
|
|
301
383
|
jsonrpc: "2.0",
|
|
@@ -310,7 +392,7 @@ export class MCPClient {
|
|
|
310
392
|
this.pendingResolve = resolve;
|
|
311
393
|
this.pendingReject = reject;
|
|
312
394
|
const timeout = this.config.timeout || 30000;
|
|
313
|
-
|
|
395
|
+
this.sseTimer = setTimeout(() => {
|
|
314
396
|
this.pendingResolve = null;
|
|
315
397
|
this.pendingReject = null;
|
|
316
398
|
reject(new Error("MCP call timeout"));
|
|
@@ -318,6 +400,7 @@ export class MCPClient {
|
|
|
318
400
|
fetch(this.config.url, {
|
|
319
401
|
method: "POST",
|
|
320
402
|
headers: {
|
|
403
|
+
Accept: MCP_ACCEPT_HEADER,
|
|
321
404
|
"Content-Type": "application/json",
|
|
322
405
|
...this.config.headers,
|
|
323
406
|
},
|
|
@@ -325,7 +408,7 @@ export class MCPClient {
|
|
|
325
408
|
signal: this.abortController?.signal,
|
|
326
409
|
})
|
|
327
410
|
.then(async (response) => {
|
|
328
|
-
|
|
411
|
+
this.clearSseTimer();
|
|
329
412
|
if (!response.ok) {
|
|
330
413
|
const text = await response.text().catch(() => "");
|
|
331
414
|
reject(new Error(`MCP POST failed: ${response.status} ${text}`));
|
|
@@ -340,60 +423,26 @@ export class MCPClient {
|
|
|
340
423
|
}
|
|
341
424
|
})
|
|
342
425
|
.catch((err) => {
|
|
343
|
-
|
|
426
|
+
this.clearSseTimer();
|
|
344
427
|
if (err.name !== "AbortError") {
|
|
345
428
|
reject(err);
|
|
346
429
|
}
|
|
347
430
|
});
|
|
348
431
|
});
|
|
349
432
|
}
|
|
433
|
+
clearSseTimer() {
|
|
434
|
+
if (this.sseTimer !== undefined) {
|
|
435
|
+
clearTimeout(this.sseTimer);
|
|
436
|
+
this.sseTimer = undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
350
439
|
callToolStdio(toolName, args) {
|
|
351
440
|
if (!this.process) {
|
|
352
441
|
throw new Error("Not connected to MCP server");
|
|
353
442
|
}
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
id: Date.now(),
|
|
358
|
-
method: "tools/call",
|
|
359
|
-
params: {
|
|
360
|
-
name: toolName,
|
|
361
|
-
arguments: args,
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
const requestStr = JSON.stringify(request) + "\n";
|
|
365
|
-
this.process.stdin.write(requestStr);
|
|
366
|
-
let responseData = "";
|
|
367
|
-
const onData = (data) => {
|
|
368
|
-
responseData += data.toString();
|
|
369
|
-
if (responseData.includes("\n")) {
|
|
370
|
-
this.process.stdout.off("data", onData);
|
|
371
|
-
try {
|
|
372
|
-
const lines = responseData.split("\n").filter((l) => l.trim());
|
|
373
|
-
for (const line of lines) {
|
|
374
|
-
const response = JSON.parse(line);
|
|
375
|
-
if (response.id === request.id) {
|
|
376
|
-
if (response.error) {
|
|
377
|
-
reject(new Error(response.error.message));
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
resolve(response.result);
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
reject(new Error("No matching response found"));
|
|
386
|
-
}
|
|
387
|
-
catch (e) {
|
|
388
|
-
reject(new Error(`Parse error: ${e.message}`));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
this.process.stdout.on("data", onData);
|
|
393
|
-
setTimeout(() => {
|
|
394
|
-
this.process.stdout.off("data", onData);
|
|
395
|
-
reject(new Error("MCP call timeout"));
|
|
396
|
-
}, 30000);
|
|
443
|
+
return this.stdioRequest("tools/call", {
|
|
444
|
+
name: toolName,
|
|
445
|
+
arguments: args,
|
|
397
446
|
});
|
|
398
447
|
}
|
|
399
448
|
}
|