micro-models-agent 0.51.2 → 0.52.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/README.md +358 -358
- package/dist/cli/commands.js +447 -0
- package/dist/cli/completer.js +167 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.js +153 -0
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +761 -0
- package/dist/cli/repl.js +702 -0
- package/dist/cli/run-result.js +33 -0
- package/dist/cli/security-commands.js +164 -0
- package/dist/cli/setup.js +237 -0
- package/dist/config/config.js +276 -0
- package/dist/config/defaults.js +141 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/experts.js +15 -0
- package/dist/config/index.js +4 -0
- package/dist/config/security.js +213 -0
- package/dist/config/types.js +1 -0
- package/dist/core/agent-moe.js +102 -0
- package/dist/core/agent.js +1018 -0
- package/dist/core/bootstrap.js +481 -0
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/index.js +2 -0
- package/dist/core/prompt-builder.js +76 -0
- package/dist/core/session-logger.js +251 -0
- package/dist/core/types.js +1 -0
- package/dist/core/version.js +26 -0
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +679 -0
- package/dist/i18n/index.js +46 -0
- package/dist/i18n/ru.json +679 -0
- package/dist/index.js +22 -0
- package/dist/llm/image-utils.js +143 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/model-loader.js +78 -0
- package/dist/llm/openai-compat.js +497 -0
- package/dist/llm/orchestrator.js +200 -0
- package/dist/llm/provider.js +10 -0
- package/dist/llm/response.js +39 -0
- package/dist/llm/token-counter.js +39 -0
- package/dist/llm/types.js +1 -0
- package/dist/logger/app-logger.js +189 -0
- package/dist/logger/file-log.js +151 -0
- package/dist/logger/index.js +1 -0
- package/dist/main.js +509 -100
- package/dist/migration/backup.js +45 -0
- package/dist/migration/detect.js +50 -0
- package/dist/migration/index.js +2 -0
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +76 -0
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +24 -0
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -0
- package/dist/modules/browser/module.js +29 -0
- package/dist/modules/browser/session.js +342 -0
- package/dist/modules/browser/snapshot.js +148 -0
- package/dist/modules/browser/types.js +12 -0
- package/dist/modules/certification/cli.js +213 -0
- package/dist/modules/certification/fact-checker.js +82 -0
- package/dist/modules/certification/loader.js +106 -0
- package/dist/modules/certification/manifest.js +58 -0
- package/dist/modules/certification/runner.js +245 -0
- package/dist/modules/certification/scenarios.js +407 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +168 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +440 -0
- package/dist/modules/execution/audit-runners.js +206 -0
- package/dist/modules/execution/auditor.js +218 -0
- package/dist/modules/execution/execution-plugin.js +431 -0
- package/dist/modules/execution/index.js +8 -0
- package/dist/modules/execution/module.js +625 -0
- package/dist/modules/execution/moe-executor.js +304 -0
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +196 -0
- package/dist/modules/execution/plan-tool.js +677 -0
- package/dist/modules/execution/plan-validator.js +153 -0
- package/dist/modules/execution/planner.js +94 -0
- package/dist/modules/execution/stuck-detector.js +746 -0
- package/dist/modules/execution/tracker.js +69 -0
- package/dist/modules/execution/types.js +1 -0
- package/dist/modules/execution/verifier.js +235 -0
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +66 -0
- package/dist/modules/hallucination/consistency.js +26 -0
- package/dist/modules/hallucination/detector.js +47 -0
- package/dist/modules/hallucination/factual.js +169 -0
- package/dist/modules/hallucination/index.js +5 -0
- package/dist/modules/hallucination/js-identifiers.js +262 -0
- package/dist/modules/hallucination/llm-judge.js +101 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/indexer/cache.js +40 -0
- package/dist/modules/indexer/index.js +3 -0
- package/dist/modules/indexer/module.js +246 -0
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +101 -0
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +389 -0
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +135 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +260 -0
- package/dist/modules/lsp/probe.js +86 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +144 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +399 -0
- package/dist/modules/mcp/index.js +3 -0
- package/dist/modules/mcp/module.js +142 -0
- package/dist/modules/mcp/registry.js +15 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +96 -0
- package/dist/modules/memory/search.js +42 -0
- package/dist/modules/memory/store.js +69 -0
- package/dist/modules/pipelines/engine.js +60 -0
- package/dist/modules/pipelines/index.js +3 -0
- package/dist/modules/pipelines/parser.js +56 -0
- package/dist/modules/pipelines/template.js +14 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +334 -0
- package/dist/modules/plugins/builtin/notify.js +9 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +70 -0
- package/dist/modules/plugins/manager.js +261 -0
- package/dist/modules/plugins/types.js +1 -0
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -0
- package/dist/modules/processes/registry.js +327 -0
- package/dist/modules/processes/runner.js +23 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +48 -0
- package/dist/modules/security/audit-log.js +136 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +219 -0
- package/dist/modules/security/content-scanner.js +53 -0
- package/dist/modules/security/data-sanitizer.js +89 -0
- package/dist/modules/security/encryption.js +242 -0
- package/dist/modules/security/index.js +14 -0
- package/dist/modules/security/network-validator.js +88 -0
- package/dist/modules/security/path-validator.js +203 -0
- package/dist/modules/security/rate-limiter.js +119 -0
- package/dist/modules/security/security-policies.js +531 -0
- package/dist/modules/security/session-encryption.js +210 -0
- package/dist/modules/security/session-isolation.js +95 -0
- package/dist/modules/session/index.js +3 -0
- package/dist/modules/session/manager.js +172 -0
- package/dist/modules/session/module.js +24 -0
- package/dist/modules/session/store.js +222 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/skills/index.js +2 -0
- package/dist/modules/skills/loader.js +72 -0
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +129 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/updater/checker.js +96 -0
- package/dist/modules/updater/index.js +2 -0
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +16 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +68 -0
- package/dist/skills/builtin/git.md +36 -36
- package/dist/skills/builtin/typescript.md +35 -35
- package/dist/tools/approve.js +33 -0
- package/dist/tools/attach-image.js +101 -0
- package/dist/tools/bash.js +519 -0
- package/dist/tools/browser.js +115 -0
- package/dist/tools/chunk-query.js +100 -0
- package/dist/tools/create-dir.js +56 -0
- package/dist/tools/delete-file.js +63 -0
- package/dist/tools/download-file.js +117 -0
- package/dist/tools/edit-file.js +80 -0
- package/dist/tools/enable-tools.js +59 -0
- package/dist/tools/executor.js +154 -0
- package/dist/tools/file-info.js +47 -0
- package/dist/tools/filter-tools.js +17 -0
- package/dist/tools/glob-tool.js +27 -0
- package/dist/tools/grep-tool.js +125 -0
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +78 -0
- package/dist/tools/list-dir.js +49 -0
- package/dist/tools/load-skill.js +43 -0
- package/dist/tools/mcp-call.js +69 -0
- package/dist/tools/move-file.js +86 -0
- package/dist/tools/path-utils.js +101 -0
- package/dist/tools/pipeline-run.js +145 -0
- package/dist/tools/preview.js +2 -0
- package/dist/tools/process-kill.js +40 -0
- package/dist/tools/process-list.js +37 -0
- package/dist/tools/process-log.js +54 -0
- package/dist/tools/question.js +141 -0
- package/dist/tools/read-file.js +179 -0
- package/dist/tools/recall.js +118 -0
- package/dist/tools/registry.js +47 -0
- package/dist/tools/remember.js +68 -0
- package/dist/tools/scope-check.js +32 -0
- package/dist/tools/search-history.js +85 -0
- package/dist/tools/subagent.js +196 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/user-input.js +123 -0
- package/dist/tools/web-browse.js +87 -0
- package/dist/tools/web-fetch.js +119 -0
- package/dist/tools/web-search.js +105 -0
- package/dist/tools/write-file.js +82 -0
- package/dist/ui/box.js +77 -0
- package/dist/ui/colors.js +4 -0
- package/dist/ui/diff.js +178 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/line-editor.js +822 -0
- package/dist/ui/line-math.js +73 -0
- package/dist/ui/md-formatter.js +212 -0
- package/dist/ui/output.js +13 -0
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +259 -0
- package/dist/ui/spinner.js +70 -0
- package/dist/ui/table.js +144 -0
- package/package.json +6 -4
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { spawn, execSync } from "child_process";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
import { resolveSpawnCommand, buildWinCommandLine } from "./command";
|
|
5
|
+
import { killTree } from "../processes/registry";
|
|
6
|
+
/**
|
|
7
|
+
* Logger adapter that writes LSP diagnostics to app.jsonl only — never to
|
|
8
|
+
* console/terminal. LSP warnings are noisy (timeout on first run, npx
|
|
9
|
+
* install) and should be diagnosable from session files without polluting
|
|
10
|
+
* the user's chat.
|
|
11
|
+
*/
|
|
12
|
+
export class FileOnlyLogger {
|
|
13
|
+
logger;
|
|
14
|
+
constructor(logger) {
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
debug(msg, meta) {
|
|
18
|
+
this.logger.logSilent("debug", msg, meta);
|
|
19
|
+
}
|
|
20
|
+
info(msg, meta) {
|
|
21
|
+
this.logger.logSilent("info", msg, meta);
|
|
22
|
+
}
|
|
23
|
+
warn(msg, meta) {
|
|
24
|
+
this.logger.logSilent("warn", msg, meta);
|
|
25
|
+
}
|
|
26
|
+
error(msg, meta) {
|
|
27
|
+
this.logger.logSilent("error", msg, meta);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const DEFAULT_TIMEOUT = 15000;
|
|
31
|
+
export class LspClient {
|
|
32
|
+
logger = null;
|
|
33
|
+
setLogger(logger) {
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
}
|
|
36
|
+
process = null;
|
|
37
|
+
requestId = 0;
|
|
38
|
+
pending = new Map();
|
|
39
|
+
buffer = "";
|
|
40
|
+
contentLength = -1;
|
|
41
|
+
diagnostics = [];
|
|
42
|
+
diagnosticsResolve = null;
|
|
43
|
+
diagnosticsTimer = null;
|
|
44
|
+
initialized = false;
|
|
45
|
+
async checkFile(filePath, baseDir, config, projectRoot = baseDir) {
|
|
46
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
47
|
+
try {
|
|
48
|
+
await this.initialize(config, projectRoot, timeout);
|
|
49
|
+
this.sendNotification("initialized", {});
|
|
50
|
+
const uri = this.pathToUri(resolve(filePath));
|
|
51
|
+
const fs = await import("fs");
|
|
52
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
53
|
+
const diagPromise = new Promise((resolve) => {
|
|
54
|
+
this.diagnosticsResolve = resolve;
|
|
55
|
+
this.diagnostics = [];
|
|
56
|
+
this.diagnosticsTimer = setTimeout(() => {
|
|
57
|
+
this.diagnosticsResolve?.([]);
|
|
58
|
+
this.diagnosticsResolve = null;
|
|
59
|
+
}, timeout);
|
|
60
|
+
});
|
|
61
|
+
this.sendNotification("textDocument/didOpen", {
|
|
62
|
+
textDocument: {
|
|
63
|
+
uri,
|
|
64
|
+
languageId: this.languageFromPath(filePath),
|
|
65
|
+
version: 1,
|
|
66
|
+
text: content,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return await diagPromise;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
// Re-throw so the LspModule can surface failures instead of hiding them.
|
|
73
|
+
throw e instanceof Error ? e : new Error(String(e));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await this.shutdown();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Lightweight "does the server actually start" probe: spawn + initialize +
|
|
81
|
+
* shutdown. Used by the REPL banner to surface a dead server (npx
|
|
82
|
+
* auto-install failure, wrong package name, missing binary) up front instead
|
|
83
|
+
* of the first write_file timing out.
|
|
84
|
+
*/
|
|
85
|
+
async probe(config, projectRoot, timeout) {
|
|
86
|
+
try {
|
|
87
|
+
await this.initialize(config, projectRoot, timeout);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await this.shutdown();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async initialize(config, projectRoot, timeout) {
|
|
94
|
+
await this.startServer(config, projectRoot);
|
|
95
|
+
const rootUri = this.pathToUri(projectRoot);
|
|
96
|
+
const initParams = {
|
|
97
|
+
processId: process.pid,
|
|
98
|
+
rootUri,
|
|
99
|
+
workspaceFolders: [{ uri: rootUri, name: "workspace" }],
|
|
100
|
+
capabilities: { textDocument: { publishDiagnostics: {} } },
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
await this.sendRequest("initialize", initParams, timeout);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
107
|
+
this.logger?.warn(`LSP initialize failed (attempt 1): ${msg}`, {
|
|
108
|
+
command: config.command,
|
|
109
|
+
projectRoot,
|
|
110
|
+
});
|
|
111
|
+
// npx auto-install can blow past the initialize timeout on first run — retry once.
|
|
112
|
+
if (!(e instanceof Error) || !e.message.includes("initialize"))
|
|
113
|
+
throw e;
|
|
114
|
+
await this.shutdown();
|
|
115
|
+
await this.startServer(config, projectRoot);
|
|
116
|
+
try {
|
|
117
|
+
await this.sendRequest("initialize", initParams, timeout);
|
|
118
|
+
}
|
|
119
|
+
catch (retryErr) {
|
|
120
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
121
|
+
this.logger?.error(`LSP initialize failed (attempt 2, giving up): ${retryMsg}`, {
|
|
122
|
+
command: config.command,
|
|
123
|
+
projectRoot,
|
|
124
|
+
});
|
|
125
|
+
throw retryErr;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.initialized = true;
|
|
129
|
+
}
|
|
130
|
+
async startServer(config, projectRoot) {
|
|
131
|
+
if (config.autoInstall === false) {
|
|
132
|
+
try {
|
|
133
|
+
execSync(`where ${config.command}`, { stdio: "pipe", timeout: 3000 });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error(`${config.command} not found in PATH`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// When the command is npx, try to find the actual binary globally first.
|
|
140
|
+
// npx downloads packages on every invocation (slow on Windows, ~8-15s),
|
|
141
|
+
// even when the binary is already installed globally.
|
|
142
|
+
let effectiveCommand = config.command;
|
|
143
|
+
let effectiveArgs = config.args ?? [];
|
|
144
|
+
if (config.command === "npx" && effectiveArgs.length > 0) {
|
|
145
|
+
const binaryName = this.extractBinaryFromNpxArgs(effectiveArgs);
|
|
146
|
+
if (binaryName) {
|
|
147
|
+
const whichCmd = platform() === "win32" ? `where ${binaryName}` : `which ${binaryName}`;
|
|
148
|
+
try {
|
|
149
|
+
execSync(whichCmd, { stdio: "pipe", timeout: 3000 });
|
|
150
|
+
// Binary found globally — use it directly, skip npx.
|
|
151
|
+
effectiveCommand = binaryName;
|
|
152
|
+
effectiveArgs = effectiveArgs.filter((a) => a !== "--yes" && a !== "--package" && a !== binaryName && !this.isPackageVersion(a));
|
|
153
|
+
this.logger?.debug(`LSP: using globally installed ${binaryName} (skipped npx)`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Not found globally — fall back to npx (will auto-install).
|
|
157
|
+
this.logger?.debug(`LSP: ${binaryName} not found globally, falling back to npx`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const args = effectiveArgs;
|
|
163
|
+
const isWin = platform() === "win32";
|
|
164
|
+
let spawnCommand = resolveSpawnCommand(effectiveCommand);
|
|
165
|
+
let spawnArgs = args;
|
|
166
|
+
const spawnOpts = {
|
|
167
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
168
|
+
env: { ...process.env, ...config.env },
|
|
169
|
+
cwd: projectRoot,
|
|
170
|
+
};
|
|
171
|
+
if (isWin) {
|
|
172
|
+
// A `.cmd` shim can't be spawned directly under Node (`spawn npx EINVAL`) — run via cmd.exe.
|
|
173
|
+
spawnCommand = buildWinCommandLine(spawnCommand, args);
|
|
174
|
+
spawnArgs = [];
|
|
175
|
+
spawnOpts.shell = true;
|
|
176
|
+
}
|
|
177
|
+
const proc = spawn(spawnCommand, spawnArgs, spawnOpts);
|
|
178
|
+
proc.on("error", reject);
|
|
179
|
+
proc.stdout.on("data", (chunk) => {
|
|
180
|
+
this.handleData(chunk);
|
|
181
|
+
});
|
|
182
|
+
proc.stderr.on("data", () => { });
|
|
183
|
+
proc.once("spawn", () => {
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
this.process = proc;
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
if (!this.initialized && this.process) {
|
|
189
|
+
const msg = "LSP server start timeout";
|
|
190
|
+
this.logger?.error(msg, { command: config.command, projectRoot, timeout: config.timeout ?? 10000 });
|
|
191
|
+
reject(new Error(msg));
|
|
192
|
+
}
|
|
193
|
+
}, config.timeout ?? 10000);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
handleData(chunk) {
|
|
197
|
+
this.buffer += chunk.toString();
|
|
198
|
+
this.parseMessages();
|
|
199
|
+
}
|
|
200
|
+
parseMessages() {
|
|
201
|
+
while (true) {
|
|
202
|
+
if (this.contentLength === -1) {
|
|
203
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
204
|
+
if (headerEnd === -1)
|
|
205
|
+
return;
|
|
206
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
207
|
+
const match = header.match(/Content-Length: (\d+)/i);
|
|
208
|
+
if (!match) {
|
|
209
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
210
|
+
this.contentLength = -1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
this.contentLength = parseInt(match[1], 10);
|
|
214
|
+
// Guard against malicious/buggy servers sending absurd Content-Length
|
|
215
|
+
if (this.contentLength > 10 * 1024 * 1024) {
|
|
216
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
217
|
+
this.contentLength = -1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
221
|
+
}
|
|
222
|
+
if (this.buffer.length < this.contentLength)
|
|
223
|
+
return;
|
|
224
|
+
const body = this.buffer.slice(0, this.contentLength);
|
|
225
|
+
this.buffer = this.buffer.slice(this.contentLength);
|
|
226
|
+
this.contentLength = -1;
|
|
227
|
+
try {
|
|
228
|
+
const msg = JSON.parse(body);
|
|
229
|
+
this.handleMessage(msg);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
this.logger?.debug(`LSP JSON parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
handleMessage(msg) {
|
|
237
|
+
if (msg.method === "textDocument/publishDiagnostics") {
|
|
238
|
+
const params = msg.params;
|
|
239
|
+
if (params?.diagnostics) {
|
|
240
|
+
this.diagnostics = params.diagnostics;
|
|
241
|
+
if (this.diagnosticsTimer) {
|
|
242
|
+
clearTimeout(this.diagnosticsTimer);
|
|
243
|
+
this.diagnosticsTimer = null;
|
|
244
|
+
}
|
|
245
|
+
this.diagnosticsResolve?.(this.diagnostics);
|
|
246
|
+
this.diagnosticsResolve = null;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (msg.id !== undefined && msg.id !== null) {
|
|
251
|
+
const pending = this.pending.get(msg.id);
|
|
252
|
+
if (pending) {
|
|
253
|
+
this.pending.delete(msg.id);
|
|
254
|
+
if (msg.error) {
|
|
255
|
+
pending.reject(new Error(String(msg.error.message ?? "LSP error")));
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
pending.resolve(msg.result);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
sendRequest(method, params, timeout) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const id = ++this.requestId;
|
|
266
|
+
this.pending.set(id, { resolve, reject });
|
|
267
|
+
const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
268
|
+
this.write(message);
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (this.pending.has(id)) {
|
|
271
|
+
this.pending.delete(id);
|
|
272
|
+
const msg = `LSP request timeout: ${method}`;
|
|
273
|
+
this.logger?.warn(msg, { method, timeout });
|
|
274
|
+
reject(new Error(msg));
|
|
275
|
+
}
|
|
276
|
+
}, timeout);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
sendNotification(method, params) {
|
|
280
|
+
const message = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
281
|
+
this.write(message);
|
|
282
|
+
}
|
|
283
|
+
write(message) {
|
|
284
|
+
if (!this.process?.stdin?.writable) {
|
|
285
|
+
this.logger?.debug("LSP write failed: process stdin not writable");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
|
|
289
|
+
try {
|
|
290
|
+
this.process.stdin.write(header + message);
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
this.logger?.debug(`LSP write error: ${e instanceof Error ? e.message : String(e)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async shutdown() {
|
|
297
|
+
try {
|
|
298
|
+
if (this.process && this.initialized && this.process.stdin?.writable) {
|
|
299
|
+
// Await the shutdown response (with timeout) so the server has time to
|
|
300
|
+
// acknowledge before we send exit and kill the process. The old code
|
|
301
|
+
// fired shutdown as fire-and-forget, then killed immediately — the
|
|
302
|
+
// server never had a chance to reply, producing spurious timeout warnings.
|
|
303
|
+
try {
|
|
304
|
+
await this.sendRequest("shutdown", null, 3000);
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
this.logger?.debug(`LSP shutdown request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
308
|
+
}
|
|
309
|
+
this.sendNotification("exit", null);
|
|
310
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
this.logger?.debug(`LSP shutdown error: ${e instanceof Error ? e.message : String(e)}`);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
if (this.process) {
|
|
318
|
+
// The direct child on win32 is the cmd.exe wrapper; killing it alone
|
|
319
|
+
// orphans the npx → node → tsserver tree (observed as leaked node
|
|
320
|
+
// processes after every check). Tree-kill so the whole chain dies.
|
|
321
|
+
try {
|
|
322
|
+
killTree(this.process);
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
this.logger?.debug(`LSP killTree error: ${e instanceof Error ? e.message : String(e)}`);
|
|
326
|
+
}
|
|
327
|
+
this.process = null;
|
|
328
|
+
}
|
|
329
|
+
this.initialized = false;
|
|
330
|
+
this.buffer = "";
|
|
331
|
+
this.contentLength = -1;
|
|
332
|
+
if (this.diagnosticsTimer) {
|
|
333
|
+
clearTimeout(this.diagnosticsTimer);
|
|
334
|
+
this.diagnosticsTimer = null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
pathToUri(filePath) {
|
|
339
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
340
|
+
return /^[a-zA-Z]:/.test(normalized) ? `file:///${normalized}` : `file://${normalized}`;
|
|
341
|
+
}
|
|
342
|
+
languageFromPath(filePath) {
|
|
343
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
344
|
+
const map = {
|
|
345
|
+
ts: "typescript",
|
|
346
|
+
tsx: "typescriptreact",
|
|
347
|
+
mts: "typescript",
|
|
348
|
+
cts: "typescript",
|
|
349
|
+
js: "javascript",
|
|
350
|
+
jsx: "javascriptreact",
|
|
351
|
+
mjs: "javascript",
|
|
352
|
+
cjs: "javascript",
|
|
353
|
+
py: "python",
|
|
354
|
+
go: "go",
|
|
355
|
+
rs: "rust",
|
|
356
|
+
json: "json",
|
|
357
|
+
html: "html",
|
|
358
|
+
css: "css",
|
|
359
|
+
scss: "scss",
|
|
360
|
+
md: "markdown",
|
|
361
|
+
yaml: "yaml",
|
|
362
|
+
yml: "yaml",
|
|
363
|
+
};
|
|
364
|
+
return map[ext ?? ""] ?? "plaintext";
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Extract the actual binary name from npx args.
|
|
368
|
+
* For `npx --yes --package typescript-language-server --package typescript@5 typescript-language-server --stdio`
|
|
369
|
+
* returns `typescript-language-server` (the last non-flag arg before `--stdio`).
|
|
370
|
+
*/
|
|
371
|
+
extractBinaryFromNpxArgs(args) {
|
|
372
|
+
// Find the binary: last arg that doesn't start with -- and isn't a package version
|
|
373
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
374
|
+
const arg = args[i];
|
|
375
|
+
if (arg === "--stdio")
|
|
376
|
+
continue;
|
|
377
|
+
if (arg.startsWith("--"))
|
|
378
|
+
return null; // unexpected format
|
|
379
|
+
if (this.isPackageVersion(arg))
|
|
380
|
+
continue;
|
|
381
|
+
return arg;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
/** Check if a string looks like a package version specifier (e.g. "typescript@5"). */
|
|
386
|
+
isPackageVersion(s) {
|
|
387
|
+
return /^[\w-]+@[\d.+*-]/.test(s);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { delimiter, join } from "path";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
const WIN_EXTS = [".cmd", ".bat", ".exe", ".com"];
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the command string passed to `child_process.spawn` on Windows.
|
|
7
|
+
*
|
|
8
|
+
* Node's `spawn` cannot execute `.cmd`/`.bat` shims directly without a shell,
|
|
9
|
+
* and a bare `npx` fails with `spawn npx ENOENT` because on Windows the
|
|
10
|
+
* real executable is `npx.cmd`. This returns the first existing shim
|
|
11
|
+
* (`npx.cmd`, `npx.bat`, `npx.exe`, ...) found on PATH, or the bare command
|
|
12
|
+
* when nothing exists (the spawn will then fail with a meaningful ENOENT).
|
|
13
|
+
*
|
|
14
|
+
* Non-Windows platforms and commands that already carry an extension or a
|
|
15
|
+
* path separator are returned unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveSpawnCommand(command, platformName = platform(), pathEnv = process.env.PATH ?? "") {
|
|
18
|
+
if (platformName !== "win32")
|
|
19
|
+
return command;
|
|
20
|
+
// Already a path or has an extension — pass through.
|
|
21
|
+
if (command.includes("/") ||
|
|
22
|
+
command.includes("\\") ||
|
|
23
|
+
WIN_EXTS.some((ext) => command.toLowerCase().endsWith(ext))) {
|
|
24
|
+
return command;
|
|
25
|
+
}
|
|
26
|
+
const dirs = pathEnv.split(delimiter).filter(Boolean);
|
|
27
|
+
for (const dir of dirs) {
|
|
28
|
+
for (const ext of WIN_EXTS) {
|
|
29
|
+
const candidate = join(dir, `${command}${ext}`);
|
|
30
|
+
if (existsSync(candidate))
|
|
31
|
+
return `${command}${ext}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return command;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Join a resolved command and its args into a single cmd.exe command line.
|
|
38
|
+
*
|
|
39
|
+
* Under Node on Windows, `spawn` of a `.cmd`/`.bat` shim WITHOUT a shell throws
|
|
40
|
+
* `spawn EINVAL` (CreateProcess cannot run batch files directly). Resolving
|
|
41
|
+
* `npx` → `npx.cmd` fixes the old `ENOENT` but not the `EINVAL` — the spawn
|
|
42
|
+
* still has no shell. The reliable fix is to run the whole command line through
|
|
43
|
+
* `cmd.exe /d /s /c` (i.e. `spawn(line, { shell: true })`). This helper builds
|
|
44
|
+
* that line, quoting any argument that contains cmd.exe metacharacters so the
|
|
45
|
+
* package names and flags in LSP configs (`typescript-language-server --stdio`,
|
|
46
|
+
* `pyright --stdio`) survive the round-trip.
|
|
47
|
+
*
|
|
48
|
+
* Non-Windows platforms never call this (they spawn the resolved command +
|
|
49
|
+
* args directly, no shell).
|
|
50
|
+
*/
|
|
51
|
+
export function buildWinCommandLine(command, args) {
|
|
52
|
+
const quote = (part) => {
|
|
53
|
+
if (part === "")
|
|
54
|
+
return '""';
|
|
55
|
+
if (!/[\s"&|^<>()%!]/.test(part))
|
|
56
|
+
return part;
|
|
57
|
+
return `"${part.replace(/"/g, '\\"')}"`;
|
|
58
|
+
};
|
|
59
|
+
return [command, ...args].map(quote).join(" ");
|
|
60
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP server defaults.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: prefer `npx` (auto-downloads) over globally installed binaries.
|
|
5
|
+
* Servers using system tools (rust-analyzer, gopls) have `autoInstall: false`
|
|
6
|
+
* and are skipped silently when not found.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_LSP_CONFIG = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
timeout: 15000,
|
|
11
|
+
servers: {
|
|
12
|
+
typescript: {
|
|
13
|
+
command: "npx",
|
|
14
|
+
args: [
|
|
15
|
+
"--yes",
|
|
16
|
+
"--package",
|
|
17
|
+
"typescript-language-server",
|
|
18
|
+
"--package",
|
|
19
|
+
"typescript@5",
|
|
20
|
+
"typescript-language-server",
|
|
21
|
+
"--stdio",
|
|
22
|
+
],
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
autoInstall: true,
|
|
25
|
+
workspaceMarkers: ["package.json", "tsconfig.json"],
|
|
26
|
+
},
|
|
27
|
+
javascript: {
|
|
28
|
+
command: "npx",
|
|
29
|
+
args: [
|
|
30
|
+
"--yes",
|
|
31
|
+
"--package",
|
|
32
|
+
"typescript-language-server",
|
|
33
|
+
"--package",
|
|
34
|
+
"typescript@5",
|
|
35
|
+
"typescript-language-server",
|
|
36
|
+
"--stdio",
|
|
37
|
+
],
|
|
38
|
+
timeout: 30000,
|
|
39
|
+
autoInstall: true,
|
|
40
|
+
workspaceMarkers: ["package.json", "tsconfig.json"],
|
|
41
|
+
},
|
|
42
|
+
python: {
|
|
43
|
+
command: "npx",
|
|
44
|
+
args: ["pyright", "--stdio"],
|
|
45
|
+
timeout: 15000,
|
|
46
|
+
autoInstall: true,
|
|
47
|
+
workspaceMarkers: ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
48
|
+
},
|
|
49
|
+
rust: {
|
|
50
|
+
command: "rust-analyzer",
|
|
51
|
+
timeout: 15000,
|
|
52
|
+
autoInstall: false,
|
|
53
|
+
workspaceMarkers: ["Cargo.toml"],
|
|
54
|
+
},
|
|
55
|
+
go: {
|
|
56
|
+
command: "gopls",
|
|
57
|
+
timeout: 15000,
|
|
58
|
+
autoInstall: false,
|
|
59
|
+
workspaceMarkers: ["go.mod"],
|
|
60
|
+
},
|
|
61
|
+
json: {
|
|
62
|
+
command: "npx",
|
|
63
|
+
args: [
|
|
64
|
+
"--yes",
|
|
65
|
+
"--package",
|
|
66
|
+
"vscode-langservers-extracted",
|
|
67
|
+
"vscode-json-language-server",
|
|
68
|
+
"--stdio",
|
|
69
|
+
],
|
|
70
|
+
timeout: 30000,
|
|
71
|
+
autoInstall: true,
|
|
72
|
+
workspaceMarkers: ["package.json"],
|
|
73
|
+
},
|
|
74
|
+
html: {
|
|
75
|
+
command: "npx",
|
|
76
|
+
args: [
|
|
77
|
+
"--yes",
|
|
78
|
+
"--package",
|
|
79
|
+
"vscode-langservers-extracted",
|
|
80
|
+
"--package",
|
|
81
|
+
"typescript@5",
|
|
82
|
+
"vscode-html-language-server",
|
|
83
|
+
"--stdio",
|
|
84
|
+
],
|
|
85
|
+
timeout: 60000,
|
|
86
|
+
autoInstall: true,
|
|
87
|
+
workspaceMarkers: ["package.json"],
|
|
88
|
+
},
|
|
89
|
+
css: {
|
|
90
|
+
command: "npx",
|
|
91
|
+
args: [
|
|
92
|
+
"--yes",
|
|
93
|
+
"--package",
|
|
94
|
+
"vscode-langservers-extracted",
|
|
95
|
+
"--package",
|
|
96
|
+
"typescript@5",
|
|
97
|
+
"vscode-css-language-server",
|
|
98
|
+
"--stdio",
|
|
99
|
+
],
|
|
100
|
+
timeout: 60000,
|
|
101
|
+
autoInstall: true,
|
|
102
|
+
workspaceMarkers: ["package.json"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
const LANGUAGE_MAP = {
|
|
107
|
+
ts: "typescript",
|
|
108
|
+
tsx: "typescript",
|
|
109
|
+
mts: "typescript",
|
|
110
|
+
cts: "typescript",
|
|
111
|
+
js: "javascript",
|
|
112
|
+
jsx: "javascript",
|
|
113
|
+
mjs: "javascript",
|
|
114
|
+
cjs: "javascript",
|
|
115
|
+
py: "python",
|
|
116
|
+
rs: "rust",
|
|
117
|
+
go: "go",
|
|
118
|
+
json: "json",
|
|
119
|
+
html: "html",
|
|
120
|
+
css: "css",
|
|
121
|
+
scss: "css",
|
|
122
|
+
};
|
|
123
|
+
/** Language id for a file extension (via LANGUAGE_MAP), or null when unsupported. */
|
|
124
|
+
export function getLanguageForFile(filePath) {
|
|
125
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
126
|
+
if (!ext)
|
|
127
|
+
return null;
|
|
128
|
+
return LANGUAGE_MAP[ext] ?? null;
|
|
129
|
+
}
|
|
130
|
+
export function getServerForFile(filePath, config) {
|
|
131
|
+
const lang = getLanguageForFile(filePath);
|
|
132
|
+
if (!lang)
|
|
133
|
+
return null;
|
|
134
|
+
return config.servers[lang] ?? null;
|
|
135
|
+
}
|