@rallycry/conveyor-agent 7.3.9 → 8.0.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,919 @@
1
+ // src/harness/types.ts
2
+ function defineTool(name, description, schema, handler, options) {
3
+ return {
4
+ name,
5
+ description,
6
+ schema,
7
+ handler,
8
+ annotations: options?.annotations
9
+ };
10
+ }
11
+
12
+ // src/harness/claude-code/index.ts
13
+ import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
14
+ var ClaudeCodeHarness = class {
15
+ async *executeQuery(opts) {
16
+ const sdkEvents = query({
17
+ prompt: opts.prompt,
18
+ options: {
19
+ ...opts.options,
20
+ ...opts.resume ? { resume: opts.resume } : {},
21
+ ...opts.options.sessionId ? { sessionId: opts.options.sessionId } : {},
22
+ ...opts.options.abortController ? { abortController: opts.options.abortController } : {}
23
+ }
24
+ });
25
+ for await (const event of sdkEvents) {
26
+ yield event;
27
+ }
28
+ }
29
+ createMcpServer(config) {
30
+ const sdkTools = config.tools.map(
31
+ (t) => tool(
32
+ t.name,
33
+ t.description,
34
+ t.schema,
35
+ t.handler,
36
+ t.annotations ? { annotations: t.annotations } : void 0
37
+ )
38
+ );
39
+ return createSdkMcpServer({ name: config.name, tools: sdkTools });
40
+ }
41
+ };
42
+
43
+ // src/harness/pty/session.ts
44
+ import { mkdtemp, mkdir as mkdir2, rm, stat } from "fs/promises";
45
+ import { tmpdir } from "os";
46
+ import { join as join3, dirname } from "path";
47
+
48
+ // src/harness/pty/event-queue.ts
49
+ var AsyncEventQueue = class {
50
+ items = [];
51
+ closed = false;
52
+ wake = null;
53
+ push(item) {
54
+ if (this.closed) return;
55
+ this.items.push(item);
56
+ this.signal();
57
+ }
58
+ close() {
59
+ this.closed = true;
60
+ this.signal();
61
+ }
62
+ get isClosed() {
63
+ return this.closed;
64
+ }
65
+ signal() {
66
+ const wake = this.wake;
67
+ if (wake) {
68
+ this.wake = null;
69
+ wake();
70
+ }
71
+ }
72
+ async *drain() {
73
+ while (!this.closed || this.items.length > 0) {
74
+ if (this.items.length > 0) {
75
+ const item = this.items.shift();
76
+ if (item !== void 0) yield item;
77
+ continue;
78
+ }
79
+ await new Promise((resolve) => {
80
+ this.wake = resolve;
81
+ });
82
+ }
83
+ }
84
+ };
85
+
86
+ // src/harness/pty/hook-socket.ts
87
+ import { createServer } from "net";
88
+ import { unlink } from "fs/promises";
89
+ function parseEnvelope(line) {
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(line);
93
+ } catch {
94
+ return null;
95
+ }
96
+ if (typeof parsed !== "object" || parsed === null) return null;
97
+ const record = parsed;
98
+ const result = {};
99
+ if (typeof record.tool_name === "string") result.tool_name = record.tool_name;
100
+ if (typeof record.elapsed_time_seconds === "number") {
101
+ result.elapsed_time_seconds = record.elapsed_time_seconds;
102
+ }
103
+ return result;
104
+ }
105
+ var HookSocketServer = class {
106
+ constructor(socketPath, onProgress) {
107
+ this.socketPath = socketPath;
108
+ this.onProgress = onProgress;
109
+ }
110
+ socketPath;
111
+ onProgress;
112
+ server = null;
113
+ buffer = "";
114
+ _closed = false;
115
+ async listen() {
116
+ await unlink(this.socketPath).catch(() => void 0);
117
+ await new Promise((resolve, reject) => {
118
+ const server = createServer((socket) => this.handleConnection(socket));
119
+ server.on("error", reject);
120
+ server.listen(this.socketPath, () => {
121
+ resolve();
122
+ });
123
+ this.server = server;
124
+ });
125
+ }
126
+ get isClosed() {
127
+ return this._closed;
128
+ }
129
+ async close() {
130
+ if (this._closed) return;
131
+ this._closed = true;
132
+ const server = this.server;
133
+ this.server = null;
134
+ if (server) {
135
+ await new Promise((resolve) => {
136
+ server.close(() => {
137
+ resolve();
138
+ });
139
+ });
140
+ }
141
+ await unlink(this.socketPath).catch(() => void 0);
142
+ }
143
+ handleConnection(socket) {
144
+ socket.on("error", () => void 0);
145
+ socket.on("data", (chunk) => {
146
+ this.buffer += chunk.toString("utf8");
147
+ this.ingest();
148
+ });
149
+ }
150
+ ingest() {
151
+ let index = this.buffer.indexOf("\n");
152
+ while (index >= 0) {
153
+ const line = this.buffer.slice(0, index);
154
+ this.buffer = this.buffer.slice(index + 1);
155
+ const progress = parseEnvelope(line);
156
+ if (progress) this.onProgress(progress);
157
+ index = this.buffer.indexOf("\n");
158
+ }
159
+ }
160
+ };
161
+
162
+ // src/harness/pty/jsonl-tailer.ts
163
+ import { open } from "fs/promises";
164
+
165
+ // src/harness/pty/record-mapper.ts
166
+ function isRecord(value) {
167
+ return typeof value === "object" && value !== null;
168
+ }
169
+ function isUnknownArray(value) {
170
+ return Array.isArray(value);
171
+ }
172
+ function stringField(record, ...keys) {
173
+ for (const key of keys) {
174
+ const value = record[key];
175
+ if (typeof value === "string") return value;
176
+ }
177
+ return void 0;
178
+ }
179
+ function numberField(record, ...keys) {
180
+ for (const key of keys) {
181
+ const value = record[key];
182
+ if (typeof value === "number") return value;
183
+ }
184
+ return void 0;
185
+ }
186
+ function mapSystem(record) {
187
+ if (record.subtype !== "init") return null;
188
+ const event = {
189
+ type: "system",
190
+ subtype: "init",
191
+ model: stringField(record, "model") ?? ""
192
+ };
193
+ const sessionId = stringField(record, "session_id", "sessionId");
194
+ if (sessionId !== void 0) event.session_id = sessionId;
195
+ return event;
196
+ }
197
+ function mapUsage(message) {
198
+ const usage = message.usage;
199
+ if (!isRecord(usage)) return void 0;
200
+ const result = {};
201
+ const input = numberField(usage, "input_tokens");
202
+ const cacheRead = numberField(usage, "cache_read_input_tokens");
203
+ const cacheCreation = numberField(usage, "cache_creation_input_tokens");
204
+ if (input !== void 0) result.input_tokens = input;
205
+ if (cacheRead !== void 0) result.cache_read_input_tokens = cacheRead;
206
+ if (cacheCreation !== void 0) result.cache_creation_input_tokens = cacheCreation;
207
+ return result;
208
+ }
209
+ function mapContentBlock(raw) {
210
+ if (!isRecord(raw)) return null;
211
+ const type = stringField(raw, "type");
212
+ if (type === void 0) return null;
213
+ const block = { type };
214
+ const text = stringField(raw, "text");
215
+ if (text !== void 0) block.text = text;
216
+ const name = stringField(raw, "name");
217
+ if (name !== void 0) block.name = name;
218
+ const id = stringField(raw, "id");
219
+ if (id !== void 0) block.id = id;
220
+ if ("input" in raw) block.input = raw.input;
221
+ return block;
222
+ }
223
+ function mapAssistant(record) {
224
+ const message = record.message;
225
+ if (!isRecord(message)) return null;
226
+ const rawContent = isUnknownArray(message.content) ? message.content : [];
227
+ const content = [];
228
+ for (const item of rawContent) {
229
+ const block = mapContentBlock(item);
230
+ if (block) content.push(block);
231
+ }
232
+ const result = {
233
+ type: "assistant",
234
+ message: { role: "assistant", content }
235
+ };
236
+ const usage = mapUsage(message);
237
+ if (usage !== void 0) result.message.usage = usage;
238
+ return result;
239
+ }
240
+ function mapResultSuccess(record) {
241
+ const event = {
242
+ type: "result",
243
+ subtype: "success",
244
+ result: stringField(record, "result") ?? "",
245
+ total_cost_usd: numberField(record, "total_cost_usd", "totalCostUsd") ?? 0
246
+ };
247
+ const modelUsage = record.modelUsage;
248
+ if (isRecord(modelUsage)) event.modelUsage = modelUsage;
249
+ const sessionId = stringField(record, "session_id", "sessionId");
250
+ if (sessionId !== void 0) event.sessionId = sessionId;
251
+ return event;
252
+ }
253
+ function mapResultError(record) {
254
+ const rawErrors = isUnknownArray(record.errors) ? record.errors : [];
255
+ const errors = rawErrors.filter((e) => typeof e === "string");
256
+ const event = { type: "result", subtype: "error", errors };
257
+ const sessionId = stringField(record, "session_id", "sessionId");
258
+ if (sessionId !== void 0) event.sessionId = sessionId;
259
+ return event;
260
+ }
261
+ function mapResult(record) {
262
+ if (record.subtype === "success") return mapResultSuccess(record);
263
+ if (record.subtype === "error") return mapResultError(record);
264
+ return null;
265
+ }
266
+ function mapTranscriptRecord(raw) {
267
+ if (!isRecord(raw)) return null;
268
+ switch (raw.type) {
269
+ case "system":
270
+ return mapSystem(raw);
271
+ case "assistant":
272
+ return mapAssistant(raw);
273
+ case "result":
274
+ return mapResult(raw);
275
+ default:
276
+ return null;
277
+ }
278
+ }
279
+
280
+ // src/harness/pty/jsonl-tailer.ts
281
+ var POLL_INTERVAL_MS = 25;
282
+ var JsonlTailer = class {
283
+ constructor(path, onEvent) {
284
+ this.path = path;
285
+ this.onEvent = onEvent;
286
+ }
287
+ path;
288
+ onEvent;
289
+ offset = 0;
290
+ buffer = "";
291
+ timer = null;
292
+ chain = Promise.resolve();
293
+ _closed = false;
294
+ /**
295
+ * Begin tailing. Pass `fromOffset` to skip bytes already present when the
296
+ * tail starts (used on resume so the prior session's records aren't replayed).
297
+ */
298
+ start(fromOffset = 0) {
299
+ if (this.timer) return;
300
+ this.offset = fromOffset;
301
+ this.timer = setInterval(() => {
302
+ if (this._closed) return;
303
+ void this.enqueueRead();
304
+ }, POLL_INTERVAL_MS);
305
+ }
306
+ close() {
307
+ this._closed = true;
308
+ if (this.timer) {
309
+ clearInterval(this.timer);
310
+ this.timer = null;
311
+ }
312
+ }
313
+ /** Read any remaining bytes and flush a trailing line without a newline. */
314
+ async flush() {
315
+ await this.enqueueRead();
316
+ if (this.buffer.length > 0) {
317
+ this.emitLine(this.buffer);
318
+ this.buffer = "";
319
+ }
320
+ }
321
+ enqueueRead() {
322
+ this.chain = this.chain.then(() => this.readOnce());
323
+ return this.chain;
324
+ }
325
+ async readOnce() {
326
+ let handle = null;
327
+ try {
328
+ handle = await open(this.path, "r");
329
+ const stats = await handle.stat();
330
+ if (stats.size <= this.offset) return;
331
+ const length = stats.size - this.offset;
332
+ const buf = Buffer.alloc(length);
333
+ await handle.read(buf, 0, length, this.offset);
334
+ this.offset = stats.size;
335
+ this.consume(buf.toString("utf8"));
336
+ } catch {
337
+ } finally {
338
+ if (handle) await handle.close();
339
+ }
340
+ }
341
+ consume(chunk) {
342
+ this.buffer += chunk;
343
+ let index = this.buffer.indexOf("\n");
344
+ while (index >= 0) {
345
+ const line = this.buffer.slice(0, index);
346
+ this.buffer = this.buffer.slice(index + 1);
347
+ this.emitLine(line);
348
+ index = this.buffer.indexOf("\n");
349
+ }
350
+ }
351
+ emitLine(line) {
352
+ const trimmed = line.trim();
353
+ if (trimmed.length === 0) return;
354
+ let parsed;
355
+ try {
356
+ parsed = JSON.parse(trimmed);
357
+ } catch {
358
+ return;
359
+ }
360
+ const event = mapTranscriptRecord(parsed);
361
+ if (event) this.onEvent(event);
362
+ }
363
+ };
364
+
365
+ // src/harness/pty/spawn-args.ts
366
+ function resolveClaudeBinary() {
367
+ return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
368
+ }
369
+ function buildSpawnArgs(input) {
370
+ const args = [];
371
+ if (input.resume) {
372
+ args.push("--resume", input.resume);
373
+ } else if (input.sessionId) {
374
+ args.push("--session-id", input.sessionId);
375
+ }
376
+ args.push("--model", input.model);
377
+ if (input.permissionMode === "bypassPermissions") {
378
+ args.push("--dangerously-skip-permissions");
379
+ } else {
380
+ args.push("--permission-mode", "plan");
381
+ }
382
+ args.push("--settings", input.settingsPath);
383
+ if (input.mcpConfigPath) {
384
+ args.push("--mcp-config", input.mcpConfigPath);
385
+ if (input.strictMcpConfig) {
386
+ args.push("--strict-mcp-config");
387
+ }
388
+ }
389
+ return args;
390
+ }
391
+
392
+ // src/harness/pty/settings.ts
393
+ import { mkdir, writeFile, chmod } from "fs/promises";
394
+ import { homedir } from "os";
395
+ import { join } from "path";
396
+ function claudeConfigHome() {
397
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
398
+ }
399
+ function projectSlug(cwd) {
400
+ return cwd.replace(/\//g, "-");
401
+ }
402
+ function sessionTranscriptPath(cwd, sessionId) {
403
+ return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
404
+ }
405
+ var HOOK_HELPER_SOURCE = `"use strict";
406
+ const net = require("node:net");
407
+
408
+ let raw = "";
409
+ process.stdin.setEncoding("utf8");
410
+ process.stdin.on("data", (chunk) => {
411
+ raw += chunk;
412
+ });
413
+ process.stdin.on("end", () => {
414
+ let toolName = "";
415
+ try {
416
+ const parsed = JSON.parse(raw);
417
+ if (parsed && typeof parsed.tool_name === "string") toolName = parsed.tool_name;
418
+ } catch {
419
+ toolName = "";
420
+ }
421
+ relay(toolName);
422
+ });
423
+
424
+ function relay(toolName) {
425
+ const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
426
+ let done = false;
427
+ const finish = () => {
428
+ if (done) return;
429
+ done = true;
430
+ process.stdout.write(JSON.stringify({ continue: true }));
431
+ process.exit(0);
432
+ };
433
+ const fallback = setTimeout(finish, 250);
434
+ if (!socketPath) {
435
+ clearTimeout(fallback);
436
+ finish();
437
+ return;
438
+ }
439
+ const client = net.connect(socketPath, () => {
440
+ // PostToolUse does not supply tool duration, so elapsed_time_seconds is
441
+ // omitted rather than reported as a misleading 0 (the field is optional).
442
+ const line = JSON.stringify({ tool_name: toolName }) + "\\n";
443
+ client.write(line, () => {
444
+ client.end();
445
+ });
446
+ });
447
+ client.on("close", () => {
448
+ clearTimeout(fallback);
449
+ finish();
450
+ });
451
+ client.on("error", () => {
452
+ clearTimeout(fallback);
453
+ finish();
454
+ });
455
+ }
456
+ `;
457
+ async function writeHookSettings(dir) {
458
+ const helperPath = join(dir, "hook-helper.cjs");
459
+ const settingsPath = join(dir, "settings.json");
460
+ await mkdir(dir, { recursive: true });
461
+ await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
462
+ await chmod(helperPath, 493);
463
+ const settings = {
464
+ // Auto-approve every tool call so the interactive (PTY) agent never stops to
465
+ // prompt the viewer. This only suppresses the per-tool approval prompt — it
466
+ // does NOT relax the planning gate: in discovery/plan mode the spawn passes
467
+ // `--permission-mode plan`, which keeps the agent read-only (no edits/commits
468
+ // until it exits plan mode) regardless of this allow-list. In build mode the
469
+ // spawn already bypasses prompts via `--dangerously-skip-permissions`.
470
+ // `mcp__conveyor` is listed explicitly so the agent's Conveyor tools are
471
+ // covered even if the `*` wildcard doesn't match MCP tool names.
472
+ permissions: {
473
+ allow: ["*", "mcp__conveyor"]
474
+ },
475
+ hooks: {
476
+ PostToolUse: [
477
+ {
478
+ matcher: "*",
479
+ hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
480
+ }
481
+ ]
482
+ }
483
+ };
484
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
485
+ return { settingsPath, helperPath };
486
+ }
487
+
488
+ // src/harness/pty/tool-server.ts
489
+ import { createServer as createServer2 } from "http";
490
+ import { writeFile as writeFile2 } from "fs/promises";
491
+ import { join as join2 } from "path";
492
+ import { randomBytes } from "crypto";
493
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
494
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
495
+
496
+ // src/harness/pty/mcp-server.ts
497
+ var PtyMcpServer = class {
498
+ constructor(name, tools) {
499
+ this.name = name;
500
+ this.tools = tools;
501
+ }
502
+ name;
503
+ tools;
504
+ getTool(name) {
505
+ return this.tools.find((tool2) => tool2.name === name);
506
+ }
507
+ invokeTool(name, input) {
508
+ const tool2 = this.getTool(name);
509
+ if (!tool2) return Promise.reject(new Error(`Unknown tool: ${name}`));
510
+ return tool2.handler(input);
511
+ }
512
+ };
513
+
514
+ // src/harness/pty/tool-server.ts
515
+ var LOOPBACK = "127.0.0.1";
516
+ var PtyToolServer = class {
517
+ constructor(name, tools) {
518
+ this.name = name;
519
+ this.tools = tools;
520
+ }
521
+ name;
522
+ tools;
523
+ http = null;
524
+ transport = null;
525
+ mcp = null;
526
+ token = randomBytes(24).toString("base64url");
527
+ async start() {
528
+ const mcp = new McpServer({ name: this.name, version: "1.0.0" });
529
+ const register = mcp.tool.bind(mcp);
530
+ for (const tool2 of this.tools) {
531
+ register(tool2.name, tool2.description, tool2.schema, (args) => tool2.handler(args));
532
+ }
533
+ const transport = new StreamableHTTPServerTransport({
534
+ sessionIdGenerator: () => randomBytes(16).toString("hex"),
535
+ enableJsonResponse: true
536
+ });
537
+ await mcp.connect(transport);
538
+ const server = createServer2((req, res) => {
539
+ void this.handle(req, res, transport);
540
+ });
541
+ await new Promise((resolve, reject) => {
542
+ server.once("error", reject);
543
+ server.listen(0, LOOPBACK, () => resolve());
544
+ });
545
+ const address = server.address();
546
+ const port = address && typeof address === "object" ? address.port : 0;
547
+ this.http = server;
548
+ this.transport = transport;
549
+ this.mcp = mcp;
550
+ return { url: `http://${LOOPBACK}:${port}/mcp`, token: this.token };
551
+ }
552
+ async handle(req, res, transport) {
553
+ if (req.headers.authorization !== `Bearer ${this.token}`) {
554
+ res.writeHead(401).end();
555
+ return;
556
+ }
557
+ try {
558
+ await transport.handleRequest(req, res);
559
+ } catch {
560
+ if (res.headersSent) res.end();
561
+ else res.writeHead(500).end();
562
+ }
563
+ }
564
+ async close() {
565
+ try {
566
+ await this.transport?.close();
567
+ } catch {
568
+ }
569
+ this.transport = null;
570
+ try {
571
+ await this.mcp?.close();
572
+ } catch {
573
+ }
574
+ this.mcp = null;
575
+ const http = this.http;
576
+ this.http = null;
577
+ if (http) {
578
+ await new Promise((resolve) => {
579
+ http.close(() => resolve());
580
+ });
581
+ }
582
+ }
583
+ };
584
+ async function startToolServers(mcpServers, tempDir) {
585
+ const servers = [];
586
+ const config = {};
587
+ for (const [name, handle] of Object.entries(mcpServers)) {
588
+ const tools = handle instanceof PtyMcpServer ? handle.tools : [];
589
+ if (tools.length === 0) continue;
590
+ const server = new PtyToolServer(name, tools);
591
+ const { url, token } = await server.start();
592
+ servers.push(server);
593
+ config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
594
+ }
595
+ if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
596
+ const mcpConfigPath = join2(tempDir, "mcp-config.json");
597
+ await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
598
+ return { servers, mcpConfigPath };
599
+ }
600
+
601
+ // src/harness/pty/session.ts
602
+ function isRecord2(value) {
603
+ return typeof value === "object" && value !== null;
604
+ }
605
+ function extractSpawn(mod) {
606
+ if (!isRecord2(mod)) return null;
607
+ if (typeof mod.spawn === "function") return mod.spawn;
608
+ const def = mod.default;
609
+ if (isRecord2(def) && typeof def.spawn === "function") return def.spawn;
610
+ return null;
611
+ }
612
+ async function loadPtySpawn() {
613
+ const mod = await import("node-pty");
614
+ const spawn = extractSpawn(mod);
615
+ if (!spawn) throw new Error("node-pty: spawn export not found");
616
+ return spawn;
617
+ }
618
+ function inheritedEnv(socketPath) {
619
+ const env = {};
620
+ for (const [key, value] of Object.entries(process.env)) {
621
+ if (typeof value === "string") env[key] = value;
622
+ }
623
+ env.CONVEYOR_HOOK_SOCKET = socketPath;
624
+ return env;
625
+ }
626
+ async function transcriptSize(path) {
627
+ try {
628
+ return (await stat(path)).size;
629
+ } catch {
630
+ return 0;
631
+ }
632
+ }
633
+ var PtySession = class {
634
+ constructor(prompt, options, resume, bridge) {
635
+ this.prompt = prompt;
636
+ this.options = options;
637
+ this.resume = resume;
638
+ this.bridge = bridge;
639
+ }
640
+ prompt;
641
+ options;
642
+ resume;
643
+ bridge;
644
+ queue = new AsyncEventQueue();
645
+ socket = null;
646
+ tailer = null;
647
+ pty = null;
648
+ tempDir = "";
649
+ sawResult = false;
650
+ _toreDown = false;
651
+ exitListeners = [];
652
+ idleListeners = [];
653
+ abortHandler = null;
654
+ cols = 120;
655
+ rows = 40;
656
+ unsubInput = null;
657
+ unsubResize = null;
658
+ // In-process HTTP MCP servers exposing the harness tools to the spawned CLI,
659
+ // plus the path to the `--mcp-config` that points at them.
660
+ toolServers = [];
661
+ mcpConfigPath = null;
662
+ onIdle(listener) {
663
+ this.idleListeners.push(listener);
664
+ }
665
+ onExit(listener) {
666
+ this.exitListeners.push(listener);
667
+ }
668
+ get isToreDown() {
669
+ return this._toreDown;
670
+ }
671
+ get hookSocketPath() {
672
+ return this.socket ? this.socket.socketPath : null;
673
+ }
674
+ events() {
675
+ return this.queue.drain();
676
+ }
677
+ async start() {
678
+ const sessionId = this.resume ?? this.options.sessionId;
679
+ if (!sessionId) {
680
+ throw new Error("PtySession requires options.sessionId or a resume target");
681
+ }
682
+ const signal = this.options.abortController?.signal;
683
+ if (signal?.aborted) {
684
+ await this.teardown();
685
+ return;
686
+ }
687
+ try {
688
+ this.tempDir = await mkdtemp(join3(tmpdir(), "conveyor-pty-"));
689
+ const socketPath = join3(this.tempDir, "hook.sock");
690
+ this.socket = new HookSocketServer(socketPath, (progress) => this.handleProgress(progress));
691
+ await this.socket.listen();
692
+ const { settingsPath } = await writeHookSettings(this.tempDir);
693
+ await this.setupToolServers();
694
+ const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
695
+ await mkdir2(dirname(transcriptPath), { recursive: true });
696
+ const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
697
+ this.tailer = new JsonlTailer(transcriptPath, (event) => this.handleTranscriptEvent(event));
698
+ this.tailer.start(startOffset);
699
+ await this.spawn(settingsPath, socketPath);
700
+ if (signal) {
701
+ this.abortHandler = () => {
702
+ void this.teardown();
703
+ };
704
+ signal.addEventListener("abort", this.abortHandler, { once: true });
705
+ if (signal.aborted) {
706
+ await this.teardown();
707
+ return;
708
+ }
709
+ }
710
+ if (this.bridge) {
711
+ this.unsubInput = this.bridge.onInput((data) => this.writeStdin(data));
712
+ this.unsubResize = this.bridge.onResize((cols, rows) => this.resizePty(cols, rows));
713
+ }
714
+ await this.feedPrompt();
715
+ } catch (err) {
716
+ await this.teardown();
717
+ throw err;
718
+ }
719
+ }
720
+ writeStdin(text) {
721
+ this.pty?.write(text);
722
+ }
723
+ /** Apply a relayed resize to the live pty (reconciled dims from the server). */
724
+ resizePty(cols, rows) {
725
+ if (cols <= 0 || rows <= 0) return;
726
+ this.cols = cols;
727
+ this.rows = rows;
728
+ try {
729
+ this.pty?.resize(cols, rows);
730
+ } catch {
731
+ }
732
+ }
733
+ async teardown() {
734
+ if (this._toreDown) return;
735
+ this._toreDown = true;
736
+ this.unsubInput?.();
737
+ this.unsubInput = null;
738
+ this.unsubResize?.();
739
+ this.unsubResize = null;
740
+ if (this.abortHandler) {
741
+ this.options.abortController?.signal.removeEventListener("abort", this.abortHandler);
742
+ this.abortHandler = null;
743
+ }
744
+ try {
745
+ this.pty?.kill();
746
+ } catch {
747
+ }
748
+ this.pty = null;
749
+ this.tailer?.close();
750
+ this.tailer = null;
751
+ if (this.socket) {
752
+ await this.socket.close();
753
+ this.socket = null;
754
+ }
755
+ for (const toolServer of this.toolServers) {
756
+ try {
757
+ await toolServer.close();
758
+ } catch {
759
+ }
760
+ }
761
+ this.toolServers = [];
762
+ this.mcpConfigPath = null;
763
+ this.queue.close();
764
+ if (this.tempDir) {
765
+ await rm(this.tempDir, { recursive: true, force: true });
766
+ this.tempDir = "";
767
+ }
768
+ }
769
+ /**
770
+ * Serve the harness's in-process tools to the spawned CLI over loopback HTTP
771
+ * (handlers run in THIS process against the live task-token connection) and
772
+ * record the `--mcp-config` path for spawn(). No-op when the harness was
773
+ * constructed without tools (e.g. SDK-only callers).
774
+ */
775
+ async setupToolServers() {
776
+ const { servers, mcpConfigPath } = await startToolServers(
777
+ this.options.mcpServers ?? {},
778
+ this.tempDir
779
+ );
780
+ this.toolServers = servers;
781
+ this.mcpConfigPath = mcpConfigPath;
782
+ }
783
+ async spawn(settingsPath, socketPath) {
784
+ const args = buildSpawnArgs({
785
+ resume: this.resume,
786
+ sessionId: this.options.sessionId,
787
+ model: this.options.model,
788
+ permissionMode: this.options.permissionMode,
789
+ settingsPath,
790
+ // Only this config: ignore the user's ~/.claude.json / project .mcp.json
791
+ // so the agent's tool set is deterministic (and a stale personal conveyor
792
+ // server doesn't load).
793
+ ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath, strictMcpConfig: true } : {}
794
+ });
795
+ const spawn = await loadPtySpawn();
796
+ const pty = spawn(resolveClaudeBinary(), args, {
797
+ name: "xterm-color",
798
+ cols: this.cols,
799
+ rows: this.rows,
800
+ cwd: this.options.cwd,
801
+ env: inheritedEnv(socketPath)
802
+ });
803
+ pty.onData((data) => {
804
+ this.bridge?.sendOutput(data, { cols: this.cols, rows: this.rows });
805
+ });
806
+ pty.onExit((event) => {
807
+ void this.finalizeOnExit(event.exitCode);
808
+ });
809
+ this.pty = pty;
810
+ }
811
+ async feedPrompt() {
812
+ if (typeof this.prompt === "string") {
813
+ this.writeStdin(`${this.prompt}\r`);
814
+ return;
815
+ }
816
+ for await (const message of this.prompt) {
817
+ const content = message.message.content;
818
+ const text = typeof content === "string" ? content : JSON.stringify(content);
819
+ this.writeStdin(`${text}\r`);
820
+ }
821
+ }
822
+ handleProgress(progress) {
823
+ this.queue.push({
824
+ type: "tool_progress",
825
+ ...progress.tool_name === void 0 ? {} : { tool_name: progress.tool_name },
826
+ ...progress.elapsed_time_seconds === void 0 ? {} : { elapsed_time_seconds: progress.elapsed_time_seconds }
827
+ });
828
+ }
829
+ handleTranscriptEvent(event) {
830
+ this.queue.push(event);
831
+ if (event.type === "result") {
832
+ this.sawResult = true;
833
+ for (const listener of this.idleListeners) listener();
834
+ this.queue.close();
835
+ }
836
+ }
837
+ async finalizeOnExit(exitCode) {
838
+ if (this._toreDown) return;
839
+ if (this.tailer) {
840
+ this.tailer.close();
841
+ await this.tailer.flush();
842
+ }
843
+ if (!this.sawResult) {
844
+ this.queue.push({
845
+ type: "result",
846
+ subtype: "error",
847
+ errors: [`claude exited (code ${exitCode}) without a result`]
848
+ });
849
+ }
850
+ for (const listener of this.exitListeners) listener(exitCode);
851
+ this.queue.close();
852
+ }
853
+ };
854
+
855
+ // src/harness/pty/index.ts
856
+ var PtyHarness = class {
857
+ /**
858
+ * `bridge` relays raw terminal I/O to/from the S2 server (and on to the S5
859
+ * terminal). It is undefined for SDK-only callers and PTY runs that never
860
+ * attach a relay; the session simply discards stdout in that case.
861
+ */
862
+ constructor(bridge) {
863
+ this.bridge = bridge;
864
+ }
865
+ bridge;
866
+ async *executeQuery(opts) {
867
+ const session = new PtySession(
868
+ opts.prompt,
869
+ opts.options,
870
+ opts.resume ?? opts.options.resume,
871
+ this.bridge
872
+ );
873
+ await session.start();
874
+ try {
875
+ for await (const event of session.events()) {
876
+ yield event;
877
+ }
878
+ } finally {
879
+ await session.teardown();
880
+ }
881
+ }
882
+ createMcpServer(config) {
883
+ return new PtyMcpServer(config.name, config.tools);
884
+ }
885
+ };
886
+
887
+ // src/harness/index.ts
888
+ function createHarness(kind = "sdk", ptyBridge) {
889
+ return kind === "pty" ? new PtyHarness(ptyBridge) : new ClaudeCodeHarness();
890
+ }
891
+
892
+ // src/utils/logger.ts
893
+ function createServiceLogger(service) {
894
+ const prefix = `[conveyor-agent:${service}]`;
895
+ return {
896
+ info(message, data) {
897
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
898
+ process.stderr.write(`${prefix} ${message}${extra}
899
+ `);
900
+ },
901
+ warn(message, data) {
902
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
903
+ process.stderr.write(`${prefix} WARN ${message}${extra}
904
+ `);
905
+ },
906
+ error(message, data) {
907
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
908
+ process.stderr.write(`${prefix} ERROR ${message}${extra}
909
+ `);
910
+ }
911
+ };
912
+ }
913
+
914
+ export {
915
+ defineTool,
916
+ createHarness,
917
+ createServiceLogger
918
+ };
919
+ //# sourceMappingURL=chunk-3X63JL6C.js.map