mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
* feat(setup): support Cline as a client * test(setup): cover Cline doctor and uninstall ownership * docs(setup): document Cline support * test(setup): verify Cline against an installed client * test(setup): include Cline in the exhaustive client detection list * docs(setup): note that a hand-written rea entry blocks Cline setup * fix(setup): isolate Cline verification and await MCP discovery * fix(verification): own and isolate Cline discovery * fix(setup): preserve native Cline transport settings --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
156 lines
5.0 KiB
JavaScript
156 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { mkdir, mkdtemp, readFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { createOwnedMcpStdioTransport } from "./lib/owned-mcp-stdio.mjs";
|
|
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
|
import { createVerifierMcpOwner } from "./lib/verifier-mcp-owner.mjs";
|
|
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
|
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
|
|
|
/** Verify default-path CLI registration, client parsing and owned MCP discovery. */
|
|
const repo = fileURLToPath(new URL("..", import.meta.url));
|
|
const command = process.env.REA_VERIFY_CLINE_COMMAND ?? "cline";
|
|
const reaEntry = join(repo, "scripts/rea.mjs");
|
|
const run = createVerifierRun();
|
|
const lab = await mkdtemp(join(tmpdir(), "rea-cline-client-"));
|
|
const home = join(lab, "home");
|
|
await mkdir(home, { recursive: true });
|
|
const dataDirectory = join(home, ".cline", "data");
|
|
const configPath = join(dataDirectory, "settings", "cline_mcp_settings.json");
|
|
const locationOverrides = [
|
|
"CLINE_MCP_SETTINGS_PATH",
|
|
"CLINE_DATA_DIR",
|
|
"CLINE_DIR",
|
|
];
|
|
const isolatedOverrides = locationOverrides.filter(
|
|
(name) => process.env[name] !== undefined,
|
|
);
|
|
const environment = {
|
|
...process.env,
|
|
HOME: home,
|
|
USERPROFILE: home,
|
|
HOPPER_LAUNCHER_PATH: join(lab, "missing-hopper"),
|
|
GHIDRA_INSTALL_DIR: join(lab, "missing-ghidra"),
|
|
JAVA_HOME: join(lab, "missing-java"),
|
|
};
|
|
// Exercise Cline's default path without letting inherited selectors escape HOME.
|
|
for (const name of locationOverrides) delete environment[name];
|
|
|
|
const commands = await createVerifierCommands();
|
|
const report = await withVerifierCleanup(async () => {
|
|
let clineVersion;
|
|
try {
|
|
const { stdout } = await commands.exec(command, ["--version"], {
|
|
cwd: home,
|
|
env: environment,
|
|
timeout: 60_000,
|
|
});
|
|
clineVersion = stdout.trim();
|
|
} catch (cause) {
|
|
return {
|
|
status: "skipped",
|
|
reason: `Could not verify ${command} --version (${cause.code ?? cause.message}). Set REA_VERIFY_CLINE_COMMAND to an installed Cline CLI.`,
|
|
command,
|
|
};
|
|
}
|
|
|
|
await commands.exec(
|
|
process.execPath,
|
|
[
|
|
reaEntry,
|
|
"setup",
|
|
"--client",
|
|
"cline",
|
|
"--skill=false",
|
|
"--yes",
|
|
"--json",
|
|
],
|
|
{ cwd: home, env: environment, timeout: 180_000 },
|
|
);
|
|
const settings = JSON.parse(await readFile(configPath, "utf8"));
|
|
const registration = settings.mcpServers?.rea;
|
|
assert(
|
|
registration,
|
|
"setup must write mcpServers.rea into Cline's default settings file",
|
|
);
|
|
assert(Array.isArray(registration.args), "the registration must carry args");
|
|
assert.equal(
|
|
registration.args.at(-1),
|
|
"mcp",
|
|
"the registration must launch the MCP entry point",
|
|
);
|
|
|
|
const listed = await commands.exec(
|
|
command,
|
|
["--data-dir", dataDirectory, "config", "mcp"],
|
|
{ cwd: home, env: environment, timeout: 120_000 },
|
|
);
|
|
assert.match(
|
|
listed.stdout,
|
|
/rea \[stdio\]/,
|
|
"Cline must list the REA server REA wrote",
|
|
);
|
|
|
|
const mcp = createVerifierMcpOwner();
|
|
commands.adopt(mcp, "Cline MCP discovery");
|
|
const transport = await createOwnedMcpStdioTransport({
|
|
command: registration.command,
|
|
arguments: registration.args,
|
|
hostEnvironment: { ...environment, ...registration.env },
|
|
cwd: home,
|
|
expectedCommand: null,
|
|
sweepTokenOwnedProcesses: true,
|
|
});
|
|
const client = new Client({ name: "rea-cline-verifier", version: "1" });
|
|
// The SDK keeps stdin open through delayed initialization and tool discovery.
|
|
const connection = await mcp.open(client, transport);
|
|
assert.equal(
|
|
client.getServerVersion()?.name,
|
|
"rea",
|
|
"initialize must identify rea",
|
|
);
|
|
assert(
|
|
connection.definitions.size > 0,
|
|
"the registered server must expose tools",
|
|
);
|
|
|
|
return {
|
|
status: "verified",
|
|
client: "cline",
|
|
client_version: clineVersion,
|
|
configuration_path: configPath,
|
|
registration_command: registration.command,
|
|
registration_arguments: registration.args,
|
|
tool_count: connection.definitions.size,
|
|
covered: [
|
|
"rea setup writes the default Cline registration without path overrides",
|
|
"Cline lists the REA server from that file",
|
|
"the registered command completes owned MCP discovery and cleanup",
|
|
],
|
|
unverified: [
|
|
"Cline's independent default-home discovery (listing selects --data-dir)",
|
|
"a model-driven tool call inside Cline (providers.json is versioned and auth-shaped)",
|
|
"the IDE extension, whose settings are UI-managed",
|
|
],
|
|
isolated_overrides: isolatedOverrides,
|
|
};
|
|
}, [() => commands.close()]);
|
|
|
|
// Publish lineage only after every command and the MCP owner have finished cleanup.
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
...report,
|
|
process_run:
|
|
report.status === "verified" ? await completeVerifierRun(run) : run,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
if (report.status === "skipped") process.exitCode = 2;
|