oorja-harness-host 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
1
+ # Harness Host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Runtime support for hosting AI coding harness sessions on a computer connected
4
+ to a Supakit room.
5
+
6
+ The package owns live harness sessions, adapters, preparation, resume state,
7
+ and the shared message contract. Applications own channel connections, routing,
8
+ machine identity, and command-line presentation.
9
+
10
+ Turn output remains buffered in memory until the client confirms that it has
11
+ saved it. On graceful shutdown, the host saves the opaque AI SDK resume state
12
+ provided by each harness session so the next process can resume it.
@@ -0,0 +1,37 @@
1
+ import { type HarnessCommand, type HarnessEvent, type HostState } from './messages.js';
2
+ import type { ResumeStateStore } from './resumeState.js';
3
+ import type { HarnessRuntime } from './runtime.js';
4
+ export type HarnessHostOptions = {
5
+ hostId: string;
6
+ hostname: string;
7
+ runtimes: HarnessRuntime[];
8
+ resumeStates: ResumeStateStore;
9
+ emit: (event: HarnessEvent) => void;
10
+ onError: (error: unknown) => void;
11
+ };
12
+ export declare class HarnessHost {
13
+ private readonly options;
14
+ private readonly runtimes;
15
+ private readonly sessions;
16
+ private readonly openingSessions;
17
+ private stopping;
18
+ constructor(options: HarnessHostOptions);
19
+ getState: () => HostState;
20
+ handleCommand: (command: HarnessCommand) => Promise<void>;
21
+ stop: () => Promise<void>;
22
+ private submitMessage;
23
+ private steer;
24
+ private runTurn;
25
+ private abortTurn;
26
+ private sendTurnSnapshot;
27
+ private handleTurnSaved;
28
+ private destroySession;
29
+ private setSessionModel;
30
+ private setModel;
31
+ private getSession;
32
+ private openSession;
33
+ private publishTurnPart;
34
+ private emitActivity;
35
+ private saveSession;
36
+ private emitState;
37
+ }
@@ -0,0 +1,337 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { smoothStream } from 'ai';
3
+ import { HarnessTurnPartType, MessageType, SessionStatus, TurnOutcome, } from './messages.js';
4
+ import { createTurnActivity, updateTurnActivity, } from './activity.js';
5
+ import { HARNESS_MODELS } from './models.js';
6
+ import { Future } from './utils.js';
7
+ export class HarnessHost {
8
+ options;
9
+ runtimes;
10
+ sessions = new Map();
11
+ openingSessions = new Map();
12
+ stopping = false;
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.runtimes = new Map(options.runtimes.map((runtime) => [runtime.harnessId, runtime]));
16
+ }
17
+ getState = () => ({
18
+ type: MessageType.HOST_STATE,
19
+ hostId: this.options.hostId,
20
+ hostname: this.options.hostname,
21
+ harnesses: [...this.runtimes.keys()],
22
+ models: HARNESS_MODELS,
23
+ sessions: [...this.sessions.values()].map(toSessionState),
24
+ });
25
+ handleCommand = async (command) => {
26
+ if (this.stopping)
27
+ return;
28
+ try {
29
+ switch (command.type) {
30
+ case MessageType.MESSAGE_SUBMIT:
31
+ await this.submitMessage(command);
32
+ break;
33
+ case MessageType.TURN_ABORT:
34
+ this.abortTurn(command);
35
+ break;
36
+ case MessageType.TURN_SNAPSHOT_REQUEST:
37
+ this.sendTurnSnapshot(command);
38
+ break;
39
+ case MessageType.TURN_SAVED:
40
+ this.handleTurnSaved(command);
41
+ break;
42
+ case MessageType.SESSION_MODEL_SET:
43
+ this.setSessionModel(command);
44
+ break;
45
+ case MessageType.SESSION_DESTROY:
46
+ await this.destroySession(command);
47
+ break;
48
+ }
49
+ }
50
+ catch (error) {
51
+ this.options.onError(error);
52
+ }
53
+ };
54
+ stop = async () => {
55
+ this.stopping = true;
56
+ await Promise.allSettled(this.openingSessions.values());
57
+ const sessions = [...this.sessions.values()];
58
+ sessions.forEach(({ activeTurn }) => activeTurn?.abortController.abort());
59
+ await Promise.allSettled(sessions.map(({ activeTurn }) => activeTurn?.completion.promise));
60
+ await Promise.allSettled(sessions.map(this.saveSession));
61
+ this.sessions.clear();
62
+ };
63
+ submitMessage = async (command) => {
64
+ const session = await this.getSession(command);
65
+ if (this.stopping)
66
+ return;
67
+ if (session.activeTurn) {
68
+ await this.steer(session, command.message);
69
+ return;
70
+ }
71
+ const turn = {
72
+ id: randomUUID(),
73
+ status: SessionStatus.RUNNING,
74
+ startedAt: Date.now(),
75
+ abortController: new AbortController(),
76
+ completion: new Future(),
77
+ };
78
+ session.activeTurn = turn;
79
+ session.turnBuffer.set(turn.id, { id: turn.id, parts: [] });
80
+ await this.runTurn(session, command, turn);
81
+ };
82
+ steer = async (session, message) => {
83
+ const turn = session.activeTurn;
84
+ session.pendingMessages.push(message);
85
+ this.emitState();
86
+ try {
87
+ await session.runtime.steer(message.content);
88
+ if (session.activeTurn === turn) {
89
+ this.publishTurnPart(session, {
90
+ type: MessageType.TURN_PART,
91
+ hostId: this.options.hostId,
92
+ harnessId: session.harnessId,
93
+ sessionId: session.sessionId,
94
+ turnId: turn.id,
95
+ sequence: session.turnBuffer.get(turn.id).parts.length + 1,
96
+ part: {
97
+ type: HarnessTurnPartType.USER_MESSAGE,
98
+ payload: message,
99
+ },
100
+ });
101
+ }
102
+ }
103
+ finally {
104
+ session.pendingMessages = session.pendingMessages.filter(({ id }) => id !== message.id);
105
+ this.emitState();
106
+ }
107
+ };
108
+ runTurn = async (session, command, turn) => {
109
+ let outcome = TurnOutcome.COMPLETED;
110
+ let turnError;
111
+ try {
112
+ if (command.model)
113
+ this.setModel(session, command.model);
114
+ const model = session.model;
115
+ session.activity = createTurnActivity(turn.id, model);
116
+ this.publishTurnPart(session, {
117
+ type: MessageType.TURN_PART,
118
+ hostId: this.options.hostId,
119
+ harnessId: session.harnessId,
120
+ sessionId: session.sessionId,
121
+ turnId: turn.id,
122
+ sequence: 1,
123
+ part: {
124
+ type: HarnessTurnPartType.USER_MESSAGE,
125
+ payload: command.message,
126
+ },
127
+ });
128
+ this.emitState();
129
+ const stream = await session.runtime.stream(command.message.content, turn.abortController.signal, model);
130
+ const smoothedStream = stream.pipeThrough(smoothStream({ chunking: 'line', delayInMs: 10 })({ tools: {} }));
131
+ for await (const part of smoothedStream) {
132
+ if (part.type === 'abort')
133
+ outcome = TurnOutcome.ABORTED;
134
+ if (part.type === 'error') {
135
+ outcome = TurnOutcome.FAILED;
136
+ turnError = errorMessage(part.error);
137
+ }
138
+ this.publishTurnPart(session, {
139
+ type: MessageType.TURN_PART,
140
+ hostId: this.options.hostId,
141
+ harnessId: session.harnessId,
142
+ sessionId: session.sessionId,
143
+ turnId: turn.id,
144
+ sequence: session.turnBuffer.get(turn.id).parts.length + 1,
145
+ part: {
146
+ type: HarnessTurnPartType.STREAM,
147
+ payload: part,
148
+ },
149
+ });
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (turn.abortController.signal.aborted) {
154
+ outcome = TurnOutcome.ABORTED;
155
+ }
156
+ else {
157
+ outcome = TurnOutcome.FAILED;
158
+ turnError = errorMessage(error);
159
+ this.options.onError(error);
160
+ }
161
+ }
162
+ finally {
163
+ if (session.activeTurn === turn) {
164
+ turn.completion.resolve?.();
165
+ session.activeTurn = undefined;
166
+ session.pendingMessages = [];
167
+ if (session.activity) {
168
+ session.activity = { ...session.activity, tools: [] };
169
+ this.emitActivity(session);
170
+ }
171
+ }
172
+ const bufferedTurn = session.turnBuffer.get(turn.id);
173
+ bufferedTurn.outcome = outcome;
174
+ bufferedTurn.error = turnError;
175
+ const ended = {
176
+ type: MessageType.TURN_ENDED,
177
+ hostId: this.options.hostId,
178
+ harnessId: session.harnessId,
179
+ sessionId: session.sessionId,
180
+ turnId: turn.id,
181
+ sequence: bufferedTurn.parts.length + 1,
182
+ outcome,
183
+ ...(turnError ? { error: turnError } : {}),
184
+ };
185
+ this.options.emit(ended);
186
+ this.emitState();
187
+ }
188
+ };
189
+ abortTurn = (command) => {
190
+ const session = this.sessions.get(command.sessionId);
191
+ if (!session?.activeTurn)
192
+ throw new Error('Harness session is not active');
193
+ session.activeTurn.status = SessionStatus.ABORTING;
194
+ session.activeTurn.abortController.abort();
195
+ this.emitState();
196
+ };
197
+ sendTurnSnapshot = (command) => {
198
+ const turn = this.sessions.get(command.sessionId)?.turnBuffer.get(command.turnId);
199
+ if (!turn)
200
+ return;
201
+ this.options.emit({
202
+ type: MessageType.TURN_SNAPSHOT,
203
+ hostId: this.options.hostId,
204
+ harnessId: command.harnessId,
205
+ sessionId: command.sessionId,
206
+ turnId: command.turnId,
207
+ sequence: turn.parts.length + (turn.outcome ? 1 : 0),
208
+ parts: turn.parts,
209
+ ...(turn.outcome ? { outcome: turn.outcome } : {}),
210
+ ...(turn.error ? { error: turn.error } : {}),
211
+ });
212
+ };
213
+ handleTurnSaved = (command) => {
214
+ const session = this.sessions.get(command.sessionId);
215
+ const turn = session?.turnBuffer.get(command.turnId);
216
+ if (!session || !turn?.outcome)
217
+ return;
218
+ session.turnBuffer.delete(command.turnId);
219
+ this.emitState();
220
+ };
221
+ destroySession = async (command) => {
222
+ const session = this.sessions.get(command.sessionId);
223
+ if (session) {
224
+ session.activeTurn?.abortController.abort();
225
+ await session.activeTurn?.completion.promise;
226
+ await session.runtime.destroy();
227
+ this.sessions.delete(command.sessionId);
228
+ }
229
+ await this.options.resumeStates.remove(command.harnessId, command.sessionId);
230
+ this.emitState();
231
+ };
232
+ setSessionModel = (command) => {
233
+ const session = this.sessions.get(command.sessionId);
234
+ if (!session)
235
+ throw new Error('Harness session does not exist');
236
+ this.setModel(session, command.model);
237
+ this.emitState();
238
+ };
239
+ setModel = (session, model) => {
240
+ if (!HARNESS_MODELS[session.harnessId].models.includes(model)) {
241
+ throw new Error(`Model is not available: ${model}`);
242
+ }
243
+ session.model = model;
244
+ };
245
+ getSession = async (command) => {
246
+ const existing = this.sessions.get(command.sessionId);
247
+ if (existing)
248
+ return existing;
249
+ const opening = this.openingSessions.get(command.sessionId);
250
+ if (opening)
251
+ return opening;
252
+ const runtime = this.runtimes.get(command.harnessId);
253
+ if (!runtime)
254
+ throw new Error(`Harness is not available: ${command.harnessId}`);
255
+ const pending = this.openSession(runtime, command.sessionId);
256
+ this.openingSessions.set(command.sessionId, pending);
257
+ try {
258
+ return await pending;
259
+ }
260
+ finally {
261
+ this.openingSessions.delete(command.sessionId);
262
+ }
263
+ };
264
+ openSession = async (runtime, sessionId) => {
265
+ const resumeFrom = await this.options.resumeStates.load(runtime.harnessId, sessionId);
266
+ const session = {
267
+ harnessId: runtime.harnessId,
268
+ sessionId,
269
+ model: HARNESS_MODELS[runtime.harnessId].defaultModel,
270
+ runtime: await runtime.createSession(sessionId, resumeFrom),
271
+ pendingMessages: [],
272
+ turnBuffer: new Map(),
273
+ };
274
+ this.sessions.set(sessionId, session);
275
+ return session;
276
+ };
277
+ publishTurnPart = (session, event) => {
278
+ session.turnBuffer.get(event.turnId).parts.push(event.part);
279
+ this.options.emit(event);
280
+ if (session.activity && event.part.type === HarnessTurnPartType.STREAM) {
281
+ const activity = updateTurnActivity(session.activity, event.part.payload);
282
+ if (activity !== session.activity) {
283
+ session.activity = activity;
284
+ this.emitActivity(session);
285
+ }
286
+ }
287
+ };
288
+ emitActivity = (session) => {
289
+ this.options.emit({
290
+ type: MessageType.SESSION_ACTIVITY,
291
+ hostId: this.options.hostId,
292
+ harnessId: session.harnessId,
293
+ sessionId: session.sessionId,
294
+ activity: session.activity,
295
+ });
296
+ };
297
+ saveSession = async (session) => {
298
+ try {
299
+ const state = await session.runtime.stop();
300
+ await this.options.resumeStates.save(session.harnessId, session.sessionId, state);
301
+ }
302
+ catch (error) {
303
+ this.options.onError(error);
304
+ await this.options.resumeStates.markFailed(session.harnessId, session.sessionId, errorMessage(error));
305
+ throw error;
306
+ }
307
+ };
308
+ emitState = () => this.options.emit(this.getState());
309
+ }
310
+ const toSessionState = ({ harnessId, sessionId, model, pendingMessages, turnBuffer, activeTurn, activity, }) => ({
311
+ harnessId,
312
+ sessionId,
313
+ model,
314
+ pendingMessages: [...pendingMessages],
315
+ status: activeTurn?.status ?? SessionStatus.IDLE,
316
+ activity,
317
+ turnBuffer: [...turnBuffer.values()].map((turn) => ({
318
+ turnId: turn.id,
319
+ lastSequence: turn.parts.length + (turn.outcome ? 1 : 0),
320
+ completed: turn.outcome !== undefined,
321
+ })),
322
+ ...(activeTurn ? { turnId: activeTurn.id, elapsedMs: Date.now() - activeTurn.startedAt } : {}),
323
+ });
324
+ const errorMessage = (error) => {
325
+ if (error instanceof Error)
326
+ return error.message;
327
+ if (typeof error === 'string')
328
+ return error;
329
+ if (error && typeof error === 'object') {
330
+ const harnessError = error;
331
+ if (typeof harnessError.data?.message === 'string')
332
+ return harnessError.data.message;
333
+ if (typeof harnessError.message === 'string')
334
+ return harnessError.message;
335
+ }
336
+ return JSON.stringify(error) ?? String(error);
337
+ };
@@ -0,0 +1,25 @@
1
+ import type { TextStreamPart, ToolSet } from 'ai';
2
+ export type ChangedFile = {
3
+ path: string;
4
+ event: 'create' | 'modify' | 'delete';
5
+ };
6
+ export type TurnActivity = {
7
+ turnId: string;
8
+ model?: string;
9
+ steps: number;
10
+ inputTokens?: number;
11
+ outputTokens?: number;
12
+ tools: Array<{
13
+ id: string;
14
+ name: string;
15
+ }>;
16
+ files: ChangedFile[];
17
+ compaction?: {
18
+ trigger: 'auto' | 'manual';
19
+ summary: string;
20
+ tokensBefore?: number;
21
+ tokensAfter?: number;
22
+ };
23
+ };
24
+ export declare const createTurnActivity: (turnId: string, model?: string) => TurnActivity;
25
+ export declare const updateTurnActivity: (activity: TurnActivity, part: TextStreamPart<ToolSet>) => TurnActivity;
@@ -0,0 +1,71 @@
1
+ export const createTurnActivity = (turnId, model) => ({
2
+ turnId,
3
+ model,
4
+ steps: 0,
5
+ tools: [],
6
+ files: [],
7
+ });
8
+ export const updateTurnActivity = (activity, part) => {
9
+ switch (part.type) {
10
+ case 'tool-input-start':
11
+ if (isActivityTool(part.toolName))
12
+ return activity;
13
+ return {
14
+ ...activity,
15
+ tools: [...activity.tools.filter((tool) => tool.id !== part.id), { id: part.id, name: part.toolName }],
16
+ };
17
+ case 'tool-call':
18
+ if (isActivityTool(part.toolName))
19
+ return activity;
20
+ return {
21
+ ...activity,
22
+ tools: [
23
+ ...activity.tools.filter((tool) => tool.id !== part.toolCallId),
24
+ { id: part.toolCallId, name: part.toolName },
25
+ ],
26
+ };
27
+ case 'tool-result': {
28
+ const tools = activity.tools.filter((tool) => tool.id !== part.toolCallId);
29
+ if (part.toolName === 'fileChange') {
30
+ const file = part.output;
31
+ return { ...activity, tools, files: [...activity.files.filter((current) => current.path !== file.path), file] };
32
+ }
33
+ if (part.toolName === 'compaction') {
34
+ return { ...activity, tools, compaction: part.output };
35
+ }
36
+ if (part.toolName === 'write' || part.toolName === 'edit') {
37
+ const input = part.input;
38
+ const path = input.path ?? input.file_path;
39
+ if (path) {
40
+ return {
41
+ ...activity,
42
+ tools,
43
+ files: [...activity.files.filter((file) => file.path !== path), { path, event: 'modify' }],
44
+ };
45
+ }
46
+ }
47
+ return { ...activity, tools };
48
+ }
49
+ case 'tool-error':
50
+ return { ...activity, tools: activity.tools.filter((tool) => tool.id !== part.toolCallId) };
51
+ case 'finish-step':
52
+ return {
53
+ ...activity,
54
+ model: part.response.modelId || activity.model,
55
+ steps: activity.steps + 1,
56
+ inputTokens: addTokens(activity.inputTokens, part.usage.inputTokens),
57
+ outputTokens: addTokens(activity.outputTokens, part.usage.outputTokens),
58
+ };
59
+ case 'finish':
60
+ return {
61
+ ...activity,
62
+ tools: [],
63
+ inputTokens: part.totalUsage.inputTokens,
64
+ outputTokens: part.totalUsage.outputTokens,
65
+ };
66
+ default:
67
+ return activity;
68
+ }
69
+ };
70
+ const isActivityTool = (name) => name === 'fileChange' || name === 'compaction';
71
+ const addTokens = (total, count) => count === undefined ? total : (total ?? 0) + count;
@@ -0,0 +1,8 @@
1
+ import { createClaudeCode } from '@ai-sdk/harness-claude-code';
2
+ export type ClaudeCodeConfig = NonNullable<Parameters<typeof createClaudeCode>[0]>;
3
+ export declare const createClaudeCodeRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: ClaudeCodeConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareClaudeCode: (cwd: string) => Promise<string>;
8
+ export declare const runClaudeCodeDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,16 @@
1
+ import { createClaudeCode } from '@ai-sdk/harness-claude-code';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createClaudeCodeRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.CLAUDE_CODE,
8
+ cwd,
9
+ createHarness: (port) => createClaudeCode({
10
+ ...config,
11
+ port,
12
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
13
+ }),
14
+ });
15
+ export const prepareClaudeCode = (cwd) => prepareHarness(cwd, createClaudeCode());
16
+ export const runClaudeCodeDoctor = (cwd) => runHarnessDoctor(cwd, 'Claude Code');
@@ -0,0 +1,8 @@
1
+ import { createCodex } from '@ai-sdk/harness-codex';
2
+ export type CodexConfig = NonNullable<Parameters<typeof createCodex>[0]>;
3
+ export declare const createCodexRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: CodexConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareCodex: (cwd: string) => Promise<string>;
8
+ export declare const runCodexDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,16 @@
1
+ import { createCodex } from '@ai-sdk/harness-codex';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createCodexRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.CODEX,
8
+ cwd,
9
+ createHarness: (port) => createCodex({
10
+ ...config,
11
+ port,
12
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
13
+ }),
14
+ });
15
+ export const prepareCodex = (cwd) => prepareHarness(cwd, createCodex());
16
+ export const runCodexDoctor = (cwd) => runHarnessDoctor(cwd, 'Codex');
@@ -0,0 +1,2 @@
1
+ import type { HarnessDoctorCheck } from './types.js';
2
+ export declare const runHarnessDoctor: (cwd: string, harnessName: string) => Promise<HarnessDoctorCheck[]>;
@@ -0,0 +1,52 @@
1
+ import { constants } from 'node:fs';
2
+ import { access } from 'node:fs/promises';
3
+ import { createServer } from 'node:net';
4
+ import { LocalSandboxSession } from '../sandbox/local.js';
5
+ export const runHarnessDoctor = async (cwd, harnessName) => {
6
+ const sandbox = new LocalSandboxSession(cwd);
7
+ const home = process.env.HOME;
8
+ const [pnpm, homeIsWritable, loopbackIsAvailable] = await Promise.all([
9
+ Promise.resolve(sandbox.run({ command: 'pnpm --version' })).catch(() => undefined),
10
+ home ? canAccess(home, constants.R_OK | constants.W_OK) : false,
11
+ checkLoopbackPort().then(() => true, () => false),
12
+ ]);
13
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
14
+ return [
15
+ {
16
+ name: 'node',
17
+ ok: nodeMajor >= 22,
18
+ detail: `Node ${process.versions.node}; requires Node 22 or newer`,
19
+ },
20
+ {
21
+ name: 'pnpm',
22
+ ok: pnpm?.exitCode === 0,
23
+ detail: pnpm?.exitCode === 0 ? `pnpm ${pnpm.stdout.trim()}` : `pnpm is required for ${harnessName} bootstrap`,
24
+ },
25
+ {
26
+ name: 'home',
27
+ ok: homeIsWritable,
28
+ detail: home ? `${home} must be readable and writable` : 'HOME is not set',
29
+ },
30
+ {
31
+ name: 'loopback-port',
32
+ ok: loopbackIsAvailable,
33
+ detail: loopbackIsAvailable
34
+ ? 'Can bind an ephemeral port on 127.0.0.1'
35
+ : 'Could not bind an ephemeral port on 127.0.0.1',
36
+ },
37
+ ];
38
+ };
39
+ const checkLoopbackPort = () => new Promise((resolve, reject) => {
40
+ const server = createServer();
41
+ server.once('error', reject);
42
+ server.listen(0, '127.0.0.1', () => server.close((error) => (error ? reject(error) : resolve())));
43
+ });
44
+ const canAccess = async (target, mode) => {
45
+ try {
46
+ await access(target, mode);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ };
@@ -0,0 +1,8 @@
1
+ import { HarnessId } from '../messages.js';
2
+ import type { HarnessRuntime } from '../runtime.js';
3
+ import type { HarnessDoctorResult, HarnessPrepareResult } from './types.js';
4
+ export declare const ALL_HARNESSES: "all";
5
+ export type HarnessSelection = HarnessId | typeof ALL_HARNESSES;
6
+ export declare const doctorHarnessHost: (cwd: string) => Promise<HarnessDoctorResult[]>;
7
+ export declare const prepareHarnessHost: (cwd: string, selections?: readonly HarnessSelection[]) => Promise<HarnessPrepareResult[]>;
8
+ export declare const createHarnessRuntimes: (cwd: string) => HarnessRuntime[];
@@ -0,0 +1,42 @@
1
+ import { createClaudeCodeRuntime, prepareClaudeCode, runClaudeCodeDoctor, } from './claudeCode/index.js';
2
+ import { createCodexRuntime, prepareCodex, runCodexDoctor } from './codex/index.js';
3
+ import { createOpenCodeRuntime, prepareOpenCode, runOpenCodeDoctor } from './openCode/index.js';
4
+ import { HarnessId } from '../messages.js';
5
+ export const ALL_HARNESSES = 'all';
6
+ export const doctorHarnessHost = async (cwd) => Promise.all(harnesses.map(async ({ harnessId, doctor }) => ({
7
+ harnessId,
8
+ checks: await doctor(cwd),
9
+ })));
10
+ export const prepareHarnessHost = async (cwd, selections = [ALL_HARNESSES]) => {
11
+ const results = [];
12
+ for (const harness of selectHarnesses(selections)) {
13
+ results.push({ harnessId: harness.harnessId, detail: await harness.prepare(cwd) });
14
+ }
15
+ return results;
16
+ };
17
+ export const createHarnessRuntimes = (cwd) => harnesses.map(({ createRuntime }) => createRuntime(cwd));
18
+ const codex = {
19
+ harnessId: HarnessId.CODEX,
20
+ createRuntime: (cwd) => createCodexRuntime({ cwd }),
21
+ doctor: runCodexDoctor,
22
+ prepare: prepareCodex,
23
+ };
24
+ const claudeCode = {
25
+ harnessId: HarnessId.CLAUDE_CODE,
26
+ createRuntime: (cwd) => createClaudeCodeRuntime({ cwd }),
27
+ doctor: runClaudeCodeDoctor,
28
+ prepare: prepareClaudeCode,
29
+ };
30
+ const openCode = {
31
+ harnessId: HarnessId.OPENCODE,
32
+ createRuntime: (cwd) => createOpenCodeRuntime({ cwd }),
33
+ doctor: runOpenCodeDoctor,
34
+ prepare: prepareOpenCode,
35
+ };
36
+ const harnesses = [codex, claudeCode, openCode];
37
+ const selectHarnesses = (selections) => {
38
+ if (selections.includes(ALL_HARNESSES))
39
+ return harnesses;
40
+ const selected = new Set(selections);
41
+ return harnesses.filter(({ harnessId }) => selected.has(harnessId));
42
+ };
@@ -0,0 +1,8 @@
1
+ import { createOpenCode } from '@ai-sdk/harness-opencode';
2
+ export type OpenCodeConfig = NonNullable<Parameters<typeof createOpenCode>[0]>;
3
+ export declare const createOpenCodeRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: OpenCodeConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareOpenCode: (cwd: string) => Promise<string>;
8
+ export declare const runOpenCodeDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,17 @@
1
+ import { createOpenCode } from '@ai-sdk/harness-opencode';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createOpenCodeRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.OPENCODE,
8
+ cwd,
9
+ createHarness: (port) => createOpenCode({
10
+ ...config,
11
+ provider: config?.provider ?? 'opencode',
12
+ port,
13
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
14
+ }),
15
+ });
16
+ export const prepareOpenCode = (cwd) => prepareHarness(cwd, createOpenCode());
17
+ export const runOpenCodeDoctor = (cwd) => runHarnessDoctor(cwd, 'OpenCode');