@nyxa/automation 0.1.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.
Files changed (48) hide show
  1. package/README.md +559 -0
  2. package/dist/cancellation.d.ts +1 -0
  3. package/dist/cancellation.js +5 -0
  4. package/dist/claude-code.d.ts +19 -0
  5. package/dist/claude-code.js +182 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +487 -0
  8. package/dist/codex-output.d.ts +6 -0
  9. package/dist/codex-output.js +167 -0
  10. package/dist/codex.d.ts +17 -0
  11. package/dist/codex.js +228 -0
  12. package/dist/doctor.d.ts +17 -0
  13. package/dist/doctor.js +138 -0
  14. package/dist/errors.d.ts +30 -0
  15. package/dist/errors.js +51 -0
  16. package/dist/events.d.ts +55 -0
  17. package/dist/events.js +51 -0
  18. package/dist/filesystem.d.ts +2 -0
  19. package/dist/filesystem.js +13 -0
  20. package/dist/harness-process.d.ts +39 -0
  21. package/dist/harness-process.js +359 -0
  22. package/dist/harness-prompt.d.ts +2 -0
  23. package/dist/harness-prompt.js +14 -0
  24. package/dist/harness.d.ts +44 -0
  25. package/dist/harness.js +68 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +9 -0
  28. package/dist/init.d.ts +1 -0
  29. package/dist/init.js +305 -0
  30. package/dist/json.d.ts +4 -0
  31. package/dist/json.js +1 -0
  32. package/dist/kimi-code.d.ts +16 -0
  33. package/dist/kimi-code.js +299 -0
  34. package/dist/opencode.d.ts +15 -0
  35. package/dist/opencode.js +164 -0
  36. package/dist/run-coordination.d.ts +10 -0
  37. package/dist/run-coordination.js +96 -0
  38. package/dist/run-journal.d.ts +14 -0
  39. package/dist/run-journal.js +129 -0
  40. package/dist/schema.d.ts +167 -0
  41. package/dist/schema.js +516 -0
  42. package/dist/standard-input.d.ts +1 -0
  43. package/dist/standard-input.js +7 -0
  44. package/dist/type-utils.d.ts +1 -0
  45. package/dist/type-utils.js +1 -0
  46. package/dist/workflow.d.ts +160 -0
  47. package/dist/workflow.js +530 -0
  48. package/package.json +51 -0
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function initializeProject(arguments_: readonly string[], invocationDirectory?: string): Promise<string>;
package/dist/init.js ADDED
@@ -0,0 +1,305 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { diagnoseEnvironment } from "./doctor.js";
4
+ import { isMissingFileError } from "./filesystem.js";
5
+ import { readStandardInput } from "./standard-input.js";
6
+ export async function initializeProject(arguments_, invocationDirectory = process.cwd()) {
7
+ const parsed = parseInitArguments(arguments_);
8
+ const prompt = !parsed.yes && (parsed.harness === undefined || parsed.install)
9
+ ? await createPrompt()
10
+ : undefined;
11
+ const manifestPath = path.join(invocationDirectory, "package.json");
12
+ try {
13
+ try {
14
+ JSON.parse(await readFile(manifestPath, "utf8"));
15
+ }
16
+ catch (error) {
17
+ if (isMissingFileError(error)) {
18
+ throw new Error("init requires a package.json from an existing npm project.");
19
+ }
20
+ throw error;
21
+ }
22
+ const harness = await resolveHarness(parsed, invocationDirectory, prompt);
23
+ const install = await resolveInstall(parsed, prompt);
24
+ const workflowPath = path.resolve(invocationDirectory, parsed.workflowPath);
25
+ if (await pathExists(workflowPath)) {
26
+ throw workflowExistsError(parsed.workflowPath);
27
+ }
28
+ const packageManager = install
29
+ ? await detectPackageManager(invocationDirectory)
30
+ : undefined;
31
+ if (packageManager !== undefined) {
32
+ await installAutomationDependency(packageManager, invocationDirectory);
33
+ }
34
+ await mkdir(path.dirname(workflowPath), { recursive: true });
35
+ try {
36
+ await writeFile(workflowPath, workflowExample(harness), {
37
+ encoding: "utf8",
38
+ flag: "wx",
39
+ });
40
+ }
41
+ catch (error) {
42
+ if (isExistingFileError(error)) {
43
+ throw workflowExistsError(parsed.workflowPath);
44
+ }
45
+ throw error;
46
+ }
47
+ let output = `Created ${parsed.workflowPath}.\n`;
48
+ if (packageManager !== undefined) {
49
+ output += `Installed @nyxa/automation with ${packageManager.displayName}.\n`;
50
+ }
51
+ return output;
52
+ }
53
+ finally {
54
+ prompt?.close();
55
+ }
56
+ }
57
+ async function resolveHarness(arguments_, invocationDirectory, prompt) {
58
+ if (arguments_.harness !== undefined) {
59
+ return arguments_.harness;
60
+ }
61
+ if (!arguments_.yes) {
62
+ const choice = await requirePrompt(prompt).question("Choose a Harness:\n 1) Codex\n 2) Claude Code\n 3) Kimi Code\n 4) OpenCode\nHarness [1/2/3/4]: ");
63
+ if (choice === "1" || choice === "codex") {
64
+ return "codex";
65
+ }
66
+ if (choice === "2" || choice === "claude-code") {
67
+ return "claude-code";
68
+ }
69
+ if (choice === "3" || choice === "kimi-code") {
70
+ return "kimi-code";
71
+ }
72
+ if (choice === "4" || choice === "opencode") {
73
+ return "opencode";
74
+ }
75
+ throw new Error("Choose Codex, Claude Code, Kimi Code, or OpenCode, or pass --harness explicitly.");
76
+ }
77
+ const candidates = [
78
+ "codex",
79
+ "claude-code",
80
+ "kimi-code",
81
+ "opencode",
82
+ ];
83
+ const diagnoses = await Promise.all(candidates.map(async (harness) => ({
84
+ harness,
85
+ result: await diagnoseEnvironment({
86
+ environment: process.env,
87
+ harness,
88
+ invocationDirectory,
89
+ localPackage: { binaryAvailable: true, version: "current" },
90
+ }),
91
+ })));
92
+ const operational = diagnoses
93
+ .filter(({ result }) => result.ready)
94
+ .map(({ harness }) => harness);
95
+ if (operational.length !== 1) {
96
+ throw new Error(`Automatic Harness selection requires exactly one operational Harness; found ${String(operational.length)}. Pass --harness codex, --harness claude-code, --harness kimi-code, or --harness opencode.`);
97
+ }
98
+ return operational[0];
99
+ }
100
+ async function resolveInstall(arguments_, prompt) {
101
+ if (!arguments_.install || arguments_.yes) {
102
+ return arguments_.install;
103
+ }
104
+ const answer = await requirePrompt(prompt).question("Install @nyxa/automation as a development dependency? [Y/n] ");
105
+ if (answer === "" || answer === "y" || answer === "yes") {
106
+ return true;
107
+ }
108
+ if (answer === "n" || answer === "no") {
109
+ return false;
110
+ }
111
+ throw new Error("Answer yes or no to the dependency installation prompt.");
112
+ }
113
+ async function createPrompt() {
114
+ if (process.stdin.isTTY !== true) {
115
+ const answers = (await readStandardInput()).split(/\r?\n/u);
116
+ return {
117
+ close() { },
118
+ async question(message) {
119
+ process.stdout.write(message);
120
+ const answer = answers.shift();
121
+ if (answer === undefined) {
122
+ throw new Error("Interactive input ended. Pass --yes, --no-install, and --harness for non-interactive use.");
123
+ }
124
+ return answer.trim().toLowerCase();
125
+ },
126
+ };
127
+ }
128
+ const { createInterface } = await import("node:readline/promises");
129
+ const readline = createInterface({
130
+ input: process.stdin,
131
+ output: process.stdout,
132
+ });
133
+ return {
134
+ close() {
135
+ readline.close();
136
+ },
137
+ async question(message) {
138
+ return (await readline.question(message)).trim().toLowerCase();
139
+ },
140
+ };
141
+ }
142
+ function requirePrompt(prompt) {
143
+ if (prompt === undefined) {
144
+ throw new Error("Interactive input is unavailable.");
145
+ }
146
+ return prompt;
147
+ }
148
+ const PACKAGE_MANAGERS = [
149
+ {
150
+ displayName: "npm",
151
+ executable: "npm",
152
+ installArguments: ["install", "--save-dev", "@nyxa/automation"],
153
+ lockfile: "package-lock.json",
154
+ },
155
+ {
156
+ displayName: "pnpm",
157
+ executable: "pnpm",
158
+ installArguments: ["add", "--save-dev", "@nyxa/automation"],
159
+ lockfile: "pnpm-lock.yaml",
160
+ },
161
+ {
162
+ displayName: "Yarn",
163
+ executable: "yarn",
164
+ installArguments: ["add", "--dev", "@nyxa/automation"],
165
+ lockfile: "yarn.lock",
166
+ },
167
+ ];
168
+ async function detectPackageManager(invocationDirectory) {
169
+ for (const packageManager of PACKAGE_MANAGERS) {
170
+ if (await pathExists(path.join(invocationDirectory, packageManager.lockfile))) {
171
+ return packageManager;
172
+ }
173
+ }
174
+ throw new Error("Could not detect npm, pnpm, or Yarn. Add the package manager lockfile first.");
175
+ }
176
+ async function installAutomationDependency(packageManager, invocationDirectory) {
177
+ const { spawn } = await import("node:child_process");
178
+ const exitCode = await new Promise((resolve, reject) => {
179
+ const child = spawn(packageManager.executable, packageManager.installArguments, {
180
+ cwd: invocationDirectory,
181
+ env: process.env,
182
+ stdio: "inherit",
183
+ });
184
+ child.once("error", reject);
185
+ child.once("exit", (code) => resolve(code ?? 1));
186
+ });
187
+ if (exitCode !== 0) {
188
+ throw new Error(`${packageManager.displayName} could not install @nyxa/automation (exit ${String(exitCode)}).`);
189
+ }
190
+ }
191
+ async function pathExists(filePath) {
192
+ try {
193
+ await access(filePath);
194
+ return true;
195
+ }
196
+ catch (error) {
197
+ if (isMissingFileError(error)) {
198
+ return false;
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+ function parseInitArguments(arguments_) {
204
+ const workflowPath = arguments_[0];
205
+ if (workflowPath === undefined || workflowPath.startsWith("-")) {
206
+ throw new Error("init requires a Workflow path.");
207
+ }
208
+ let harness;
209
+ let install = true;
210
+ let yes = false;
211
+ let index = 1;
212
+ while (index < arguments_.length) {
213
+ const option = arguments_[index];
214
+ if (option === "--no-install") {
215
+ if (!install) {
216
+ throw new Error("The --no-install option may only be provided once.");
217
+ }
218
+ install = false;
219
+ index += 1;
220
+ continue;
221
+ }
222
+ if (option === "--yes") {
223
+ if (yes) {
224
+ throw new Error("The --yes option may only be provided once.");
225
+ }
226
+ yes = true;
227
+ index += 1;
228
+ continue;
229
+ }
230
+ if (option === "--harness") {
231
+ if (harness !== undefined) {
232
+ throw new Error("The --harness option may only be provided once.");
233
+ }
234
+ const value = arguments_[index + 1];
235
+ if (value !== "codex" &&
236
+ value !== "claude-code" &&
237
+ value !== "kimi-code" &&
238
+ value !== "opencode") {
239
+ throw new Error("The --harness option requires codex, claude-code, kimi-code, or opencode.");
240
+ }
241
+ harness = value;
242
+ index += 2;
243
+ continue;
244
+ }
245
+ throw new Error(`Unexpected init argument: ${String(option)}`);
246
+ }
247
+ return {
248
+ ...(harness === undefined ? {} : { harness }),
249
+ install,
250
+ workflowPath,
251
+ yes,
252
+ };
253
+ }
254
+ function workflowExample(harness) {
255
+ const factory = harness === "codex"
256
+ ? "codex"
257
+ : harness === "claude-code"
258
+ ? "claudeCode"
259
+ : harness === "kimi-code"
260
+ ? "kimiCode"
261
+ : "opencode";
262
+ const runPolicy = harness === "kimi-code" || harness === "opencode"
263
+ ? '{ access: "full", approval: "deny", output: outcome }'
264
+ : '{ access: "read", output: outcome }';
265
+ const accessWarning = harness === "opencode"
266
+ ? " // OpenCode requires full access because its CLI does not sandbox shell commands to the Workspace.\n"
267
+ : "";
268
+ return `import { ${factory}, defineWorkflow, schema } from "@nyxa/automation";
269
+
270
+ const outcome = schema.union([
271
+ schema.object({
272
+ status: schema.literal("completed").describe("The request was completed."),
273
+ summary: schema.string().describe("A concise summary of the result."),
274
+ }).describe("A completed request."),
275
+ schema.object({
276
+ status: schema.literal("needs_input").describe("More information is required."),
277
+ question: schema.string().describe("The question the user must answer."),
278
+ }).describe("A request that needs user input."),
279
+ ]).describe("The structured outcome of the Run.");
280
+
281
+ const workflow = defineWorkflow({
282
+ harness: ${factory}(),
283
+ async run(context) {
284
+ ${accessWarning} const result = await context.run(
285
+ "Inspect this project and summarize its purpose. Ask one question when essential context is missing.",
286
+ ${runPolicy},
287
+ );
288
+
289
+ if (result.status === "needs_input") {
290
+ return { status: "needs_input", question: result.question };
291
+ }
292
+
293
+ return { status: "completed", summary: result.summary };
294
+ },
295
+ });
296
+
297
+ export default workflow;
298
+ `;
299
+ }
300
+ function isExistingFileError(error) {
301
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
302
+ }
303
+ function workflowExistsError(workflowPath) {
304
+ return new Error(`Refusing to overwrite the existing Workflow at ${workflowPath}.`);
305
+ }
package/dist/json.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type JsonPrimitive = boolean | null | number | string;
2
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
3
+ readonly [key: string]: JsonValue;
4
+ };
package/dist/json.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { type Harness } from "./harness.js";
2
+ import type { NoExtraKeys } from "./type-utils.js";
3
+ export declare const KIMI_CODE_HARNESS_PREFLIGHT: {
4
+ readonly harnessName: "Kimi Code";
5
+ readonly minimumVersion: readonly [0, 28, 1];
6
+ readonly unsupportedVersionMessage: "Kimi Code CLI 0.28.1 or later is required.";
7
+ readonly versionPattern: RegExp;
8
+ };
9
+ export interface KimiCodeOptions {
10
+ readonly executable?: string;
11
+ readonly model?: string;
12
+ readonly effort?: string;
13
+ }
14
+ export declare function kimiCode(): Harness;
15
+ export declare function kimiCode<const TOptions extends KimiCodeOptions>(options: TOptions & NoExtraKeys<TOptions, KimiCodeOptions>): Harness;
16
+ export declare function verifyKimiCodeAuthentication(executable: string, workingDirectory: string, environment: NodeJS.ProcessEnv): Promise<void>;
@@ -0,0 +1,299 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import { ClientSideConnection, PROTOCOL_VERSION, RequestError, ndJsonStream, } from "@agentclientprotocol/sdk";
3
+ import { AutomationError, isAutomationError } from "./errors.js";
4
+ import { createHarnessPreflight, harnessProcessExitError, runHarnessProtocolProcess, } from "./harness-process.js";
5
+ import { createHarness, harnessProcessControls, } from "./harness.js";
6
+ import { promptWithOutputContract } from "./harness-prompt.js";
7
+ const HARNESS_NAME = "Kimi Code";
8
+ export const KIMI_CODE_HARNESS_PREFLIGHT = {
9
+ harnessName: HARNESS_NAME,
10
+ minimumVersion: [0, 28, 1],
11
+ unsupportedVersionMessage: "Kimi Code CLI 0.28.1 or later is required.",
12
+ versionPattern: /(?:\bkimi(?:[ -]code)?\s+)?(\d+)\.(\d+)\.(\d+)(?:-([0-9a-z.-]+))?(?:\+[0-9a-z.-]+)?(?:\s|$)/iu,
13
+ };
14
+ const preflightKimiCode = createHarnessPreflight(KIMI_CODE_HARNESS_PREFLIGHT);
15
+ export function kimiCode(options = {}) {
16
+ const configuredOptions = Object.freeze({ ...options });
17
+ return createHarness({
18
+ name: HARNESS_NAME,
19
+ async run(request) {
20
+ if (request.access !== "full") {
21
+ throw new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", "Kimi Code cannot guarantee read or write-workspace Access Policies.");
22
+ }
23
+ const executable = await preflightKimiCode(configuredOptions.executable ?? "kimi", request.environment, harnessProcessControls(request));
24
+ return executeKimiCode(executable, configuredOptions, request);
25
+ },
26
+ });
27
+ }
28
+ export async function verifyKimiCodeAuthentication(executable, workingDirectory, environment) {
29
+ try {
30
+ const result = await runHarnessProtocolProcess(executable, ["acp"], workingDirectory, HARNESS_NAME, async (process) => {
31
+ const connection = createKimiConnection(process.input, process.output, {
32
+ approval: "deny",
33
+ async sessionUpdate() { },
34
+ });
35
+ await initializeAndAuthenticate(connection);
36
+ }, { environment });
37
+ if (result.exitCode !== 0) {
38
+ throw harnessProcessExitError(HARNESS_NAME, {
39
+ exitCode: result.exitCode,
40
+ stderr: result.stderr,
41
+ stdout: "",
42
+ });
43
+ }
44
+ }
45
+ catch (cause) {
46
+ throw mapKimiCodeError(cause, "authentication");
47
+ }
48
+ }
49
+ async function executeKimiCode(executable, options, request) {
50
+ await request.start();
51
+ try {
52
+ const result = await runHarnessProtocolProcess(executable, ["acp"], request.workingDirectory, HARNESS_NAME, async (process) => {
53
+ let activeSessionId;
54
+ let callbackFailure;
55
+ const pendingUpdates = new Set();
56
+ let receivedAgentMessage = false;
57
+ let response = "";
58
+ const connection = createKimiConnection(process.input, process.output, {
59
+ approval: request.approval,
60
+ sessionUpdate(notification) {
61
+ const update = processKimiCodeUpdate(notification, () => activeSessionId, request, (text) => {
62
+ receivedAgentMessage = true;
63
+ response += text;
64
+ }).catch((cause) => {
65
+ callbackFailure ??= cause;
66
+ throw cause;
67
+ });
68
+ pendingUpdates.add(update);
69
+ void update.then(() => pendingUpdates.delete(update), () => pendingUpdates.delete(update));
70
+ return update;
71
+ },
72
+ });
73
+ process.onInterrupt(async () => {
74
+ if (activeSessionId !== undefined) {
75
+ await connection.cancel({ sessionId: activeSessionId });
76
+ }
77
+ });
78
+ const initialized = await initializeAndAuthenticate(connection);
79
+ let configOptions;
80
+ if (request.nativeSessionId === undefined) {
81
+ const created = await connection.newSession({
82
+ cwd: request.workingDirectory,
83
+ mcpServers: [],
84
+ });
85
+ activeSessionId = requireSessionId(created.sessionId);
86
+ configOptions = created.configOptions ?? [];
87
+ }
88
+ else {
89
+ if (initialized.agentCapabilities?.sessionCapabilities?.resume ===
90
+ undefined ||
91
+ initialized.agentCapabilities.sessionCapabilities.resume === null) {
92
+ throw new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", "Kimi Code did not advertise ACP Session resume support.");
93
+ }
94
+ activeSessionId = requireSessionId(request.nativeSessionId);
95
+ const resumed = await connection.resumeSession({
96
+ cwd: request.workingDirectory,
97
+ mcpServers: [],
98
+ sessionId: activeSessionId,
99
+ });
100
+ configOptions = resumed.configOptions ?? [];
101
+ }
102
+ await applyKimiCodeConfiguration(connection, activeSessionId, configOptions, options, request.approval);
103
+ const prompt = await connection.prompt({
104
+ prompt: [
105
+ {
106
+ type: "text",
107
+ text: promptWithOutputContract(request),
108
+ },
109
+ ],
110
+ sessionId: activeSessionId,
111
+ });
112
+ await Promise.allSettled([...pendingUpdates]);
113
+ if (callbackFailure !== undefined) {
114
+ throw callbackFailure;
115
+ }
116
+ return {
117
+ nativeSessionId: activeSessionId,
118
+ receivedAgentMessage,
119
+ response,
120
+ stopReason: prompt.stopReason,
121
+ };
122
+ }, {
123
+ environment: request.environment,
124
+ ...harnessProcessControls(request),
125
+ });
126
+ if (result.exitCode !== 0) {
127
+ throw harnessProcessExitError(HARNESS_NAME, {
128
+ exitCode: result.exitCode,
129
+ stderr: result.stderr,
130
+ stdout: "",
131
+ });
132
+ }
133
+ if (result.value.stopReason !== "end_turn") {
134
+ request.sessionReady?.(result.value.nativeSessionId);
135
+ throw new AutomationError("HARNESS_PROCESS_FAILED", `Kimi Code stopped the Run with reason ${result.value.stopReason}.`);
136
+ }
137
+ if (!result.value.receivedAgentMessage) {
138
+ if (request.output !== undefined) {
139
+ request.sessionReady?.(result.value.nativeSessionId);
140
+ throw new AutomationError("RUN_OUTPUT_INVALID", "The Kimi Code Run completed without structured output.", { cause: new TypeError("Missing Kimi Code agent message.") });
141
+ }
142
+ throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code completed without a final agent message.");
143
+ }
144
+ if (request.output === undefined) {
145
+ return {
146
+ nativeSessionId: result.value.nativeSessionId,
147
+ value: result.value.response,
148
+ };
149
+ }
150
+ try {
151
+ return {
152
+ nativeSessionId: result.value.nativeSessionId,
153
+ value: JSON.parse(result.value.response),
154
+ };
155
+ }
156
+ catch (cause) {
157
+ request.sessionReady?.(result.value.nativeSessionId);
158
+ throw new AutomationError("RUN_OUTPUT_INVALID", "The Kimi Code Run returned malformed structured output.", { cause });
159
+ }
160
+ }
161
+ catch (cause) {
162
+ throw mapKimiCodeError(cause, "run");
163
+ }
164
+ }
165
+ async function processKimiCodeUpdate(notification, activeSessionId, request, appendResponse) {
166
+ await request.emitNativeEvent({
167
+ method: "session/update",
168
+ params: notification,
169
+ });
170
+ const sessionId = activeSessionId();
171
+ if (sessionId !== undefined && notification.sessionId !== sessionId) {
172
+ throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code emitted an update for a different Session.");
173
+ }
174
+ if (notification.update.sessionUpdate === "agent_message_chunk" &&
175
+ notification.update.content.type === "text") {
176
+ appendResponse(notification.update.content.text);
177
+ }
178
+ }
179
+ function createKimiConnection(input, output, options) {
180
+ const stream = ndJsonStream(Writable.toWeb(input), Readable.toWeb(output));
181
+ return new ClientSideConnection(() => ({
182
+ requestPermission(request) {
183
+ return resolvePermissionRequest(request, options.approval);
184
+ },
185
+ sessionUpdate: options.sessionUpdate,
186
+ }), stream);
187
+ }
188
+ async function initializeAndAuthenticate(connection) {
189
+ const initialized = await connection.initialize({
190
+ clientCapabilities: {},
191
+ clientInfo: { name: "@nyxa/automation", version: "0.1.0" },
192
+ protocolVersion: PROTOCOL_VERSION,
193
+ });
194
+ if (initialized.protocolVersion !== PROTOCOL_VERSION) {
195
+ throw new AutomationError("HARNESS_PROTOCOL_ERROR", `Kimi Code negotiated unsupported ACP version ${String(initialized.protocolVersion)}.`);
196
+ }
197
+ if (!initialized.authMethods?.some((method) => method.id === "login")) {
198
+ throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code did not advertise its non-interactive login authentication check.");
199
+ }
200
+ await connection.authenticate({ methodId: "login" });
201
+ return initialized;
202
+ }
203
+ async function applyKimiCodeConfiguration(connection, sessionId, initialOptions, options, approval) {
204
+ let configOptions = initialOptions;
205
+ if (options.model !== undefined) {
206
+ configOptions = await setAdvertisedConfigOption(connection, sessionId, configOptions, "model", options.model);
207
+ }
208
+ if (options.effort !== undefined) {
209
+ configOptions = await setAdvertisedConfigOption(connection, sessionId, configOptions, "effort", options.effort);
210
+ }
211
+ const mode = approval === "auto" ? "auto" : "default";
212
+ const advertisedMode = findConfigOption(configOptions, "mode", mode);
213
+ if (advertisedMode !== undefined && !optionIncludes(advertisedMode, mode)) {
214
+ throw unsupportedKimiConfiguration("mode", mode);
215
+ }
216
+ try {
217
+ await connection.setSessionMode({ modeId: mode, sessionId });
218
+ }
219
+ catch (cause) {
220
+ throw mapKimiCodeError(cause, "configuration");
221
+ }
222
+ }
223
+ async function setAdvertisedConfigOption(connection, sessionId, configOptions, kind, value) {
224
+ const option = findConfigOption(configOptions, kind, value);
225
+ if (option === undefined || !optionIncludes(option, value)) {
226
+ throw unsupportedKimiConfiguration(kind, value);
227
+ }
228
+ try {
229
+ const configured = await connection.setSessionConfigOption({
230
+ configId: option.id,
231
+ sessionId,
232
+ value,
233
+ });
234
+ return configured.configOptions;
235
+ }
236
+ catch (cause) {
237
+ throw mapKimiCodeError(cause, "configuration");
238
+ }
239
+ }
240
+ function findConfigOption(options, kind, requestedValue) {
241
+ const ids = kind === "model"
242
+ ? ["model"]
243
+ : kind === "mode"
244
+ ? ["mode"]
245
+ : ["effort", "thinking_effort", "reasoning_effort", "thinking"];
246
+ const category = kind === "effort" ? "thought_level" : kind;
247
+ return (options.find((option) => ids.includes(option.id) && optionIncludes(option, requestedValue)) ??
248
+ options.find((option) => option.category === category && optionIncludes(option, requestedValue)) ??
249
+ options.find((option) => ids.includes(option.id)) ??
250
+ options.find((option) => option.category === category));
251
+ }
252
+ function optionIncludes(option, value) {
253
+ if (option.type !== "select") {
254
+ return false;
255
+ }
256
+ return option.options.some((entry) => "value" in entry
257
+ ? entry.value === value
258
+ : entry.options.some((nested) => nested.value === value));
259
+ }
260
+ function unsupportedKimiConfiguration(kind, value) {
261
+ return new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", `Kimi Code does not support the requested ${kind}: ${value}.`);
262
+ }
263
+ function resolvePermissionRequest(request, approval) {
264
+ if (approval === "auto") {
265
+ return Promise.resolve({ outcome: { outcome: "cancelled" } });
266
+ }
267
+ const rejected = request.options.find((option) => option.kind === "reject_once") ??
268
+ request.options.find((option) => option.kind === "reject_always");
269
+ return Promise.resolve(rejected === undefined
270
+ ? { outcome: { outcome: "cancelled" } }
271
+ : {
272
+ outcome: {
273
+ outcome: "selected",
274
+ optionId: rejected.optionId,
275
+ },
276
+ });
277
+ }
278
+ function requireSessionId(value) {
279
+ if (value.length === 0) {
280
+ throw new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code returned an invalid Session identifier.");
281
+ }
282
+ return value;
283
+ }
284
+ function mapKimiCodeError(cause, phase) {
285
+ if (isAutomationError(cause)) {
286
+ return cause;
287
+ }
288
+ if (cause instanceof RequestError) {
289
+ if (cause.code === -32_000 ||
290
+ /auth(?:entication)? required/iu.test(cause.message)) {
291
+ return new AutomationError("HARNESS_NOT_AUTHENTICATED", "Kimi Code is not authenticated.", { cause });
292
+ }
293
+ if (phase === "configuration") {
294
+ return new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", "Kimi Code rejected the requested model, effort, or approval mode.", { cause });
295
+ }
296
+ return new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code returned an ACP error.", { cause });
297
+ }
298
+ return new AutomationError("HARNESS_PROTOCOL_ERROR", "Kimi Code could not complete the ACP exchange.", { cause });
299
+ }
@@ -0,0 +1,15 @@
1
+ import { type Harness } from "./harness.js";
2
+ import type { NoExtraKeys } from "./type-utils.js";
3
+ export declare const OPENCODE_HARNESS_PREFLIGHT: {
4
+ readonly harnessName: "OpenCode";
5
+ readonly minimumVersion: readonly [1, 18, 4];
6
+ readonly unsupportedVersionMessage: "OpenCode CLI 1.18.4 or later is required.";
7
+ readonly versionPattern: RegExp;
8
+ };
9
+ export interface OpenCodeOptions {
10
+ readonly executable?: string;
11
+ readonly model?: string;
12
+ readonly effort?: string;
13
+ }
14
+ export declare function opencode(): Harness;
15
+ export declare function opencode<const TOptions extends OpenCodeOptions>(options: TOptions & NoExtraKeys<TOptions, OpenCodeOptions>): Harness;