agent-hitch 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.
@@ -0,0 +1,361 @@
1
+ import path from "node:path";
2
+ import { HitchError } from "./errors.js";
3
+
4
+ const definitions = {
5
+ codex: {
6
+ id: "codex",
7
+ display_name: "Codex CLI",
8
+ command: "codex",
9
+ path_env: "HITCH_CODEX_PATH",
10
+ version_args: ["--version"],
11
+ revision_sources: {
12
+ version: { type: "npm", package: "@openai/codex", bin: "codex" },
13
+ commit: {
14
+ type: "git",
15
+ url: "https://github.com/openai/codex.git",
16
+ commands: [
17
+ { executable: "cargo", args: ["build", "--release", "--locked", "--bin", "codex"], cwd: "codex-rs" },
18
+ ],
19
+ entrypoint: "codex-rs/target/release/codex",
20
+ },
21
+ },
22
+ capabilities: {
23
+ non_interactive: true,
24
+ streaming: true,
25
+ structured_messages: true,
26
+ structured_tool_events: true,
27
+ sessions: true,
28
+ resume: false,
29
+ model_selection: true,
30
+ graceful_cancel: true,
31
+ },
32
+ process(request, executable, runtime = {}) {
33
+ const args = ["exec", "--json"];
34
+ if (codexSupportsEphemeral(runtime.observed_version)) args.push("--ephemeral");
35
+ args.push("--skip-git-repo-check", "--color", "never", "-C", request.cwd);
36
+ if (request.model) args.push("--model", request.model);
37
+ args.push(...request.agent_args, "-");
38
+ return { executable, args, input: request.prompt };
39
+ },
40
+ translate(event) {
41
+ if (event.type === "thread.started") {
42
+ return [{ type: "session.created", session_id: event.thread_id }];
43
+ }
44
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
45
+ return [{ type: "message.delta", text: event.item.text || "" }];
46
+ }
47
+ const toolTypes = new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
48
+ if (event.type === "item.started" && toolTypes.has(event.item?.type)) {
49
+ return [{
50
+ type: "tool.started",
51
+ call_id: event.item.id,
52
+ name: event.item.type,
53
+ native: event.item,
54
+ }];
55
+ }
56
+ if (event.type === "item.completed" && toolTypes.has(event.item?.type)) {
57
+ return [{
58
+ type: "tool.completed",
59
+ call_id: event.item.id,
60
+ name: event.item.type,
61
+ status: event.item.status || "completed",
62
+ native: event.item,
63
+ }];
64
+ }
65
+ if (event.type === "turn.completed" && event.usage) {
66
+ return [{ type: "usage.updated", usage: event.usage }];
67
+ }
68
+ if (event.type === "error") {
69
+ return [{ type: "diagnostic", level: "error", message: event.message || "Codex error" }];
70
+ }
71
+ return [{ type: "provider.event", provider_type: event.type || "unknown", native: event }];
72
+ },
73
+ },
74
+ claude: {
75
+ id: "claude",
76
+ display_name: "Claude Code",
77
+ command: "claude",
78
+ path_env: "HITCH_CLAUDE_PATH",
79
+ version_args: ["--version"],
80
+ revision_sources: {
81
+ version: { type: "npm", package: "@anthropic-ai/claude-code", bin: "claude" },
82
+ },
83
+ capabilities: {
84
+ non_interactive: true,
85
+ streaming: true,
86
+ structured_messages: true,
87
+ structured_tool_events: true,
88
+ sessions: true,
89
+ resume: false,
90
+ model_selection: true,
91
+ graceful_cancel: true,
92
+ },
93
+ process(request, executable) {
94
+ const args = ["-p", "--output-format", "stream-json", "--verbose"];
95
+ if (request.model) args.push("--model", request.model);
96
+ args.push(...request.agent_args);
97
+ return { executable, args, input: request.prompt };
98
+ },
99
+ translate(event) {
100
+ if (event.type === "system" && event.session_id) {
101
+ return [{ type: "session.created", session_id: event.session_id }];
102
+ }
103
+ if (event.type === "assistant") {
104
+ const content = event.message?.content || [];
105
+ return content.flatMap((block) => {
106
+ if (block.type === "text") return [{ type: "message.delta", text: block.text || "" }];
107
+ if (block.type === "tool_use") {
108
+ return [{ type: "tool.started", call_id: block.id, name: block.name, input: block.input }];
109
+ }
110
+ return [];
111
+ });
112
+ }
113
+ if (event.type === "user") {
114
+ const content = event.message?.content || [];
115
+ return content.flatMap((block) => {
116
+ if (block.type !== "tool_result") return [];
117
+ return [{
118
+ type: "tool.completed",
119
+ call_id: block.tool_use_id,
120
+ status: block.is_error ? "failed" : "succeeded",
121
+ output: claudeToolResultText(block.content),
122
+ }];
123
+ });
124
+ }
125
+ if (event.type === "result") {
126
+ const translated = [];
127
+ if (typeof event.result === "string") translated.push({ type: "message.completed", text: event.result });
128
+ if (event.usage) translated.push({ type: "usage.updated", usage: event.usage });
129
+ return translated;
130
+ }
131
+ return [{ type: "provider.event", provider_type: event.type || "unknown", native: event }];
132
+ },
133
+ },
134
+ pi: {
135
+ id: "pi",
136
+ display_name: "Pi Coding Agent",
137
+ command: "pi",
138
+ path_env: "HITCH_PI_PATH",
139
+ version_args: ["--version"],
140
+ revision_sources: {
141
+ version: {
142
+ type: "npm",
143
+ packages: ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"],
144
+ bin: "pi",
145
+ },
146
+ commit: {
147
+ type: "git",
148
+ url: "https://github.com/earendil-works/pi.git",
149
+ commands: [
150
+ { executable: "npm", args: ["ci", "--ignore-scripts"] },
151
+ { executable: "npm", args: ["run", "build"] },
152
+ ],
153
+ entrypoint: "packages/coding-agent/dist/cli.js",
154
+ },
155
+ },
156
+ capabilities: {
157
+ non_interactive: true,
158
+ streaming: true,
159
+ structured_messages: true,
160
+ structured_tool_events: true,
161
+ sessions: true,
162
+ resume: false,
163
+ model_selection: true,
164
+ graceful_cancel: true,
165
+ },
166
+ process(request, executable) {
167
+ const args = ["--mode", "json", "--no-session"];
168
+ if (request.model) args.push("--model", request.model);
169
+ args.push(...request.agent_args);
170
+ return { executable, args, input: request.prompt };
171
+ },
172
+ translate(event) {
173
+ if (event.type === "session" && event.id) {
174
+ return [{ type: "session.created", session_id: event.id }];
175
+ }
176
+ if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
177
+ return [{ type: "message.delta", text: event.assistantMessageEvent.delta || "" }];
178
+ }
179
+ if (event.type === "message_end" && event.message?.role === "assistant") {
180
+ const translated = [{ type: "message.completed", text: assistantMessageText(event.message) }];
181
+ if (event.message.usage) translated.push({ type: "usage.updated", usage: event.message.usage });
182
+ if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
183
+ translated.push({
184
+ type: "diagnostic",
185
+ level: "error",
186
+ message: event.message.errorMessage || `Pi request ${event.message.stopReason}`,
187
+ });
188
+ }
189
+ return translated;
190
+ }
191
+ if (event.type === "tool_execution_start") {
192
+ return [{
193
+ type: "tool.started",
194
+ call_id: event.toolCallId,
195
+ name: event.toolName,
196
+ input: event.args,
197
+ }];
198
+ }
199
+ if (event.type === "tool_execution_end") {
200
+ return [{
201
+ type: "tool.completed",
202
+ call_id: event.toolCallId,
203
+ name: event.toolName,
204
+ status: event.isError ? "failed" : "succeeded",
205
+ output: event.result,
206
+ }];
207
+ }
208
+ return [{ type: "provider.event", provider_type: event.type || "unknown", native: event }];
209
+ },
210
+ },
211
+ opencode: {
212
+ id: "opencode",
213
+ display_name: "OpenCode",
214
+ command: "opencode",
215
+ path_env: "HITCH_OPENCODE_PATH",
216
+ version_args: ["--version"],
217
+ revision_sources: {
218
+ version: { type: "npm", package: "opencode-ai", bin: "opencode" },
219
+ },
220
+ capabilities: {
221
+ non_interactive: true,
222
+ streaming: true,
223
+ structured_messages: true,
224
+ structured_tool_events: true,
225
+ sessions: true,
226
+ resume: false,
227
+ model_selection: true,
228
+ graceful_cancel: true,
229
+ },
230
+ process(request, executable) {
231
+ const args = ["run", "--format", "json", "--dir", request.cwd];
232
+ if (request.model) args.push("--model", request.model);
233
+ args.push(...request.agent_args);
234
+ return { executable, args, input: request.prompt };
235
+ },
236
+ translate(event, state = {}) {
237
+ const translated = [];
238
+ if (event.sessionID && state.session_id !== event.sessionID) {
239
+ state.session_id = event.sessionID;
240
+ translated.push({ type: "session.created", session_id: event.sessionID });
241
+ }
242
+ if (event.type === "text") {
243
+ translated.push({
244
+ type: "message.delta",
245
+ text: typeof event.part?.text === "string" ? event.part.text : event.text || "",
246
+ });
247
+ return translated;
248
+ }
249
+ if (event.type === "tool_use" && event.part) {
250
+ const failed = event.part.state?.status === "error";
251
+ translated.push({
252
+ type: "tool.completed",
253
+ call_id: event.part.callID || event.part.id,
254
+ name: event.part.tool,
255
+ status: failed ? "failed" : "succeeded",
256
+ input: event.part.state?.input,
257
+ output: failed ? event.part.state?.error : event.part.state?.output,
258
+ native: event.part,
259
+ });
260
+ return translated;
261
+ }
262
+ if (event.type === "step_finish" && event.part?.tokens) {
263
+ translated.push({
264
+ type: "usage.updated",
265
+ usage: { ...event.part.tokens, cost: event.part.cost },
266
+ });
267
+ return translated;
268
+ }
269
+ if (event.type === "error") {
270
+ translated.push({ type: "diagnostic", level: "error", message: openCodeErrorMessage(event.error) });
271
+ return translated;
272
+ }
273
+ translated.push({ type: "provider.event", provider_type: event.type || "unknown", native: event });
274
+ return translated;
275
+ },
276
+ },
277
+ };
278
+
279
+ function codexSupportsEphemeral(observedVersion) {
280
+ const match = String(observedVersion || "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
281
+ if (!match) return false;
282
+ const [, major, minor] = match.map(Number);
283
+ return major > 0 || minor >= 99;
284
+ }
285
+
286
+ export function listDefinitions() {
287
+ return Object.values(definitions).map(publicDefinition);
288
+ }
289
+
290
+ function claudeToolResultText(content) {
291
+ if (typeof content === "string") return content;
292
+ if (!Array.isArray(content)) return content == null ? "" : JSON.stringify(content);
293
+ return content.map((block) => {
294
+ if (typeof block === "string") return block;
295
+ if (block?.type === "text") return block.text || "";
296
+ return JSON.stringify(block);
297
+ }).join("");
298
+ }
299
+
300
+ function assistantMessageText(message) {
301
+ if (!Array.isArray(message?.content)) return "";
302
+ return message.content
303
+ .filter((block) => block?.type === "text")
304
+ .map((block) => block.text || "")
305
+ .join("");
306
+ }
307
+
308
+ function openCodeErrorMessage(error) {
309
+ if (typeof error === "string") return error;
310
+ if (typeof error?.data?.message === "string") return error.data.message;
311
+ if (typeof error?.message === "string") return error.message;
312
+ if (typeof error?.name === "string") return error.name;
313
+ return error == null ? "OpenCode error" : JSON.stringify(error);
314
+ }
315
+
316
+ export function getAdapter(id) {
317
+ const adapter = definitions[id];
318
+ if (!adapter) {
319
+ throw new HitchError(`unknown harness: ${id}`, { code: "harness_not_found", exitCode: 3 });
320
+ }
321
+ return adapter;
322
+ }
323
+
324
+ export function publicDefinition(definition) {
325
+ const revisionSources = definition.revision_sources || {};
326
+ return {
327
+ id: definition.id,
328
+ display_name: definition.display_name,
329
+ command: definition.command,
330
+ path_env: definition.path_env,
331
+ capabilities: definition.capabilities,
332
+ revision_selectors: ["installed", ...Object.keys(revisionSources)],
333
+ revision_sources: Object.fromEntries(Object.entries(revisionSources).map(([selector, source]) => [
334
+ selector,
335
+ {
336
+ type: source.type,
337
+ ...(source.package ? { package: source.package } : {}),
338
+ ...(source.packages ? { packages: source.packages } : {}),
339
+ ...(source.url ? { url: source.url } : {}),
340
+ },
341
+ ])),
342
+ };
343
+ }
344
+
345
+ export function normalizeRequest(input) {
346
+ const cwd = path.resolve(typeof input?.cwd === "string" && input.cwd ? input.cwd : process.cwd());
347
+ const harnessRef = typeof input?.harness_ref === "string"
348
+ ? input.harness_ref.trim()
349
+ : typeof input?.agent === "string" && input.agent.trim()
350
+ ? `${input.agent.trim()}@installed`
351
+ : "";
352
+ return {
353
+ harness_ref: harnessRef,
354
+ model: typeof input?.model === "string" ? input.model : "",
355
+ cwd,
356
+ workspace_mode: typeof input?.workspace_mode === "string" ? input.workspace_mode : "shared",
357
+ prompt: typeof input?.prompt === "string" ? input.prompt : "",
358
+ timeout_ms: input?.timeout_ms ?? 0,
359
+ agent_args: Array.isArray(input?.agent_args) ? [...input.agent_args] : [],
360
+ };
361
+ }