(
harness: KimiHarness,
opts?: {
input?: NodeJS.ReadableStream;
output?: NodeJS.WritableStream;
/**
* Optional agent identity metadata advertised in the `initialize`
* response (`InitializeResponse.agentInfo`). When omitted, the
* field is left out of the response rather than serialized as
* `null`, matching the kimi-cli reference implementation.
*/
agentInfo?: Implementation;
/**
* Env vars to forward to the `kimi login` subprocess clients spawn
* via `terminal-auth`. See {@link AcpServer} ctor for the use case.
*/
terminalAuthEnv?: Readonly<Record<string, string>>;
/**
* Absolute path to the agent binary, advertised in the legacy
* `_meta['terminal-auth'].command` fallback. See {@link AcpServer}
* ctor for compatibility rationale.
*/
terminalAuthLegacyCommand?: string;
/**
* Slash commands to advertise to ACP clients so their slash-command
* palette is populated. See {@link AcpServer} ctor for details.
*/
slashCommands?: SlashCommandsResolver;
/**
* @internal Test seam — supply a fake `EventEmitter` (or a
* subset that exposes `.once` / `.off`) to drive SIGINT / SIGTERM
* without touching the real `process` listener set. Defaults to
* `process` in production.
*/
signals?: Pick<NodeJS.EventEmitter, 'once' | 'off'>;
},
)
| 965 | * handlers (which vitest itself relies on). |
| 966 | */ |
| 967 | export async function runAcpServer( |
| 968 | harness: KimiHarness, |
| 969 | opts?: { |
| 970 | input?: NodeJS.ReadableStream; |
| 971 | output?: NodeJS.WritableStream; |
| 972 | /** |
| 973 | * Optional agent identity metadata advertised in the `initialize` |
| 974 | * response (`InitializeResponse.agentInfo`). When omitted, the |
| 975 | * field is left out of the response rather than serialized as |
| 976 | * `null`, matching the kimi-cli reference implementation. |
| 977 | */ |
| 978 | agentInfo?: Implementation; |
| 979 | /** |
| 980 | * Env vars to forward to the `kimi login` subprocess clients spawn |
| 981 | * via `terminal-auth`. See {@link AcpServer} ctor for the use case. |
| 982 | */ |
| 983 | terminalAuthEnv?: Readonly<Record<string, string>>; |
| 984 | /** |
| 985 | * Absolute path to the agent binary, advertised in the legacy |
| 986 | * `_meta['terminal-auth'].command` fallback. See {@link AcpServer} |
| 987 | * ctor for compatibility rationale. |
| 988 | */ |
| 989 | terminalAuthLegacyCommand?: string; |
| 990 | /** |
| 991 | * Slash commands to advertise to ACP clients so their slash-command |
| 992 | * palette is populated. See {@link AcpServer} ctor for details. |
| 993 | */ |
| 994 | slashCommands?: SlashCommandsResolver; |
| 995 | /** |
| 996 | * @internal Test seam — supply a fake `EventEmitter` (or a |
| 997 | * subset that exposes `.once` / `.off`) to drive SIGINT / SIGTERM |
| 998 | * without touching the real `process` listener set. Defaults to |
| 999 | * `process` in production. |
| 1000 | */ |
| 1001 | signals?: Pick<NodeJS.EventEmitter, 'once' | 'off'>; |
| 1002 | }, |
| 1003 | ): Promise<void> { |
| 1004 | // Stdout is the JSON-RPC channel; protect it before anything else |
| 1005 | // (a dependency, harness, etc.) can emit non-JSON via console.log. |
| 1006 | redirectConsoleToStderr(); |
| 1007 | const input = (opts?.input ?? process.stdin) as Readable; |
| 1008 | const output = (opts?.output ?? process.stdout) as Writable; |
| 1009 | const stream = ndJsonStream(Writable.toWeb(output), Readable.toWeb(input)); |
| 1010 | const signals = opts?.signals ?? process; |
| 1011 | |
| 1012 | let cleanedUp = false; |
| 1013 | const cleanup = async (signal?: NodeJS.Signals): Promise<void> => { |
| 1014 | // Idempotent: signal-then-natural-close (or vice-versa) must not |
| 1015 | // call `harness.close()` twice. `cleanedUp` is checked-and-set |
| 1016 | // synchronously so concurrent invocations cannot race. |
| 1017 | if (cleanedUp) return; |
| 1018 | cleanedUp = true; |
| 1019 | if (signal) { |
| 1020 | log.info('acp: received signal, draining harness', { signal }); |
| 1021 | } |
| 1022 | try { |
| 1023 | await harness.close(); |
| 1024 | } catch (err) { |
no test coverage detected