@cruxy/cli 0.19.0 → 0.21.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/dist/approval/classify.js +24 -0
- package/dist/approval/policy.js +7 -0
- package/dist/approval/prompt.js +7 -0
- package/dist/approval/types.d.ts +6 -0
- package/dist/brand/voice.d.ts +1 -1
- package/dist/brand/voice.js +1 -1
- package/dist/cli/commands/mcp.d.ts +9 -0
- package/dist/cli/commands/mcp.js +87 -0
- package/dist/cli/commands/run.js +30 -2
- package/dist/cli/program.js +2 -0
- package/dist/cli/session-factory.d.ts +2 -2
- package/dist/cli/session-factory.js +21 -2
- package/dist/config/schema.d.ts +344 -33
- package/dist/config/schema.js +94 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +8 -0
- package/dist/errors/constructors.d.ts +40 -0
- package/dist/errors/constructors.js +113 -0
- package/dist/errors/types.d.ts +19 -0
- package/dist/errors/types.js +32 -0
- package/dist/lsp/client.d.ts +25 -0
- package/dist/lsp/client.js +43 -0
- package/dist/lsp/index.d.ts +8 -0
- package/dist/lsp/index.js +8 -0
- package/dist/lsp/pool.d.ts +48 -0
- package/dist/lsp/pool.js +132 -0
- package/dist/lsp/registry.d.ts +38 -0
- package/dist/lsp/registry.js +133 -0
- package/dist/lsp/server.d.ts +48 -0
- package/dist/lsp/server.js +264 -0
- package/dist/lsp/service.d.ts +44 -0
- package/dist/lsp/service.js +76 -0
- package/dist/lsp/tools/common.d.ts +23 -0
- package/dist/lsp/tools/common.js +75 -0
- package/dist/lsp/tools/find-definition.d.ts +23 -0
- package/dist/lsp/tools/find-definition.js +41 -0
- package/dist/lsp/tools/find-references.d.ts +23 -0
- package/dist/lsp/tools/find-references.js +41 -0
- package/dist/lsp/tools/get-diagnostics.d.ts +17 -0
- package/dist/lsp/tools/get-diagnostics.js +43 -0
- package/dist/lsp/tools/hover.d.ts +23 -0
- package/dist/lsp/tools/hover.js +38 -0
- package/dist/lsp/tools/index.d.ts +4 -0
- package/dist/lsp/tools/index.js +4 -0
- package/dist/lsp/transport.d.ts +39 -0
- package/dist/lsp/transport.js +208 -0
- package/dist/lsp/types.d.ts +107 -0
- package/dist/lsp/types.js +1 -0
- package/dist/mcp/adapter.d.ts +44 -0
- package/dist/mcp/adapter.js +70 -0
- package/dist/mcp/bounds.d.ts +35 -0
- package/dist/mcp/bounds.js +36 -0
- package/dist/mcp/client.d.ts +19 -0
- package/dist/mcp/client.js +93 -0
- package/dist/mcp/demarcate.d.ts +12 -0
- package/dist/mcp/demarcate.js +71 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/service.d.ts +54 -0
- package/dist/mcp/service.js +99 -0
- package/dist/mcp/transport.d.ts +30 -0
- package/dist/mcp/transport.js +188 -0
- package/dist/mcp/trust-gate.d.ts +35 -0
- package/dist/mcp/trust-gate.js +40 -0
- package/dist/mcp/trust.d.ts +52 -0
- package/dist/mcp/trust.js +111 -0
- package/dist/mcp/types.d.ts +52 -0
- package/dist/mcp/types.js +7 -0
- package/dist/tools/file/grep-files.d.ts +2 -2
- package/dist/tools/registry.js +3 -1
- package/dist/tools/types.d.ts +15 -1
- package/dist/utils/child-tree.d.ts +35 -0
- package/dist/utils/child-tree.js +76 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatLocations, runLspTool } from "./common.js";
|
|
3
|
+
const parameters = z.object({
|
|
4
|
+
file: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("Project-relative path to the file containing the symbol."),
|
|
8
|
+
line: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.positive()
|
|
12
|
+
.describe("1-based line number of the symbol to resolve."),
|
|
13
|
+
column: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.positive()
|
|
17
|
+
.describe("1-based column of the symbol (position of the identifier)."),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* LSP go-to-definition (C.12). Read-only — no approval, like search_codebase.
|
|
21
|
+
* Returns the definition site(s) as `path:line:col-endLine:endCol`. A missing
|
|
22
|
+
* language server is a coded, actionable error (distinct from "no definition
|
|
23
|
+
* found", which is an honest empty result).
|
|
24
|
+
*/
|
|
25
|
+
export const findDefinitionTool = {
|
|
26
|
+
name: "find_definition",
|
|
27
|
+
description: "Resolve where a symbol is defined using the project's language server (go-to-definition). Give the file and the 1-based line/column of the identifier. Returns definition locations as 'path:line:col-endLine:endCol'. Read-only, no approval. Precise where grep is textual — prefer this to jump to a symbol's definition.",
|
|
28
|
+
parameters,
|
|
29
|
+
execute(input, ctx) {
|
|
30
|
+
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
31
|
+
const locations = await service.definition(absFile, input.line, input.column);
|
|
32
|
+
if (locations.length === 0) {
|
|
33
|
+
return { ok: true, output: "(no definition found)" };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
output: formatLocations(locations, ctx.config.lsp.maxResults),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "../../tools/types.js";
|
|
3
|
+
declare const parameters: z.ZodObject<{
|
|
4
|
+
file: z.ZodString;
|
|
5
|
+
line: z.ZodNumber;
|
|
6
|
+
column: z.ZodNumber;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}, {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* LSP find-references (C.12). Read-only — no approval. Returns every use of the
|
|
18
|
+
* symbol (declaration included) as `path:line:col-endLine:endCol`, capped to
|
|
19
|
+
* `lsp.maxResults` with an "N more" note so a hot symbol can't flood the output.
|
|
20
|
+
* A missing server is a coded error, distinct from an honest "no references".
|
|
21
|
+
*/
|
|
22
|
+
export declare const findReferencesTool: Tool<typeof parameters>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatLocations, runLspTool } from "./common.js";
|
|
3
|
+
const parameters = z.object({
|
|
4
|
+
file: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("Project-relative path to the file containing the symbol."),
|
|
8
|
+
line: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.positive()
|
|
12
|
+
.describe("1-based line number of the symbol."),
|
|
13
|
+
column: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.positive()
|
|
17
|
+
.describe("1-based column of the symbol (position of the identifier)."),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* LSP find-references (C.12). Read-only — no approval. Returns every use of the
|
|
21
|
+
* symbol (declaration included) as `path:line:col-endLine:endCol`, capped to
|
|
22
|
+
* `lsp.maxResults` with an "N more" note so a hot symbol can't flood the output.
|
|
23
|
+
* A missing server is a coded error, distinct from an honest "no references".
|
|
24
|
+
*/
|
|
25
|
+
export const findReferencesTool = {
|
|
26
|
+
name: "find_references",
|
|
27
|
+
description: "Find all references to a symbol using the project's language server. Give the file and the 1-based line/column of the identifier. Returns use sites as 'path:line:col-endLine:endCol' (capped, with an 'N more' note). Read-only, no approval. Precise where grep is textual — prefer this to see every caller/user of a symbol.",
|
|
28
|
+
parameters,
|
|
29
|
+
execute(input, ctx) {
|
|
30
|
+
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
31
|
+
const locations = await service.references(absFile, input.line, input.column);
|
|
32
|
+
if (locations.length === 0) {
|
|
33
|
+
return { ok: true, output: "(no references found)" };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
output: formatLocations(locations, ctx.config.lsp.maxResults),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "../../tools/types.js";
|
|
3
|
+
declare const parameters: z.ZodObject<{
|
|
4
|
+
file: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
file: string;
|
|
7
|
+
}, {
|
|
8
|
+
file: string;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* LSP diagnostics (C.12): the language server's errors/warnings for a file.
|
|
12
|
+
* Read-only — no approval. Returns each diagnostic as
|
|
13
|
+
* `severity path:line:col message [source]`, capped to `lsp.maxResults`. A
|
|
14
|
+
* missing server is a coded error; a clean file is an honest empty result.
|
|
15
|
+
*/
|
|
16
|
+
export declare const getDiagnosticsTool: Tool<typeof parameters>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runLspTool } from "./common.js";
|
|
3
|
+
const parameters = z.object({
|
|
4
|
+
file: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("Project-relative path to the file to diagnose."),
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* LSP diagnostics (C.12): the language server's errors/warnings for a file.
|
|
11
|
+
* Read-only — no approval. Returns each diagnostic as
|
|
12
|
+
* `severity path:line:col message [source]`, capped to `lsp.maxResults`. A
|
|
13
|
+
* missing server is a coded error; a clean file is an honest empty result.
|
|
14
|
+
*/
|
|
15
|
+
export const getDiagnosticsTool = {
|
|
16
|
+
name: "get_diagnostics",
|
|
17
|
+
description: "Get the language server's diagnostics (errors, warnings) for a file. Give the project-relative path. Returns diagnostics as 'severity path:line:col message'. Read-only, no approval. Use this after an edit to see type errors the compiler/linter reports, without running a build.",
|
|
18
|
+
parameters,
|
|
19
|
+
execute(input, ctx) {
|
|
20
|
+
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
21
|
+
const diagnostics = await service.diagnostics(absFile);
|
|
22
|
+
if (diagnostics.length === 0) {
|
|
23
|
+
return { ok: true, output: "(no diagnostics)" };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
output: formatDiagnostics(diagnostics, ctx.config.lsp.maxResults),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
function formatDiagnostics(diagnostics, max) {
|
|
33
|
+
const shown = diagnostics.slice(0, max);
|
|
34
|
+
const lines = shown.map((d) => {
|
|
35
|
+
const src = d.source ? ` [${d.source}]` : "";
|
|
36
|
+
return `${d.severity} ${d.path}:${d.range.startLine}:${d.range.startCol} ${d.message}${src}`;
|
|
37
|
+
});
|
|
38
|
+
const omitted = diagnostics.length - shown.length;
|
|
39
|
+
if (omitted > 0) {
|
|
40
|
+
lines.push(`… [${omitted} more diagnostic(s) omitted]`);
|
|
41
|
+
}
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "../../tools/types.js";
|
|
3
|
+
declare const parameters: z.ZodObject<{
|
|
4
|
+
file: z.ZodString;
|
|
5
|
+
line: z.ZodNumber;
|
|
6
|
+
column: z.ZodNumber;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}, {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* LSP hover (C.12): type signature / documentation for the symbol at a position.
|
|
18
|
+
* Read-only — no approval. Returns the server's hover text (markup flattened to
|
|
19
|
+
* plain text). A missing server is a coded error; a symbol with no hover info is
|
|
20
|
+
* an honest empty result.
|
|
21
|
+
*/
|
|
22
|
+
export declare const hoverTool: Tool<typeof parameters>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runLspTool } from "./common.js";
|
|
3
|
+
const parameters = z.object({
|
|
4
|
+
file: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.describe("Project-relative path to the file containing the symbol."),
|
|
8
|
+
line: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.positive()
|
|
12
|
+
.describe("1-based line number of the symbol."),
|
|
13
|
+
column: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.positive()
|
|
17
|
+
.describe("1-based column of the symbol (position of the identifier)."),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* LSP hover (C.12): type signature / documentation for the symbol at a position.
|
|
21
|
+
* Read-only — no approval. Returns the server's hover text (markup flattened to
|
|
22
|
+
* plain text). A missing server is a coded error; a symbol with no hover info is
|
|
23
|
+
* an honest empty result.
|
|
24
|
+
*/
|
|
25
|
+
export const hoverTool = {
|
|
26
|
+
name: "hover",
|
|
27
|
+
description: "Get type information and documentation for a symbol using the project's language server (hover). Give the file and the 1-based line/column of the identifier. Returns the symbol's type signature and docs. Read-only, no approval. Use this to learn a symbol's type without opening and reading its declaration.",
|
|
28
|
+
parameters,
|
|
29
|
+
execute(input, ctx) {
|
|
30
|
+
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
31
|
+
const hover = await service.hover(absFile, input.line, input.column);
|
|
32
|
+
if (!hover) {
|
|
33
|
+
return { ok: true, output: "(no hover information)" };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true, output: hover.contents };
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { killTree, killTrackedTrees, trackedTreeCount } from "../utils/child-tree.js";
|
|
2
|
+
import type { LspTransport, ServerSpec } from "./types.js";
|
|
3
|
+
export { killTree };
|
|
4
|
+
/** @deprecated Use the shared backstop; kept for LSP tests. */
|
|
5
|
+
export declare const killTrackedServers: typeof killTrackedTrees;
|
|
6
|
+
/** @deprecated Use the shared backstop; kept for LSP tests. */
|
|
7
|
+
export declare const trackedServerCount: typeof trackedTreeCount;
|
|
8
|
+
export declare class StdioTransport implements LspTransport {
|
|
9
|
+
private readonly child;
|
|
10
|
+
private nextId;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
private readonly notificationHandlers;
|
|
13
|
+
private crashHandler;
|
|
14
|
+
/** stdout parse buffer (header + body may arrive across chunks). */
|
|
15
|
+
private buffer;
|
|
16
|
+
private disposed;
|
|
17
|
+
/** Deregisters this process from the process-exit kill-tree backstop. */
|
|
18
|
+
private readonly unregisterCleanup;
|
|
19
|
+
constructor(spec: ServerSpec, root: string);
|
|
20
|
+
request(method: string, params: unknown, timeoutMs: number): Promise<unknown>;
|
|
21
|
+
notify(method: string, params: unknown): void;
|
|
22
|
+
onNotification(method: string, handler: (params: unknown) => void): void;
|
|
23
|
+
onCrash(handler: (info: {
|
|
24
|
+
code: number | null;
|
|
25
|
+
signal: string | null;
|
|
26
|
+
}) => void): void;
|
|
27
|
+
dispose(force?: boolean): Promise<void>;
|
|
28
|
+
private send;
|
|
29
|
+
private onStdout;
|
|
30
|
+
private dispatch;
|
|
31
|
+
private onExit;
|
|
32
|
+
private onSpawnError;
|
|
33
|
+
}
|
|
34
|
+
/** A per-request timeout, distinguishable from an ordinary LSP error response. */
|
|
35
|
+
export declare class TransportTimeoutError extends Error {
|
|
36
|
+
readonly method: string;
|
|
37
|
+
readonly timeoutMs: number;
|
|
38
|
+
constructor(method: string, timeoutMs: number);
|
|
39
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { killTree, killTrackedTrees, registerForCleanup, trackedTreeCount, } from "../utils/child-tree.js";
|
|
3
|
+
// Re-exported under their historical LSP names so callers and tests keep
|
|
4
|
+
// importing them from here; the machinery now lives in the shared child-tree
|
|
5
|
+
// backstop (also used by the C.27 MCP transport) so exit-time reaping is
|
|
6
|
+
// unified across every managed process tree.
|
|
7
|
+
export { killTree };
|
|
8
|
+
/** @deprecated Use the shared backstop; kept for LSP tests. */
|
|
9
|
+
export const killTrackedServers = killTrackedTrees;
|
|
10
|
+
/** @deprecated Use the shared backstop; kept for LSP tests. */
|
|
11
|
+
export const trackedServerCount = trackedTreeCount;
|
|
12
|
+
/**
|
|
13
|
+
* JSON-RPC 2.0 over a language server's stdio (C.12). Owns the child process:
|
|
14
|
+
* spawns it in its OWN process group (`detached`) so the whole tree is killable,
|
|
15
|
+
* frames messages with `Content-Length` headers, correlates responses to
|
|
16
|
+
* requests by id, times out per request, and detects a crash (unexpected exit).
|
|
17
|
+
*
|
|
18
|
+
* This is the real {@link LspTransport}; tests inject a fake peer instead, so no
|
|
19
|
+
* real language-server binary is required in CI.
|
|
20
|
+
*/
|
|
21
|
+
const GRACE_MS = 2000;
|
|
22
|
+
export class StdioTransport {
|
|
23
|
+
child;
|
|
24
|
+
nextId = 1;
|
|
25
|
+
pending = new Map();
|
|
26
|
+
notificationHandlers = new Map();
|
|
27
|
+
crashHandler = null;
|
|
28
|
+
/** stdout parse buffer (header + body may arrive across chunks). */
|
|
29
|
+
buffer = Buffer.alloc(0);
|
|
30
|
+
disposed = false;
|
|
31
|
+
/** Deregisters this process from the process-exit kill-tree backstop. */
|
|
32
|
+
unregisterCleanup;
|
|
33
|
+
constructor(spec, root) {
|
|
34
|
+
// `detached` makes the child a process-group leader so the whole tree can be
|
|
35
|
+
// killed via a negative-PID signal — same discipline as run_command (C.16).
|
|
36
|
+
this.child = spawn(spec.command, spec.args, {
|
|
37
|
+
cwd: root,
|
|
38
|
+
detached: true,
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
this.unregisterCleanup = registerForCleanup(this.child.pid);
|
|
42
|
+
this.child.stdout?.on("data", (chunk) => this.onStdout(chunk));
|
|
43
|
+
// Server stderr is diagnostic only; surface nothing by default (it's noisy).
|
|
44
|
+
this.child.stderr?.on("data", () => { });
|
|
45
|
+
this.child.on("exit", (code, signal) => this.onExit(code, signal));
|
|
46
|
+
this.child.on("error", (err) => this.onSpawnError(err));
|
|
47
|
+
}
|
|
48
|
+
request(method, params, timeoutMs) {
|
|
49
|
+
if (this.disposed) {
|
|
50
|
+
return Promise.reject(new Error("transport disposed"));
|
|
51
|
+
}
|
|
52
|
+
const id = this.nextId++;
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
this.pending.delete(id);
|
|
56
|
+
reject(new TransportTimeoutError(method, timeoutMs));
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
// Don't let a pending request keep the event loop alive.
|
|
59
|
+
timer.unref?.();
|
|
60
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
61
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
notify(method, params) {
|
|
65
|
+
if (this.disposed)
|
|
66
|
+
return;
|
|
67
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
68
|
+
}
|
|
69
|
+
onNotification(method, handler) {
|
|
70
|
+
const list = this.notificationHandlers.get(method) ?? [];
|
|
71
|
+
list.push(handler);
|
|
72
|
+
this.notificationHandlers.set(method, list);
|
|
73
|
+
}
|
|
74
|
+
onCrash(handler) {
|
|
75
|
+
this.crashHandler = handler;
|
|
76
|
+
}
|
|
77
|
+
async dispose(force = false) {
|
|
78
|
+
if (this.disposed)
|
|
79
|
+
return;
|
|
80
|
+
this.disposed = true;
|
|
81
|
+
this.unregisterCleanup();
|
|
82
|
+
// Reject anything still in flight so callers never hang on teardown.
|
|
83
|
+
for (const [, p] of this.pending) {
|
|
84
|
+
clearTimeout(p.timer);
|
|
85
|
+
p.reject(new Error("transport disposed"));
|
|
86
|
+
}
|
|
87
|
+
this.pending.clear();
|
|
88
|
+
if (!this.child.pid || this.child.exitCode !== null)
|
|
89
|
+
return;
|
|
90
|
+
if (force) {
|
|
91
|
+
killTree(this.child.pid);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Give the process a grace window to exit on its own (the caller has
|
|
95
|
+
// already sent the LSP `exit` notification), then force-kill the group.
|
|
96
|
+
await new Promise((resolve) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
killTree(this.child.pid);
|
|
99
|
+
resolve();
|
|
100
|
+
}, GRACE_MS);
|
|
101
|
+
timer.unref?.();
|
|
102
|
+
this.child.once("exit", () => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// ── framing ─────────────────────────────────────────────────────────────────
|
|
109
|
+
send(message) {
|
|
110
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
111
|
+
const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii");
|
|
112
|
+
this.child.stdin?.write(Buffer.concat([header, body]));
|
|
113
|
+
}
|
|
114
|
+
onStdout(chunk) {
|
|
115
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
116
|
+
// Drain every fully-received message currently in the buffer.
|
|
117
|
+
for (;;) {
|
|
118
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
119
|
+
if (headerEnd === -1)
|
|
120
|
+
return;
|
|
121
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
122
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
123
|
+
if (!match) {
|
|
124
|
+
// Malformed header — drop it and resync past the separator.
|
|
125
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const length = Number(match[1]);
|
|
129
|
+
const bodyStart = headerEnd + 4;
|
|
130
|
+
if (this.buffer.length < bodyStart + length)
|
|
131
|
+
return; // body not complete yet
|
|
132
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length);
|
|
133
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
134
|
+
this.dispatch(body.toString("utf8"));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
dispatch(text) {
|
|
138
|
+
let msg;
|
|
139
|
+
try {
|
|
140
|
+
msg = JSON.parse(text);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return; // ignore unparseable frames
|
|
144
|
+
}
|
|
145
|
+
// Response to one of our requests.
|
|
146
|
+
if (typeof msg.id === "number" && (msg.result !== undefined || msg.error)) {
|
|
147
|
+
const pending = this.pending.get(msg.id);
|
|
148
|
+
if (!pending)
|
|
149
|
+
return;
|
|
150
|
+
this.pending.delete(msg.id);
|
|
151
|
+
clearTimeout(pending.timer);
|
|
152
|
+
if (msg.error)
|
|
153
|
+
pending.reject(new Error(msg.error.message ?? "LSP error"));
|
|
154
|
+
else
|
|
155
|
+
pending.resolve(msg.result);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Server → client notification (e.g. textDocument/publishDiagnostics).
|
|
159
|
+
if (typeof msg.method === "string" && msg.id === undefined) {
|
|
160
|
+
for (const handler of this.notificationHandlers.get(msg.method) ?? []) {
|
|
161
|
+
handler(msg.params);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Server → client requests (e.g. workspace/configuration) are unsupported;
|
|
165
|
+
// we intentionally don't reply — our advertised capabilities don't ask for
|
|
166
|
+
// features that require a mandatory response.
|
|
167
|
+
}
|
|
168
|
+
onExit(code, signal) {
|
|
169
|
+
this.unregisterCleanup();
|
|
170
|
+
if (this.disposed)
|
|
171
|
+
return; // an expected shutdown, not a crash
|
|
172
|
+
// Unexpected exit → a crash. The server's OWN child processes (e.g. gopls's
|
|
173
|
+
// `go`, rust-analyzer's `cargo`) can outlive it — reap the whole group so a
|
|
174
|
+
// crash never orphans them. The group persists as long as any member lives,
|
|
175
|
+
// so the negative-pid signal still lands even though the leader is gone.
|
|
176
|
+
killTree(this.child.pid);
|
|
177
|
+
// Reject everything in flight, then notify.
|
|
178
|
+
for (const [, p] of this.pending) {
|
|
179
|
+
clearTimeout(p.timer);
|
|
180
|
+
p.reject(new Error(`language server exited (code ${code}, signal ${signal})`));
|
|
181
|
+
}
|
|
182
|
+
this.pending.clear();
|
|
183
|
+
this.crashHandler?.({ code, signal });
|
|
184
|
+
}
|
|
185
|
+
onSpawnError(err) {
|
|
186
|
+
// The binary vanished between the presence check and spawn, or is not
|
|
187
|
+
// executable. Treat as a crash so the pool surfaces a coded error.
|
|
188
|
+
if (this.disposed)
|
|
189
|
+
return;
|
|
190
|
+
for (const [, p] of this.pending) {
|
|
191
|
+
clearTimeout(p.timer);
|
|
192
|
+
p.reject(err);
|
|
193
|
+
}
|
|
194
|
+
this.pending.clear();
|
|
195
|
+
this.crashHandler?.({ code: null, signal: null });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** A per-request timeout, distinguishable from an ordinary LSP error response. */
|
|
199
|
+
export class TransportTimeoutError extends Error {
|
|
200
|
+
method;
|
|
201
|
+
timeoutMs;
|
|
202
|
+
constructor(method, timeoutMs) {
|
|
203
|
+
super(`LSP request "${method}" timed out after ${timeoutMs}ms`);
|
|
204
|
+
this.method = method;
|
|
205
|
+
this.timeoutMs = timeoutMs;
|
|
206
|
+
this.name = "TransportTimeoutError";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { LspConfig } from "../config/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Per-language LSP integration (C.12) — the shared vocabulary. Results here are
|
|
4
|
+
* NORMALIZED (1-based lines/columns, project-relative paths, plain severities):
|
|
5
|
+
* the LSP wire format's 0-based positions and `file://` URIs are converted at
|
|
6
|
+
* the edge (`server.ts`) so nothing downstream speaks raw LSP.
|
|
7
|
+
*/
|
|
8
|
+
export type { LspConfig };
|
|
9
|
+
/**
|
|
10
|
+
* A source range, normalized to 1-based lines and columns (LSP is 0-based; we
|
|
11
|
+
* add 1 at the boundary). `path` is project-relative for display.
|
|
12
|
+
*/
|
|
13
|
+
export interface LspLocation {
|
|
14
|
+
/** Project-relative file path. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** 1-based start line. */
|
|
17
|
+
startLine: number;
|
|
18
|
+
/** 1-based start column. */
|
|
19
|
+
startCol: number;
|
|
20
|
+
/** 1-based end line. */
|
|
21
|
+
endLine: number;
|
|
22
|
+
/** 1-based end column. */
|
|
23
|
+
endCol: number;
|
|
24
|
+
}
|
|
25
|
+
/** A normalized diagnostic (severity mapped off the LSP 1–4 integer scale). */
|
|
26
|
+
export interface LspDiagnostic {
|
|
27
|
+
/** Project-relative file path. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Where the diagnostic applies (1-based). */
|
|
30
|
+
range: LspLocation;
|
|
31
|
+
severity: "error" | "warning" | "info" | "hint";
|
|
32
|
+
message: string;
|
|
33
|
+
/** The producing tool, when the server reports it (e.g. "tsc", "gopls"). */
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
/** A normalized hover result — markdown/markup flattened to plain text. */
|
|
37
|
+
export interface LspHover {
|
|
38
|
+
contents: string;
|
|
39
|
+
/** The symbol range the hover describes, when the server reports it. */
|
|
40
|
+
range?: LspLocation;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A single, initialized language server (≈ one live child process). Queries are
|
|
44
|
+
* absolute-path in / normalized-out; the client relativizes paths against the
|
|
45
|
+
* workspace root. A method resolving to `[]`/`null` is an honest "no result" —
|
|
46
|
+
* unavailability is signalled by a thrown coded error from the pool, never here.
|
|
47
|
+
*/
|
|
48
|
+
export interface LanguageServer {
|
|
49
|
+
readonly language: string;
|
|
50
|
+
/** True until the process exits (crash detection flips this to false). */
|
|
51
|
+
readonly alive: boolean;
|
|
52
|
+
definition(file: string, line: number, col: number): Promise<LspLocation[]>;
|
|
53
|
+
references(file: string, line: number, col: number): Promise<LspLocation[]>;
|
|
54
|
+
diagnostics(file: string): Promise<LspDiagnostic[]>;
|
|
55
|
+
hover(file: string, line: number, col: number): Promise<LspHover | null>;
|
|
56
|
+
/** LSP `shutdown` → `exit`, then force-kill the process group after a grace. */
|
|
57
|
+
shutdown(force?: boolean): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/** How to launch one language's server, resolved from config ∪ the default table. */
|
|
60
|
+
export interface ServerSpec {
|
|
61
|
+
language: string;
|
|
62
|
+
/** Executable name or absolute path. */
|
|
63
|
+
command: string;
|
|
64
|
+
/** Argument vector (e.g. `["--stdio"]`). */
|
|
65
|
+
args: string[];
|
|
66
|
+
/** Concrete install instruction, surfaced when the binary is missing. */
|
|
67
|
+
installHint?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The registry's answer for a language. Availability is EXPLICIT: an
|
|
71
|
+
* unavailable server carries the reason so the caller raises a coded,
|
|
72
|
+
* actionable error rather than silently returning nothing.
|
|
73
|
+
*/
|
|
74
|
+
export type ServerResolution = {
|
|
75
|
+
available: true;
|
|
76
|
+
spec: ServerSpec;
|
|
77
|
+
} | {
|
|
78
|
+
available: false;
|
|
79
|
+
language: string;
|
|
80
|
+
spec: ServerSpec | null;
|
|
81
|
+
reason: "no-spec" | "binary-missing";
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The one seam tests replace: a live JSON-RPC peer over the server's stdio. A
|
|
85
|
+
* fake implementation lets the whole stack run with NO real language-server
|
|
86
|
+
* binary (mirroring `RecordingRuntime` for the sandbox).
|
|
87
|
+
*/
|
|
88
|
+
export interface LspTransport {
|
|
89
|
+
/** Send a request and await its response, rejecting after `timeoutMs`. */
|
|
90
|
+
request(method: string, params: unknown, timeoutMs: number): Promise<unknown>;
|
|
91
|
+
/** Fire-and-forget notification (no response expected). */
|
|
92
|
+
notify(method: string, params: unknown): void;
|
|
93
|
+
/** Register a handler for a server→client notification (e.g. diagnostics). */
|
|
94
|
+
onNotification(method: string, handler: (params: unknown) => void): void;
|
|
95
|
+
/** Register a one-shot crash callback (process exited unexpectedly). */
|
|
96
|
+
onCrash(handler: (info: {
|
|
97
|
+
code: number | null;
|
|
98
|
+
signal: string | null;
|
|
99
|
+
}) => void): void;
|
|
100
|
+
/** LSP graceful path already sent by the caller; drop the transport,
|
|
101
|
+
* force-killing the process group after the grace when `force` is set. */
|
|
102
|
+
dispose(force?: boolean): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
/** Builds a transport for a resolved spec, rooted at `root`. Injectable in tests. */
|
|
105
|
+
export type TransportFactory = (spec: ServerSpec, root: string) => LspTransport;
|
|
106
|
+
/** Predicate: is `command` runnable (on PATH, or an existing executable path)? */
|
|
107
|
+
export type BinaryPresent = (command: string) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Tool } from "../tools/types.js";
|
|
2
|
+
import { type McpBounds } from "./bounds.js";
|
|
3
|
+
import type { McpCallResult, RawMcpTool } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* THE SINGLE SEAM (C.27). `mcpToolsFrom` is the ONE and ONLY place an MCP server
|
|
6
|
+
* becomes an agent {@link Tool}. Nothing else in the codebase constructs an
|
|
7
|
+
* MCP-backed tool, so every security property is enforced here, by construction:
|
|
8
|
+
*
|
|
9
|
+
* - GATE. Every produced tool's `execute` calls `ctx.requestApproval({kind:"mcp",
|
|
10
|
+
* …})` BEFORE it ever calls the server. A rejection returns the feedback as the
|
|
11
|
+
* tool result and `tools/call` never fires. The action is classified
|
|
12
|
+
* `destructive` and the classifier never reads the server's `readOnlyHint`
|
|
13
|
+
* (which this adapter deliberately does not even forward) — a server cannot
|
|
14
|
+
* self-declare its tool safe. A session grant is keyed on the exact server+tool
|
|
15
|
+
* pair, so approving one tool never covers another.
|
|
16
|
+
* - DEMARCATE + GAG. Both the advertised description and every result are wrapped
|
|
17
|
+
* as untrusted external data with upstream model names scrubbed
|
|
18
|
+
* ({@link demarcateDescription} / {@link demarcateResult}) — the model never
|
|
19
|
+
* sees a raw, un-boxed description or result.
|
|
20
|
+
* - BOUNDS. The server's tool list is capped in count and per-tool size
|
|
21
|
+
* ({@link boundToolList}); overflow is truncated with a visible note, never a
|
|
22
|
+
* silent drop or an unbounded context blow-up.
|
|
23
|
+
* - NON-PERSISTENCE. `execute` returns a plain {@link ToolResult}; it writes to
|
|
24
|
+
* no store. Results live only in the in-memory conversation (asserted by test).
|
|
25
|
+
*
|
|
26
|
+
* Trust is enforced UPSTREAM (the service never calls this until the server is
|
|
27
|
+
* trusted), so reaching this function already means "the user accepted running
|
|
28
|
+
* this server's code unsandboxed".
|
|
29
|
+
*/
|
|
30
|
+
export interface McpToolSource {
|
|
31
|
+
/** The configured server id (the tool-name prefix and gate key). */
|
|
32
|
+
server: string;
|
|
33
|
+
/** Tools exactly as the server advertised them (untrusted). */
|
|
34
|
+
tools: readonly RawMcpTool[];
|
|
35
|
+
/** Invoke a tool on the server by its ORIGINAL name. Adapter-internal only. */
|
|
36
|
+
call(toolName: string, args: unknown): Promise<McpCallResult>;
|
|
37
|
+
/** Size/count caps for the advertised list. */
|
|
38
|
+
bounds: McpBounds;
|
|
39
|
+
/** Where the visible "N tools dropped" note is surfaced. */
|
|
40
|
+
logger?: {
|
|
41
|
+
warn(message: string): void;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function mcpToolsFrom(source: McpToolSource): Tool[];
|