feat(setup): add Kimi Code client support (#2744)
* feat(setup): add Kimi Code client support Registers rea in ~/.kimi-code/mcp.json (or $KIMI_CODE_HOME/mcp.json), preserving unrelated settings with a .rea.backup; registration is command-based stdio with no type field, startupTimeoutMs 30000 and toolTimeoutMs 120000 from MCP_STARTUP_POLICY. Strict JSON parsing: empty or whitespace-only files initialize; malformed JSON is rejected untouched with no backup; explicit transport other than stdio reports invalid. Bundled skill uses the shared ~/.agents/skills destination that Kimi Code discovers, installed and verified through the shared-skill flow. Validated by 19 Kimi-specific tests, 260 adjacent-client regressions passing, check:fast and docs:check, and real-client verification on Kimi Code 0.40.1 (173 tools discovered, real mcp__rea__analyze_javascript_application call succeeded). * fix(setup): preserve Kimi tool filters and strict JSON coverage --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
b19f9e4d2d
commit
efc491ab45
|
|
@ -129,6 +129,18 @@ and Claude Code reports that autocompact is thrashing. To avoid this:
|
|||
The sizes above come from REA's own `tools/list`. A live Claude Code session
|
||||
against DeepSeek's API is unverified.
|
||||
|
||||
## Kimi Code
|
||||
|
||||
Setup registers `rea` in `~/.kimi-code/mcp.json`, or
|
||||
`$KIMI_CODE_HOME/mcp.json` when set, while preserving unrelated settings and
|
||||
creating a `.rea.backup` before updating an existing file. Kimi Code infers
|
||||
stdio from the `command` entry, so setup does not add a `type` field. The
|
||||
registration includes `startupTimeoutMs = 30000` and `toolTimeoutMs = 120000`.
|
||||
Kimi Code also discovers personal skills in `~/.agents/skills` and project-local
|
||||
`.agents/skills`, so no separate skill root is required; setup still installs
|
||||
and verifies the matching skill files through the normal shared-skill flow.
|
||||
Start a new Kimi Code session to pick up MCP and skill changes.
|
||||
|
||||
## Pi
|
||||
|
||||
For [Pi](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md),
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
|||
| GitHub Copilot CLI | `copilot_cli` |
|
||||
| Command Code | `commandcode` |
|
||||
| Qwen Code | `qwen_code` |
|
||||
| Kimi Code | `kimi` |
|
||||
| VS Code | `vscode` |
|
||||
| Grok Build | `grok_build` |
|
||||
| OMP | `omp` |
|
||||
|
|
@ -290,8 +291,8 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
|||
| ZCode | `zcode` |
|
||||
| DeepSeek Harness | `dsh` |
|
||||
|
||||
`dsh` is main-only until a release after 6.4.0. The registry check above does
|
||||
not cover it.
|
||||
`dsh` and `kimi` require a release containing them. The registry check above does
|
||||
not cover them.
|
||||
|
||||
See [client configuration and troubleshooting](client-configuration.md) for
|
||||
configuration paths, profiles, trust policies, and schema-limit workarounds.
|
||||
|
|
|
|||
|
|
@ -110,9 +110,12 @@ const parseDocument = (
|
|||
return requireObject(parseToml(text), "The configuration root");
|
||||
// Pi loads `mcp.json` with a strict `JSON.parse` and no BOM tolerance, so
|
||||
// comments, trailing commas, a BOM, or an empty existing file must be
|
||||
// rejected instead of registering into a document Pi cannot load.
|
||||
if (format === "pi")
|
||||
// rejected instead of registering into a document the client cannot load.
|
||||
// Kimi also reads mcp.json with JSON.parse and an empty-file fallback.
|
||||
if (format === "pi" || format === "kimi") {
|
||||
if (format === "kimi" && text.trim().length === 0) return {};
|
||||
return requireObject(JSON.parse(text), "The configuration root");
|
||||
}
|
||||
// An empty file, which some clients create before any server is added,
|
||||
// holds no settings to preserve.
|
||||
if (isEmptyClientConfigurationText(text)) return {};
|
||||
|
|
@ -1446,6 +1449,13 @@ export const clientRegistrationEntry = (
|
|||
? {}
|
||||
: { environment: { ...environment } }),
|
||||
};
|
||||
case "kimi":
|
||||
// Kimi infers stdio from command; it uses `transport`, not `type`.
|
||||
return {
|
||||
command: executable,
|
||||
args,
|
||||
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
|
||||
};
|
||||
case "opencode_v2":
|
||||
// V2 connects servers unless `disabled` is true.
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ const registrationSchema = z
|
|||
command: z.string().min(1),
|
||||
args: z.array(z.string()).default([]),
|
||||
startup_timeout_sec: z.number().positive().optional(),
|
||||
startupTimeoutMs: z.number().positive().optional(),
|
||||
toolTimeoutMs: z.number().positive().optional(),
|
||||
type: z.string().optional(),
|
||||
transport: z.string().optional(),
|
||||
serverName: z.string().optional(),
|
||||
|
|
@ -258,6 +260,21 @@ const registrationAligned = (
|
|||
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
client.name === "kimi" &&
|
||||
(registration.startupTimeoutMs !==
|
||||
MCP_STARTUP_POLICY.kimiStartupTimeoutMs ||
|
||||
registration.toolTimeoutMs !== MCP_STARTUP_POLICY.kimiToolTimeoutMs)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(client.format === "kimi" ||
|
||||
client.format === "omp" ||
|
||||
client.format === "pi") &&
|
||||
registration.type !== undefined &&
|
||||
registration.type !== "stdio"
|
||||
)
|
||||
return false;
|
||||
if (client.format === "vscode" && registration.type !== "stdio") return false;
|
||||
// A Harness registration only connects as the rea stdio server; another
|
||||
// server name or transport leaves the session without REA tools.
|
||||
|
|
@ -266,13 +283,6 @@ const registrationAligned = (
|
|||
(registration.serverName !== "rea" || registration.transport !== "stdio")
|
||||
)
|
||||
return false;
|
||||
// OMP and Pi infer stdio for a command entry without an explicit type.
|
||||
if (
|
||||
(client.format === "omp" || client.format === "pi") &&
|
||||
registration.type !== undefined &&
|
||||
registration.type !== "stdio"
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
client.format === "copilot_cli" &&
|
||||
(registration.type !== "stdio" ||
|
||||
|
|
@ -331,6 +341,12 @@ const parseRegistration = (
|
|||
});
|
||||
}
|
||||
const registration = registrationSchema.parse(value);
|
||||
if (
|
||||
client.format === "kimi" &&
|
||||
registration.transport !== undefined &&
|
||||
registration.transport !== "stdio"
|
||||
)
|
||||
throw new TypeError("Expected an stdio registration");
|
||||
if (
|
||||
(client.format === "vscode" || client.format === "copilot_cli") &&
|
||||
registration.type !== "stdio"
|
||||
|
|
@ -339,7 +355,9 @@ const parseRegistration = (
|
|||
if (client.format === "commandcode" && registration.transport !== "stdio")
|
||||
throw new TypeError("Expected an stdio registration");
|
||||
if (
|
||||
(client.format === "omp" || client.format === "pi") &&
|
||||
(client.format === "kimi" ||
|
||||
client.format === "omp" ||
|
||||
client.format === "pi") &&
|
||||
registration.type !== undefined &&
|
||||
registration.type !== "stdio"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -704,6 +704,14 @@ const clientConfigurationDesired = (
|
|||
launcher,
|
||||
environment.value,
|
||||
);
|
||||
const previous = parsed?.servers[PRODUCT_IDENTITY.mcpServerKey];
|
||||
if (
|
||||
client.name === "kimi" &&
|
||||
typeof previous === "object" &&
|
||||
previous !== null &&
|
||||
"enabledTools" in previous
|
||||
)
|
||||
registration.enabledTools = previous.enabledTools;
|
||||
return {
|
||||
...registration,
|
||||
...(client.name === "codex" || client.name === "grok_build"
|
||||
|
|
@ -711,5 +719,11 @@ const clientConfigurationDesired = (
|
|||
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
|
||||
}
|
||||
: {}),
|
||||
...(client.name === "kimi"
|
||||
? {
|
||||
startupTimeoutMs: MCP_STARTUP_POLICY.kimiStartupTimeoutMs,
|
||||
toolTimeoutMs: MCP_STARTUP_POLICY.kimiToolTimeoutMs,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface SetupClient {
|
|||
| "grok"
|
||||
| "omp"
|
||||
| "pi"
|
||||
| "kimi"
|
||||
| "hermes"
|
||||
| "zcode"
|
||||
| "dsh"
|
||||
|
|
@ -165,6 +166,10 @@ const claudeCodeMarkerDirectory = ({ home, env }: ClientPathContext): string =>
|
|||
const codexDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.CODEX_HOME || join(home, ".codex");
|
||||
|
||||
/** Kimi Code stores MCP declarations beside the rest of its home data. */
|
||||
const kimiDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.KIMI_CODE_HOME || join(home, ".kimi-code");
|
||||
|
||||
const qoderDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.QODER_CONFIG_DIR || join(home, ".qoder");
|
||||
|
||||
|
|
@ -576,6 +581,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
|
|||
markerPath: qwenCodeDirectory,
|
||||
format: "json",
|
||||
},
|
||||
{
|
||||
name: "kimi",
|
||||
displayName: "Kimi Code",
|
||||
configPath: (context: ClientPathContext) =>
|
||||
join(kimiDirectory(context), "mcp.json"),
|
||||
markerPath: kimiDirectory,
|
||||
format: "kimi",
|
||||
},
|
||||
{
|
||||
name: "vscode",
|
||||
displayName: "VS Code",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
export const MCP_STARTUP_POLICY = {
|
||||
codexStartupTimeoutSeconds: 30,
|
||||
kimiStartupTimeoutMs: 30_000,
|
||||
kimiToolTimeoutMs: 120_000,
|
||||
initializeBudgetMs: 15_000,
|
||||
toolsListBudgetMs: 5_000,
|
||||
firstCatalogBudgetMs: 20_000,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ describe("client configuration filesystem lifecycle", () => {
|
|||
"copilot_cli",
|
||||
"commandcode",
|
||||
"qwen_code",
|
||||
"kimi",
|
||||
"vscode",
|
||||
"trae",
|
||||
"grok_build",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
|
||||
import { setupRegistrationCommand } from "../../../src/application/SetupHost.js";
|
||||
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
|
||||
import {
|
||||
clientSkillDirectories,
|
||||
supportedClients,
|
||||
} from "../../../src/application/SupportedClients.js";
|
||||
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
|
||||
import { MCP_STARTUP_POLICY } from "../../../src/mcpStartupPolicy.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
clearClientLocationEnvironment,
|
||||
NPX_REGISTRATION_COMMAND,
|
||||
} from "../../fixtures/clientEnvironment.js";
|
||||
|
||||
const command = NPX_REGISTRATION_COMMAND;
|
||||
|
||||
beforeEach(clearClientLocationEnvironment);
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
const kimiPath = (
|
||||
home: string,
|
||||
platform: NodeJS.Platform,
|
||||
env: Parameters<typeof supportedClients>[2] = {},
|
||||
) => supportedClients(home, platform, env).find(({ name }) => name === "kimi");
|
||||
|
||||
const exists = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const expectedRegistration = (launcher: readonly string[]) => ({
|
||||
command: launcher[0],
|
||||
args: launcher.slice(1),
|
||||
startupTimeoutMs: MCP_STARTUP_POLICY.kimiStartupTimeoutMs,
|
||||
toolTimeoutMs: MCP_STARTUP_POLICY.kimiToolTimeoutMs,
|
||||
});
|
||||
|
||||
describe("Kimi Code configuration paths and skills", () => {
|
||||
it.each([
|
||||
[undefined, join("/home/a/.kimi-code")],
|
||||
["", join("/home/a/.kimi-code")],
|
||||
[join("/tmp", "isolated-kimi"), join("/tmp", "isolated-kimi")],
|
||||
])("resolves KIMI_CODE_HOME for %j", (override, directory) => {
|
||||
const client = kimiPath(
|
||||
"/home/a",
|
||||
"linux",
|
||||
override === undefined ? {} : { KIMI_CODE_HOME: override },
|
||||
);
|
||||
expect(client).toEqual({
|
||||
name: "kimi",
|
||||
displayName: "Kimi Code",
|
||||
configPath: join(directory, "mcp.json"),
|
||||
markerPath: directory,
|
||||
format: "kimi",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the existing shared skill destination without a separate skill root", () => {
|
||||
expect(clientSkillDirectories("/home/a", ["kimi"])).toEqual([
|
||||
{ client: "shared", directory: "/home/a/.agents/skills" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kimi Code registration lifecycle", () => {
|
||||
const kimiClient = async () => {
|
||||
const home = await createTestTempDirectory("rea-kimi-");
|
||||
const client = kimiPath(home, process.platform, {});
|
||||
if (client?.markerPath === undefined)
|
||||
throw new Error("missing Kimi Code client");
|
||||
await mkdir(client.markerPath, { recursive: true });
|
||||
return { home, client };
|
||||
};
|
||||
|
||||
it("preserves the caller's enabledTools selection during refresh", async () => {
|
||||
const { client } = await kimiClient();
|
||||
const enabledTools = ["binary_session", "analyze_javascript_application"];
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: { command: "old-rea", args: ["mcp"], enabledTools },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("configured");
|
||||
expect(
|
||||
JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea,
|
||||
).toEqual({
|
||||
...expectedRegistration(command),
|
||||
enabledTools,
|
||||
});
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("unchanged");
|
||||
});
|
||||
|
||||
it("registers without type, preserves unrelated settings, backs up, and is idempotent", async () => {
|
||||
const { home, client } = await kimiClient();
|
||||
const original = `{
|
||||
"customSetting": { "enabled": true },
|
||||
"mcpServers": {
|
||||
"other": { "command": "other-server", "startupTimeoutMs": 5000 }
|
||||
}
|
||||
}
|
||||
`;
|
||||
await writeFile(client.configPath, original);
|
||||
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "configured",
|
||||
backupPath: `${client.configPath}.rea.backup`,
|
||||
});
|
||||
const text = await readFile(client.configPath, "utf8");
|
||||
expect(JSON.parse(text)).toEqual({
|
||||
customSetting: { enabled: true },
|
||||
mcpServers: {
|
||||
other: { command: "other-server", startupTimeoutMs: 5000 },
|
||||
rea: expectedRegistration(command),
|
||||
},
|
||||
});
|
||||
expect(text).not.toContain('"type"');
|
||||
expect(text).not.toContain('"transport"');
|
||||
expect(await readFile(`${client.configPath}.rea.backup`, "utf8")).toBe(
|
||||
original,
|
||||
);
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(await readFile(client.configPath, "utf8")).toBe(text);
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "aligned", command });
|
||||
|
||||
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
|
||||
"removed",
|
||||
);
|
||||
expect(JSON.parse(await readFile(client.configPath, "utf8"))).toEqual({
|
||||
customSetting: { enabled: true },
|
||||
mcpServers: {
|
||||
other: { command: "other-server", startupTimeoutMs: 5000 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kimi Code package launchers", () => {
|
||||
const kimiClient = async () => {
|
||||
const home = await createTestTempDirectory("rea-kimi-launcher-");
|
||||
const client = kimiPath(home, process.platform, {});
|
||||
if (client?.markerPath === undefined)
|
||||
throw new Error("missing Kimi Code client");
|
||||
await mkdir(client.markerPath, { recursive: true });
|
||||
return { home, client } as const;
|
||||
};
|
||||
|
||||
it.each([
|
||||
["npx", "linux"],
|
||||
["bunx", "linux"],
|
||||
["npx", "win32"],
|
||||
["bunx", "win32"],
|
||||
] as const)(
|
||||
"keeps the %s launcher aligned on %s",
|
||||
async (runner, platform) => {
|
||||
const { home, client } = await kimiClient();
|
||||
const launcher = setupRegistrationCommand(platform, runner);
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, launcher)).status,
|
||||
).toBe("configured");
|
||||
expect(
|
||||
JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea,
|
||||
).toEqual(expectedRegistration(launcher));
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
platform,
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "aligned", command: launcher });
|
||||
expect(await configureClientConfiguration(client, {}, launcher)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Kimi Code transport validation", () => {
|
||||
const kimiClient = async () => {
|
||||
const home = await createTestTempDirectory("rea-kimi-validation-");
|
||||
const client = kimiPath(home, process.platform, {});
|
||||
if (client?.markerPath === undefined)
|
||||
throw new Error("missing Kimi Code client");
|
||||
await mkdir(client.markerPath, { recursive: true });
|
||||
return { home, client };
|
||||
};
|
||||
|
||||
it("creates a registration in a zero-byte file and accepts whitespace-only files", async () => {
|
||||
const { home, client } = await kimiClient();
|
||||
for (const original of ["", " \n\t"]) {
|
||||
await writeFile(client.configPath, original);
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("configured");
|
||||
expect(
|
||||
JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea,
|
||||
).toEqual(expectedRegistration(command));
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(
|
||||
(await systemUninstallHost(home).removeClient(client)).status,
|
||||
).toBe("removed");
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
'{"mcpServers": {',
|
||||
'{/* comment */ "mcpServers": {}}',
|
||||
'{"mcpServers": {},}',
|
||||
'\uFEFF{"mcpServers": {}}',
|
||||
])("rejects invalid strict JSON %j without writes", async (original) => {
|
||||
const { client } = await kimiClient();
|
||||
await writeFile(client.configPath, original);
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "failed",
|
||||
reason: "readback",
|
||||
});
|
||||
expect(await readFile(client.configPath, "utf8")).toBe(original);
|
||||
expect(await exists(`${client.configPath}.rea.backup`)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts explicit stdio transport without causing repeated rewrites", async () => {
|
||||
const { home, client } = await kimiClient();
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
transport: "stdio",
|
||||
...expectedRegistration(command),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "aligned", command });
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, command)).status,
|
||||
).toBe("configured");
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(await configureClientConfiguration(client, {}, command)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "aligned", command });
|
||||
});
|
||||
|
||||
it.each(["sse"])(
|
||||
"rejects explicit transport %j instead of silently accepting another protocol",
|
||||
async (transport) => {
|
||||
const { home, client } = await kimiClient();
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
transport,
|
||||
...expectedRegistration(command),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "invalid", command: [] });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Kimi Code timeout validation", () => {
|
||||
const kimiClient = async () => {
|
||||
const home = await createTestTempDirectory("rea-kimi-timeout-");
|
||||
const client = kimiPath(home, process.platform, {});
|
||||
if (client?.markerPath === undefined)
|
||||
throw new Error("missing Kimi Code client");
|
||||
await mkdir(client.markerPath, { recursive: true });
|
||||
return { home, client } as const;
|
||||
};
|
||||
|
||||
it.each([
|
||||
["startupTimeoutMs", 1],
|
||||
["toolTimeoutMs", 1],
|
||||
["startupTimeoutMs", undefined],
|
||||
["toolTimeoutMs", undefined],
|
||||
] as const)("reports stale when %s is %j", async (field, value) => {
|
||||
const { home, client } = await kimiClient();
|
||||
const registration = {
|
||||
...expectedRegistration(command),
|
||||
[field]: value,
|
||||
};
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({ mcpServers: { rea: registration } }),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "stale" });
|
||||
});
|
||||
|
||||
it("uses a direct installed REA command without requiring a package runner", async () => {
|
||||
const { home, client } = await kimiClient();
|
||||
const entry = join(home, "scripts", "rea.mjs");
|
||||
const launcher = [process.execPath, entry, "mcp"];
|
||||
expect(
|
||||
(await configureClientConfiguration(client, {}, launcher)).status,
|
||||
).toBe("configured");
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, entry, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "aligned", command: launcher });
|
||||
});
|
||||
|
||||
it("leaves a foreign rea-key registration out of aligned state", async () => {
|
||||
const { home, client } = await kimiClient();
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
...expectedRegistration(command),
|
||||
command: "another-package",
|
||||
args: ["mcp"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await readClientRegistrationStatuses(home, undefined, {
|
||||
environment: {},
|
||||
})
|
||||
).find(({ client: name }) => name === "kimi"),
|
||||
).toMatchObject({ state: "stale" });
|
||||
});
|
||||
});
|
||||
|
|
@ -27,6 +27,7 @@ export const CLIENT_LOCATION_ENVIRONMENT = [
|
|||
"GEMINI_CLI_SYSTEM_DEFAULTS_PATH",
|
||||
"HERMES_DATA_DIR_SUFFIX",
|
||||
"HERMES_HOME",
|
||||
"KIMI_CODE_HOME",
|
||||
"LOCALAPPDATA",
|
||||
"OMP_PROFILE",
|
||||
"OPENCODE_CONFIG",
|
||||
|
|
|
|||
Loading…
Reference in New Issue