openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
393 lines (392 loc) • 12.8 kB
JavaScript
import { s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { s as sanitizeHostExecEnv } from "./host-env-security-RnaFYKhk.js";
import { a as logWarn, t as logDebug } from "./logger-lqqYRtFw.js";
import { t as killProcessTree } from "./kill-tree-kSm0C74g.js";
import { o as setPluginToolMeta } from "./tools-oBw0X8xm.js";
import { a as resolveWindowsSpawnProgram, r as materializeWindowsSpawnProgram } from "./windows-spawn-C3eNecff.js";
import { n as loadEnabledBundleLspConfig } from "./bundle-lsp-BWPiW--l.js";
import { n as resolveStdioMcpServerLaunchConfig, t as describeStdioMcpServerLaunchConfig } from "./mcp-stdio-VQ8rIEem.js";
import { spawn } from "node:child_process";
//#region src/agents/embedded-agent-lsp.ts
/** Resolve enabled embedded-agent LSP servers and diagnostics. */
function loadEmbeddedAgentLspConfig(params) {
const bundleLsp = loadEnabledBundleLspConfig({
workspaceDir: params.workspaceDir,
cfg: params.cfg
});
return {
lspServers: { ...bundleLsp.config.lspServers },
diagnostics: bundleLsp.diagnostics
};
}
//#endregion
//#region src/agents/agent-bundle-lsp-runtime.ts
/** Session-scoped embedded LSP runtime and tool materialization for agent bundles. */
const LSP_SHUTDOWN_GRACE_MS = 500;
const LSP_PROCESS_TREE_KILL_GRACE_MS = 1e3;
const activeBundleLspSessions = /* @__PURE__ */ new Set();
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, Math.max(1, ms)).unref?.();
});
}
/** Spawns one LSP server process using sanitized host env and Windows shim handling. */
function spawnLspServerProcess(config) {
const mergedEnv = sanitizeHostExecEnv({
baseEnv: process.env,
overrides: config.env ?? null
});
const invocation = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
command: config.command,
env: mergedEnv,
allowShellFallback: true
}), config.args ?? []);
return spawn(invocation.command, invocation.argv, {
stdio: [
"pipe",
"pipe",
"pipe"
],
env: mergedEnv,
cwd: config.cwd,
detached: process.platform !== "win32",
windowsHide: invocation.windowsHide ?? process.platform === "win32",
shell: invocation.shell
});
}
function createLspSession(serverName, child) {
return {
serverName,
process: child,
requestId: 0,
pendingRequests: /* @__PURE__ */ new Map(),
buffer: Buffer.alloc(0),
initialized: false,
capabilities: {},
disposed: false
};
}
function registerActiveLspSession(session) {
activeBundleLspSessions.add(session);
}
function attachLspProcessHandlers(session) {
session.process.stdout?.on("data", (chunk) => handleIncomingData(session, chunk));
session.process.stderr?.setEncoding("utf-8");
session.process.stderr?.on("data", (chunk) => {
for (const line of chunk.split(/\r?\n/).filter(Boolean)) logDebug(`bundle-lsp:${session.serverName}: ${line.trim()}`);
});
}
function encodeLspMessage(body) {
const json = JSON.stringify(body);
return `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n${json}`;
}
function parseLspMessages(buffer) {
const messages = [];
let remaining = buffer;
const headerSeparator = Buffer.from("\r\n\r\n", "ascii");
while (true) {
const headerEnd = remaining.indexOf(headerSeparator);
if (headerEnd === -1) break;
const match = remaining.subarray(0, headerEnd).toString("ascii").match(/Content-Length:\s*(\d+)/i);
if (!match) {
remaining = remaining.subarray(headerEnd + headerSeparator.length);
continue;
}
const contentLength = Number.parseInt(match[1], 10);
const bodyStart = headerEnd + headerSeparator.length;
const bodyEnd = bodyStart + contentLength;
if (remaining.length < bodyEnd) break;
try {
const body = remaining.subarray(bodyStart, bodyEnd).toString("utf8");
messages.push(JSON.parse(body));
} catch {}
remaining = remaining.subarray(bodyEnd);
}
return {
messages,
remaining
};
}
function sendRequest(session, method, params) {
const id = ++session.requestId;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (session.pendingRequests.has(id)) {
session.pendingRequests.delete(id);
reject(/* @__PURE__ */ new Error(`LSP request ${method} timed out`));
}
}, 1e4);
timeout.unref?.();
session.pendingRequests.set(id, {
resolve,
reject,
timeout
});
const encoded = encodeLspMessage({
jsonrpc: "2.0",
id,
method,
params
});
session.process.stdin?.write(encoded, "utf-8");
});
}
function handleIncomingData(session, chunk) {
session.buffer = Buffer.concat([session.buffer, typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk]);
const { messages, remaining } = parseLspMessages(session.buffer);
session.buffer = remaining.length === 0 ? Buffer.alloc(0) : Buffer.from(remaining);
for (const msg of messages) {
if (typeof msg !== "object" || msg === null) continue;
const record = msg;
if ("id" in record && typeof record.id === "number") {
const pending = session.pendingRequests.get(record.id);
if (pending) {
session.pendingRequests.delete(record.id);
clearTimeout(pending.timeout);
if ("error" in record) pending.reject(new Error(JSON.stringify(record.error)));
else pending.resolve(record.result);
}
}
if ("method" in record && !("id" in record)) logDebug(`bundle-lsp:${session.serverName}: notification ${String(record.method)}`);
}
}
async function initializeSession(session) {
const result = await sendRequest(session, "initialize", {
processId: process.pid,
rootUri: null,
capabilities: { textDocument: {
hover: { contentFormat: ["plaintext", "markdown"] },
completion: { completionItem: { snippetSupport: false } },
definition: {},
references: {}
} }
});
session.process.stdin?.write(encodeLspMessage({
jsonrpc: "2.0",
method: "initialized",
params: {}
}), "utf-8");
session.initialized = true;
return result?.capabilities ?? {};
}
function hasLspProcessExited(child) {
return child.exitCode !== null || child.signalCode !== null;
}
function terminateLspProcessTree(session) {
const pid = session.process.pid;
if (pid && !hasLspProcessExited(session.process)) {
killProcessTree(pid, { graceMs: LSP_PROCESS_TREE_KILL_GRACE_MS });
return;
}
if (!hasLspProcessExited(session.process)) session.process.kill("SIGTERM");
}
async function disposeSession(session) {
if (session.disposed) return;
session.disposed = true;
activeBundleLspSessions.delete(session);
if (session.initialized) try {
const shutdown = sendRequest(session, "shutdown").catch(() => void 0);
await Promise.race([shutdown, delay(LSP_SHUTDOWN_GRACE_MS)]);
session.process.stdin?.write(encodeLspMessage({
jsonrpc: "2.0",
method: "exit",
params: null
}), "utf-8");
} catch {}
for (const [, pending] of session.pendingRequests) {
clearTimeout(pending.timeout);
pending.reject(/* @__PURE__ */ new Error("LSP session disposed"));
}
session.pendingRequests.clear();
terminateLspProcessTree(session);
}
async function disposeSessions(sessions) {
await Promise.allSettled(Array.from(sessions, (session) => disposeSession(session)));
}
function createLspPositionTool(params) {
return {
name: params.toolName,
label: params.label,
description: params.description,
parameters: {
type: "object",
properties: {
uri: {
type: "string",
description: "File URI (file:///path/to/file)"
},
line: {
type: "number",
description: "Zero-based line number"
},
character: {
type: "number",
description: "Zero-based character offset"
}
},
required: [
"uri",
"line",
"character"
]
},
execute: async (_toolCallId, input) => {
const position = input;
const result = await sendRequest(params.session, params.method, {
textDocument: { uri: position.uri },
position: {
line: position.line,
character: position.character
}
});
return formatLspResult(params.session.serverName, params.resultLabel, result);
}
};
}
function buildLspTools(session) {
const tools = [];
const caps = session.capabilities;
const serverLabel = session.serverName;
if (caps.hoverProvider) tools.push(createLspPositionTool({
session,
toolName: `lsp_hover_${serverLabel}`,
label: `LSP Hover (${serverLabel})`,
description: `Get hover information for a symbol at a position in a file via the ${serverLabel} language server.`,
method: "textDocument/hover",
resultLabel: "hover"
}));
if (caps.definitionProvider) tools.push(createLspPositionTool({
session,
toolName: `lsp_definition_${serverLabel}`,
label: `LSP Go to Definition (${serverLabel})`,
description: `Find the definition of a symbol at a position in a file via the ${serverLabel} language server.`,
method: "textDocument/definition",
resultLabel: "definition"
}));
if (caps.referencesProvider) tools.push({
name: `lsp_references_${serverLabel}`,
label: `LSP Find References (${serverLabel})`,
description: `Find all references to a symbol at a position in a file via the ${serverLabel} language server.`,
parameters: {
type: "object",
properties: {
uri: {
type: "string",
description: "File URI (file:///path/to/file)"
},
line: {
type: "number",
description: "Zero-based line number"
},
character: {
type: "number",
description: "Zero-based character offset"
},
includeDeclaration: {
type: "boolean",
description: "Include the declaration in results"
}
},
required: [
"uri",
"line",
"character"
]
},
execute: async (_toolCallId, input) => {
const params = input;
return formatLspResult(serverLabel, "references", await sendRequest(session, "textDocument/references", {
textDocument: { uri: params.uri },
position: {
line: params.line,
character: params.character
},
context: { includeDeclaration: params.includeDeclaration ?? true }
}));
}
});
return tools;
}
function formatLspResult(serverName, method, result) {
return {
content: [{
type: "text",
text: result !== null && result !== void 0 ? JSON.stringify(result, null, 2) : `No ${method} result from ${serverName}`
}],
details: {
lspServer: serverName,
lspMethod: method
}
};
}
async function createBundleLspToolRuntime(params) {
const loaded = loadEmbeddedAgentLspConfig({
workspaceDir: params.workspaceDir,
cfg: params.cfg
});
for (const diagnostic of loaded.diagnostics) logWarn(`bundle-lsp: ${diagnostic.pluginId}: ${diagnostic.message}`);
if (Object.keys(loaded.lspServers).length === 0) return {
tools: [],
sessions: [],
dispose: async () => {}
};
const reservedNames = new Set(Array.from(params.reservedToolNames ?? [], (name) => normalizeOptionalLowercaseString(name)).filter(Boolean));
const sessions = [];
const tools = [];
try {
for (const [serverName, rawServer] of Object.entries(loaded.lspServers)) {
const launch = resolveStdioMcpServerLaunchConfig(rawServer);
if (!launch.ok) {
logWarn(`bundle-lsp: skipped server "${serverName}" because ${launch.reason}.`);
continue;
}
const launchConfig = launch.config;
let session;
try {
session = createLspSession(serverName, spawnLspServerProcess(launchConfig));
registerActiveLspSession(session);
attachLspProcessHandlers(session);
const capabilities = await initializeSession(session);
session.capabilities = capabilities;
sessions.push(session);
const serverTools = buildLspTools(session);
for (const tool of serverTools) {
const normalizedName = normalizeOptionalLowercaseString(tool.name);
if (!normalizedName) continue;
if (reservedNames.has(normalizedName)) {
logWarn(`bundle-lsp: skipped tool "${tool.name}" from server "${serverName}" because the name already exists.`);
continue;
}
reservedNames.add(normalizedName);
setPluginToolMeta(tool, {
pluginId: "bundle-lsp",
optional: false
});
tools.push(tool);
}
logDebug(`bundle-lsp: started "${serverName}" (${describeStdioMcpServerLaunchConfig(launchConfig)}) with ${serverTools.length} tools`);
} catch (error) {
if (session) await disposeSession(session);
logWarn(`bundle-lsp: failed to start server "${serverName}" (${describeStdioMcpServerLaunchConfig(launchConfig)}): ${String(error)}`);
}
}
return {
tools,
sessions: sessions.map((s) => ({
serverName: s.serverName,
capabilities: s.capabilities
})),
dispose: async () => {
await disposeSessions(sessions);
}
};
} catch (error) {
await disposeSessions(sessions);
throw error;
}
}
async function disposeAllBundleLspRuntimes() {
await disposeSessions(activeBundleLspSessions);
}
//#endregion
export { disposeAllBundleLspRuntimes as n, spawnLspServerProcess as r, createBundleLspToolRuntime as t };