@rallycry/conveyor-agent 8.12.0 → 9.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.
- package/dist/{chunk-FSUVTOFP.js → chunk-JH7GUOGW.js} +6673 -8717
- package/dist/chunk-JH7GUOGW.js.map +1 -0
- package/dist/cli.js +148 -208
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -500
- package/dist/index.js +51 -1473
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-FDWECEDJ.js +0 -33
- package/dist/chunk-FDWECEDJ.js.map +0 -1
- package/dist/chunk-FSUVTOFP.js.map +0 -1
- package/dist/chunk-TN2YYT2V.js +0 -1444
- package/dist/chunk-TN2YYT2V.js.map +0 -1
- package/dist/tag-audit-handler-X3LEX63H.js +0 -222
- package/dist/tag-audit-handler-X3LEX63H.js.map +0 -1
- package/dist/task-audit-handler-BRCXB5V6.js +0 -801
- package/dist/task-audit-handler-BRCXB5V6.js.map +0 -1
package/dist/chunk-TN2YYT2V.js
DELETED
|
@@ -1,1444 +0,0 @@
|
|
|
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
|
-
if (record.kind === "pre_tool_use") {
|
|
99
|
-
if (typeof record.id !== "string" || typeof record.tool_name !== "string") return null;
|
|
100
|
-
const toolInput = typeof record.tool_input === "object" && record.tool_input !== null ? record.tool_input : {};
|
|
101
|
-
return {
|
|
102
|
-
kind: "pre_tool_use",
|
|
103
|
-
request: { id: record.id, tool_name: record.tool_name, tool_input: toolInput }
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
const result = {};
|
|
107
|
-
if (typeof record.tool_name === "string") result.tool_name = record.tool_name;
|
|
108
|
-
if (typeof record.elapsed_time_seconds === "number") {
|
|
109
|
-
result.elapsed_time_seconds = record.elapsed_time_seconds;
|
|
110
|
-
}
|
|
111
|
-
return { kind: "progress", progress: result };
|
|
112
|
-
}
|
|
113
|
-
var HookSocketServer = class {
|
|
114
|
-
constructor(socketPath, onProgress, onPreToolUse) {
|
|
115
|
-
this.socketPath = socketPath;
|
|
116
|
-
this.onProgress = onProgress;
|
|
117
|
-
this.onPreToolUse = onPreToolUse;
|
|
118
|
-
}
|
|
119
|
-
socketPath;
|
|
120
|
-
onProgress;
|
|
121
|
-
onPreToolUse;
|
|
122
|
-
server = null;
|
|
123
|
-
_closed = false;
|
|
124
|
-
async listen() {
|
|
125
|
-
await unlink(this.socketPath).catch(() => void 0);
|
|
126
|
-
await new Promise((resolve, reject) => {
|
|
127
|
-
const server = createServer((socket) => this.handleConnection(socket));
|
|
128
|
-
server.on("error", reject);
|
|
129
|
-
server.listen(this.socketPath, () => {
|
|
130
|
-
resolve();
|
|
131
|
-
});
|
|
132
|
-
this.server = server;
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
get isClosed() {
|
|
136
|
-
return this._closed;
|
|
137
|
-
}
|
|
138
|
-
async close() {
|
|
139
|
-
if (this._closed) return;
|
|
140
|
-
this._closed = true;
|
|
141
|
-
const server = this.server;
|
|
142
|
-
this.server = null;
|
|
143
|
-
if (server) {
|
|
144
|
-
await new Promise((resolve) => {
|
|
145
|
-
server.close(() => {
|
|
146
|
-
resolve();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
await unlink(this.socketPath).catch(() => void 0);
|
|
151
|
-
}
|
|
152
|
-
handleConnection(socket) {
|
|
153
|
-
socket.on("error", () => void 0);
|
|
154
|
-
let buffer = "";
|
|
155
|
-
socket.on("data", (chunk) => {
|
|
156
|
-
buffer += chunk.toString("utf8");
|
|
157
|
-
let index = buffer.indexOf("\n");
|
|
158
|
-
while (index >= 0) {
|
|
159
|
-
const line = buffer.slice(0, index);
|
|
160
|
-
buffer = buffer.slice(index + 1);
|
|
161
|
-
this.dispatch(line, socket);
|
|
162
|
-
index = buffer.indexOf("\n");
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
dispatch(line, socket) {
|
|
167
|
-
const envelope = parseEnvelope(line);
|
|
168
|
-
if (!envelope) return;
|
|
169
|
-
if (envelope.kind === "progress") {
|
|
170
|
-
this.onProgress(envelope.progress);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
void this.respondToPreToolUse(envelope.request, socket);
|
|
174
|
-
}
|
|
175
|
-
async respondToPreToolUse(request, socket) {
|
|
176
|
-
let verdict;
|
|
177
|
-
try {
|
|
178
|
-
verdict = this.onPreToolUse ? await this.onPreToolUse(request) : { decision: "allow" };
|
|
179
|
-
} catch (err) {
|
|
180
|
-
verdict = {
|
|
181
|
-
decision: "deny",
|
|
182
|
-
reason: `Conveyor validation failed: ${err instanceof Error ? err.message : String(err)}. Fix the issue and try again.`
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
socket.write(`${JSON.stringify({ id: request.id, ...verdict })}
|
|
187
|
-
`);
|
|
188
|
-
} catch {
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// src/harness/pty/jsonl-tailer.ts
|
|
194
|
-
import { open } from "fs/promises";
|
|
195
|
-
|
|
196
|
-
// src/harness/pty/record-mapper.ts
|
|
197
|
-
function isRecord(value) {
|
|
198
|
-
return typeof value === "object" && value !== null;
|
|
199
|
-
}
|
|
200
|
-
function isUnknownArray(value) {
|
|
201
|
-
return Array.isArray(value);
|
|
202
|
-
}
|
|
203
|
-
function stringField(record, ...keys) {
|
|
204
|
-
for (const key of keys) {
|
|
205
|
-
const value = record[key];
|
|
206
|
-
if (typeof value === "string") return value;
|
|
207
|
-
}
|
|
208
|
-
return void 0;
|
|
209
|
-
}
|
|
210
|
-
function numberField(record, ...keys) {
|
|
211
|
-
for (const key of keys) {
|
|
212
|
-
const value = record[key];
|
|
213
|
-
if (typeof value === "number") return value;
|
|
214
|
-
}
|
|
215
|
-
return void 0;
|
|
216
|
-
}
|
|
217
|
-
function mapSystem(record) {
|
|
218
|
-
if (record.subtype !== "init") return null;
|
|
219
|
-
const event = {
|
|
220
|
-
type: "system",
|
|
221
|
-
subtype: "init",
|
|
222
|
-
model: stringField(record, "model") ?? ""
|
|
223
|
-
};
|
|
224
|
-
const sessionId = stringField(record, "session_id", "sessionId");
|
|
225
|
-
if (sessionId !== void 0) event.session_id = sessionId;
|
|
226
|
-
return event;
|
|
227
|
-
}
|
|
228
|
-
function mapUsage(message) {
|
|
229
|
-
const usage = message.usage;
|
|
230
|
-
if (!isRecord(usage)) return void 0;
|
|
231
|
-
const result = {};
|
|
232
|
-
const input = numberField(usage, "input_tokens");
|
|
233
|
-
const cacheRead = numberField(usage, "cache_read_input_tokens");
|
|
234
|
-
const cacheCreation = numberField(usage, "cache_creation_input_tokens");
|
|
235
|
-
if (input !== void 0) result.input_tokens = input;
|
|
236
|
-
if (cacheRead !== void 0) result.cache_read_input_tokens = cacheRead;
|
|
237
|
-
if (cacheCreation !== void 0) result.cache_creation_input_tokens = cacheCreation;
|
|
238
|
-
return result;
|
|
239
|
-
}
|
|
240
|
-
function mapContentBlock(raw) {
|
|
241
|
-
if (!isRecord(raw)) return null;
|
|
242
|
-
const type = stringField(raw, "type");
|
|
243
|
-
if (type === void 0) return null;
|
|
244
|
-
const block = { type };
|
|
245
|
-
const text = stringField(raw, "text");
|
|
246
|
-
if (text !== void 0) block.text = text;
|
|
247
|
-
const name = stringField(raw, "name");
|
|
248
|
-
if (name !== void 0) block.name = name;
|
|
249
|
-
const id = stringField(raw, "id");
|
|
250
|
-
if (id !== void 0) block.id = id;
|
|
251
|
-
if ("input" in raw) block.input = raw.input;
|
|
252
|
-
return block;
|
|
253
|
-
}
|
|
254
|
-
function mapAssistant(record) {
|
|
255
|
-
const message = record.message;
|
|
256
|
-
if (!isRecord(message)) return null;
|
|
257
|
-
const rawContent = isUnknownArray(message.content) ? message.content : [];
|
|
258
|
-
const content = [];
|
|
259
|
-
for (const item of rawContent) {
|
|
260
|
-
const block = mapContentBlock(item);
|
|
261
|
-
if (block) content.push(block);
|
|
262
|
-
}
|
|
263
|
-
const result = {
|
|
264
|
-
type: "assistant",
|
|
265
|
-
message: { role: "assistant", content }
|
|
266
|
-
};
|
|
267
|
-
const usage = mapUsage(message);
|
|
268
|
-
if (usage !== void 0) result.message.usage = usage;
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
function mapResultSuccess(record) {
|
|
272
|
-
const event = {
|
|
273
|
-
type: "result",
|
|
274
|
-
subtype: "success",
|
|
275
|
-
result: stringField(record, "result") ?? "",
|
|
276
|
-
total_cost_usd: numberField(record, "total_cost_usd", "totalCostUsd") ?? 0
|
|
277
|
-
};
|
|
278
|
-
const modelUsage = record.modelUsage;
|
|
279
|
-
if (isRecord(modelUsage)) event.modelUsage = modelUsage;
|
|
280
|
-
const sessionId = stringField(record, "session_id", "sessionId");
|
|
281
|
-
if (sessionId !== void 0) event.sessionId = sessionId;
|
|
282
|
-
return event;
|
|
283
|
-
}
|
|
284
|
-
function mapResultError(record) {
|
|
285
|
-
const rawErrors = isUnknownArray(record.errors) ? record.errors : [];
|
|
286
|
-
const errors = rawErrors.filter((e) => typeof e === "string");
|
|
287
|
-
const event = { type: "result", subtype: "error", errors };
|
|
288
|
-
const sessionId = stringField(record, "session_id", "sessionId");
|
|
289
|
-
if (sessionId !== void 0) event.sessionId = sessionId;
|
|
290
|
-
return event;
|
|
291
|
-
}
|
|
292
|
-
function mapResult(record) {
|
|
293
|
-
if (record.subtype === "success") return mapResultSuccess(record);
|
|
294
|
-
if (record.subtype === "error") return mapResultError(record);
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
function mapTranscriptRecord(raw) {
|
|
298
|
-
if (!isRecord(raw)) return null;
|
|
299
|
-
switch (raw.type) {
|
|
300
|
-
case "system":
|
|
301
|
-
return mapSystem(raw);
|
|
302
|
-
case "assistant":
|
|
303
|
-
return mapAssistant(raw);
|
|
304
|
-
case "result":
|
|
305
|
-
return mapResult(raw);
|
|
306
|
-
default:
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// src/harness/pty/jsonl-tailer.ts
|
|
312
|
-
var POLL_INTERVAL_MS = 25;
|
|
313
|
-
var JsonlTailer = class {
|
|
314
|
-
constructor(path, onEvent) {
|
|
315
|
-
this.path = path;
|
|
316
|
-
this.onEvent = onEvent;
|
|
317
|
-
}
|
|
318
|
-
path;
|
|
319
|
-
onEvent;
|
|
320
|
-
offset = 0;
|
|
321
|
-
buffer = "";
|
|
322
|
-
timer = null;
|
|
323
|
-
chain = Promise.resolve();
|
|
324
|
-
_closed = false;
|
|
325
|
-
/**
|
|
326
|
-
* Begin tailing. Pass `fromOffset` to skip bytes already present when the
|
|
327
|
-
* tail starts (used on resume so the prior session's records aren't replayed).
|
|
328
|
-
*/
|
|
329
|
-
start(fromOffset = 0) {
|
|
330
|
-
if (this.timer) return;
|
|
331
|
-
this.offset = fromOffset;
|
|
332
|
-
this.timer = setInterval(() => {
|
|
333
|
-
if (this._closed) return;
|
|
334
|
-
void this.enqueueRead();
|
|
335
|
-
}, POLL_INTERVAL_MS);
|
|
336
|
-
}
|
|
337
|
-
close() {
|
|
338
|
-
this._closed = true;
|
|
339
|
-
if (this.timer) {
|
|
340
|
-
clearInterval(this.timer);
|
|
341
|
-
this.timer = null;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/** Read any remaining bytes and flush a trailing line without a newline. */
|
|
345
|
-
async flush() {
|
|
346
|
-
await this.enqueueRead();
|
|
347
|
-
if (this.buffer.length > 0) {
|
|
348
|
-
this.emitLine(this.buffer);
|
|
349
|
-
this.buffer = "";
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
enqueueRead() {
|
|
353
|
-
this.chain = this.chain.then(() => this.readOnce());
|
|
354
|
-
return this.chain;
|
|
355
|
-
}
|
|
356
|
-
async readOnce() {
|
|
357
|
-
let handle = null;
|
|
358
|
-
try {
|
|
359
|
-
handle = await open(this.path, "r");
|
|
360
|
-
const stats = await handle.stat();
|
|
361
|
-
if (stats.size <= this.offset) return;
|
|
362
|
-
const length = stats.size - this.offset;
|
|
363
|
-
const buf = Buffer.alloc(length);
|
|
364
|
-
await handle.read(buf, 0, length, this.offset);
|
|
365
|
-
this.offset = stats.size;
|
|
366
|
-
this.consume(buf.toString("utf8"));
|
|
367
|
-
} catch {
|
|
368
|
-
} finally {
|
|
369
|
-
if (handle) await handle.close();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
consume(chunk) {
|
|
373
|
-
this.buffer += chunk;
|
|
374
|
-
let index = this.buffer.indexOf("\n");
|
|
375
|
-
while (index >= 0) {
|
|
376
|
-
const line = this.buffer.slice(0, index);
|
|
377
|
-
this.buffer = this.buffer.slice(index + 1);
|
|
378
|
-
this.emitLine(line);
|
|
379
|
-
index = this.buffer.indexOf("\n");
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
emitLine(line) {
|
|
383
|
-
const trimmed = line.trim();
|
|
384
|
-
if (trimmed.length === 0) return;
|
|
385
|
-
let parsed;
|
|
386
|
-
try {
|
|
387
|
-
parsed = JSON.parse(trimmed);
|
|
388
|
-
} catch {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const event = mapTranscriptRecord(parsed);
|
|
392
|
-
if (event) this.onEvent(event);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
// src/harness/pty/spawn-args.ts
|
|
397
|
-
function resolveClaudeBinary() {
|
|
398
|
-
return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
|
|
399
|
-
}
|
|
400
|
-
function buildSpawnArgs(input) {
|
|
401
|
-
const args = [];
|
|
402
|
-
if (input.resume) {
|
|
403
|
-
args.push("--resume", input.resume);
|
|
404
|
-
} else if (input.sessionId) {
|
|
405
|
-
args.push("--session-id", input.sessionId);
|
|
406
|
-
}
|
|
407
|
-
args.push("--model", input.model);
|
|
408
|
-
if (input.permissionMode === "bypassPermissions") {
|
|
409
|
-
args.push("--dangerously-skip-permissions");
|
|
410
|
-
} else {
|
|
411
|
-
args.push("--permission-mode", "plan");
|
|
412
|
-
}
|
|
413
|
-
args.push("--settings", input.settingsPath);
|
|
414
|
-
if (input.appendSystemPrompt) {
|
|
415
|
-
args.push("--append-system-prompt", input.appendSystemPrompt);
|
|
416
|
-
}
|
|
417
|
-
if (input.mcpConfigPath) {
|
|
418
|
-
args.push("--mcp-config", input.mcpConfigPath);
|
|
419
|
-
if (input.strictMcpConfig) {
|
|
420
|
-
args.push("--strict-mcp-config");
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return args;
|
|
424
|
-
}
|
|
425
|
-
var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
426
|
-
function cleanTerminalOutput(raw, maxChars = 1200) {
|
|
427
|
-
const noAnsi = raw.replace(ANSI_CSI, "");
|
|
428
|
-
let out = "";
|
|
429
|
-
for (const ch of noAnsi) {
|
|
430
|
-
const code = ch.charCodeAt(0);
|
|
431
|
-
if (ch === "\r" || ch === "\n") out += "\n";
|
|
432
|
-
else if (ch === " ") out += ch;
|
|
433
|
-
else if (code < 32 || code === 127) continue;
|
|
434
|
-
else out += ch;
|
|
435
|
-
}
|
|
436
|
-
const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
437
|
-
const text = lines.join("\n").trim();
|
|
438
|
-
return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
|
|
439
|
-
}
|
|
440
|
-
function isMissingBinaryFailure(tail) {
|
|
441
|
-
return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
|
|
442
|
-
}
|
|
443
|
-
function buildExitErrors(exitCode, rawOutput, binary) {
|
|
444
|
-
const errors = [`claude exited (code ${exitCode}) without a result`];
|
|
445
|
-
const tail = cleanTerminalOutput(rawOutput);
|
|
446
|
-
if (isMissingBinaryFailure(tail)) {
|
|
447
|
-
errors.push(
|
|
448
|
-
`The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
if (tail) {
|
|
452
|
-
errors.push(`Last terminal output before exit:
|
|
453
|
-
${tail}`);
|
|
454
|
-
}
|
|
455
|
-
return errors;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// src/harness/pty/settings.ts
|
|
459
|
-
import { mkdir, writeFile, chmod } from "fs/promises";
|
|
460
|
-
import { homedir } from "os";
|
|
461
|
-
import { join } from "path";
|
|
462
|
-
function claudeConfigHome() {
|
|
463
|
-
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
464
|
-
}
|
|
465
|
-
function projectSlug(cwd) {
|
|
466
|
-
return cwd.replace(/\//g, "-");
|
|
467
|
-
}
|
|
468
|
-
function sessionTranscriptPath(cwd, sessionId) {
|
|
469
|
-
return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
|
|
470
|
-
}
|
|
471
|
-
function configHomePlansDir() {
|
|
472
|
-
return join(claudeConfigHome(), "plans");
|
|
473
|
-
}
|
|
474
|
-
var ALLOW_RULES = [
|
|
475
|
-
"Bash",
|
|
476
|
-
"BashOutput",
|
|
477
|
-
"Edit",
|
|
478
|
-
"Write",
|
|
479
|
-
"Read",
|
|
480
|
-
"Glob",
|
|
481
|
-
"Grep",
|
|
482
|
-
"WebFetch",
|
|
483
|
-
"WebSearch",
|
|
484
|
-
"NotebookEdit",
|
|
485
|
-
"Task",
|
|
486
|
-
"TodoWrite",
|
|
487
|
-
"ToolSearch",
|
|
488
|
-
"KillShell",
|
|
489
|
-
"SlashCommand",
|
|
490
|
-
"Skill",
|
|
491
|
-
"mcp__conveyor__*"
|
|
492
|
-
];
|
|
493
|
-
var HOOK_HELPER_SOURCE = `"use strict";
|
|
494
|
-
const net = require("node:net");
|
|
495
|
-
const crypto = require("node:crypto");
|
|
496
|
-
|
|
497
|
-
const PRE_TOOL_USE_TIMEOUT_MS = 110000;
|
|
498
|
-
|
|
499
|
-
let raw = "";
|
|
500
|
-
process.stdin.setEncoding("utf8");
|
|
501
|
-
process.stdin.on("data", (chunk) => {
|
|
502
|
-
raw += chunk;
|
|
503
|
-
});
|
|
504
|
-
process.stdin.on("end", () => {
|
|
505
|
-
let payload = {};
|
|
506
|
-
try {
|
|
507
|
-
const parsed = JSON.parse(raw);
|
|
508
|
-
if (parsed && typeof parsed === "object") payload = parsed;
|
|
509
|
-
} catch {
|
|
510
|
-
payload = {};
|
|
511
|
-
}
|
|
512
|
-
if (payload.hook_event_name === "PreToolUse") {
|
|
513
|
-
preToolUse(payload);
|
|
514
|
-
} else {
|
|
515
|
-
relayProgress(typeof payload.tool_name === "string" ? payload.tool_name : "");
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
function relayProgress(toolName) {
|
|
520
|
-
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
521
|
-
let done = false;
|
|
522
|
-
const finish = () => {
|
|
523
|
-
if (done) return;
|
|
524
|
-
done = true;
|
|
525
|
-
process.stdout.write(JSON.stringify({ continue: true }));
|
|
526
|
-
process.exit(0);
|
|
527
|
-
};
|
|
528
|
-
const fallback = setTimeout(finish, 250);
|
|
529
|
-
if (!socketPath) {
|
|
530
|
-
clearTimeout(fallback);
|
|
531
|
-
finish();
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
const client = net.connect(socketPath, () => {
|
|
535
|
-
// PostToolUse does not supply tool duration, so elapsed_time_seconds is
|
|
536
|
-
// omitted rather than reported as a misleading 0 (the field is optional).
|
|
537
|
-
const line = JSON.stringify({ tool_name: toolName }) + "\\n";
|
|
538
|
-
client.write(line, () => {
|
|
539
|
-
client.end();
|
|
540
|
-
});
|
|
541
|
-
});
|
|
542
|
-
client.on("close", () => {
|
|
543
|
-
clearTimeout(fallback);
|
|
544
|
-
finish();
|
|
545
|
-
});
|
|
546
|
-
client.on("error", () => {
|
|
547
|
-
clearTimeout(fallback);
|
|
548
|
-
finish();
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function preToolUse(payload) {
|
|
553
|
-
const socketPath = process.env.CONVEYOR_HOOK_SOCKET;
|
|
554
|
-
let done = false;
|
|
555
|
-
const respond = (decision, reason) => {
|
|
556
|
-
if (done) return;
|
|
557
|
-
done = true;
|
|
558
|
-
process.stdout.write(
|
|
559
|
-
JSON.stringify({
|
|
560
|
-
hookSpecificOutput: {
|
|
561
|
-
hookEventName: "PreToolUse",
|
|
562
|
-
permissionDecision: decision,
|
|
563
|
-
...(reason ? { permissionDecisionReason: reason } : {}),
|
|
564
|
-
},
|
|
565
|
-
}),
|
|
566
|
-
);
|
|
567
|
-
process.exit(0);
|
|
568
|
-
};
|
|
569
|
-
const denyUnavailable = () =>
|
|
570
|
-
respond(
|
|
571
|
-
"deny",
|
|
572
|
-
"Conveyor validation is unavailable right now. Wait a moment and call the tool again.",
|
|
573
|
-
);
|
|
574
|
-
if (!socketPath) {
|
|
575
|
-
denyUnavailable();
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
const id = crypto.randomBytes(8).toString("hex");
|
|
579
|
-
const timer = setTimeout(denyUnavailable, PRE_TOOL_USE_TIMEOUT_MS);
|
|
580
|
-
const client = net.connect(socketPath, () => {
|
|
581
|
-
client.write(
|
|
582
|
-
JSON.stringify({
|
|
583
|
-
kind: "pre_tool_use",
|
|
584
|
-
id,
|
|
585
|
-
tool_name: typeof payload.tool_name === "string" ? payload.tool_name : "",
|
|
586
|
-
tool_input:
|
|
587
|
-
payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {},
|
|
588
|
-
}) + "\\n",
|
|
589
|
-
);
|
|
590
|
-
});
|
|
591
|
-
let buffer = "";
|
|
592
|
-
client.on("data", (chunk) => {
|
|
593
|
-
buffer += chunk.toString("utf8");
|
|
594
|
-
let index = buffer.indexOf("\\n");
|
|
595
|
-
while (index >= 0) {
|
|
596
|
-
const line = buffer.slice(0, index);
|
|
597
|
-
buffer = buffer.slice(index + 1);
|
|
598
|
-
try {
|
|
599
|
-
const verdict = JSON.parse(line);
|
|
600
|
-
if (verdict && verdict.id === id) {
|
|
601
|
-
clearTimeout(timer);
|
|
602
|
-
client.end();
|
|
603
|
-
respond(verdict.decision === "allow" ? "allow" : "deny", verdict.reason);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
} catch {
|
|
607
|
-
/* keep scanning */
|
|
608
|
-
}
|
|
609
|
-
index = buffer.indexOf("\\n");
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
client.on("error", () => {
|
|
613
|
-
clearTimeout(timer);
|
|
614
|
-
denyUnavailable();
|
|
615
|
-
});
|
|
616
|
-
client.on("close", () => {
|
|
617
|
-
clearTimeout(timer);
|
|
618
|
-
denyUnavailable();
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
`;
|
|
622
|
-
async function writeHookSettings(dir) {
|
|
623
|
-
const helperPath = join(dir, "hook-helper.cjs");
|
|
624
|
-
const settingsPath = join(dir, "settings.json");
|
|
625
|
-
await mkdir(dir, { recursive: true });
|
|
626
|
-
await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
|
|
627
|
-
await chmod(helperPath, 493);
|
|
628
|
-
const settings = {
|
|
629
|
-
// Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
|
|
630
|
-
// spawns pass `--dangerously-skip-permissions`; on a real PTY the CLI otherwise
|
|
631
|
-
// parks on the interactive "Yes, I accept / No, exit" dialog whose default focus
|
|
632
|
-
// is "No, exit" — so it cannot be auto-dismissed by an Enter nudge and the run
|
|
633
|
-
// stalls until the auto-nudge watchdog shuts it down. The CLI reads this key
|
|
634
|
-
// from the `--settings` (flagSettings) layer via its bypass-mode gate, so
|
|
635
|
-
// setting it here suppresses the dialog deterministically on every spawn —
|
|
636
|
-
// independent of the `bypassPermissionsModeAccepted` seed in credentials.ts,
|
|
637
|
-
// which the CLI's one-time migration consumes and which is subject to
|
|
638
|
-
// persistence races on the codespace user-home.
|
|
639
|
-
skipDangerousModePermissionPrompt: true,
|
|
640
|
-
// Auto-approve tool calls so the interactive (PTY) agent never stops to
|
|
641
|
-
// prompt the viewer. This only suppresses per-tool approval prompts — it
|
|
642
|
-
// does NOT relax the planning gate: in discovery/plan mode the spawn passes
|
|
643
|
-
// `--permission-mode plan`, which keeps the agent read-only (no edits/commits
|
|
644
|
-
// until it exits plan mode) regardless of this allow-list. In build mode the
|
|
645
|
-
// spawn already bypasses prompts via `--dangerously-skip-permissions`.
|
|
646
|
-
// NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
|
|
647
|
-
// a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
|
|
648
|
-
permissions: {
|
|
649
|
-
allow: ALLOW_RULES
|
|
650
|
-
},
|
|
651
|
-
hooks: {
|
|
652
|
-
PreToolUse: [
|
|
653
|
-
{
|
|
654
|
-
matcher: "ExitPlanMode",
|
|
655
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}`, timeout: 120 }]
|
|
656
|
-
}
|
|
657
|
-
],
|
|
658
|
-
PostToolUse: [
|
|
659
|
-
{
|
|
660
|
-
matcher: "*",
|
|
661
|
-
hooks: [{ type: "command", command: `node ${JSON.stringify(helperPath)}` }]
|
|
662
|
-
}
|
|
663
|
-
]
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
667
|
-
return { settingsPath, helperPath };
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/harness/pty/output-coalescer.ts
|
|
671
|
-
var DEFAULT_FLUSH_MS = 40;
|
|
672
|
-
var DEFAULT_MAX_BUFFER_CHARS = 48 * 1024;
|
|
673
|
-
var PtyOutputCoalescer = class {
|
|
674
|
-
constructor(sink, getDims, flushMs = DEFAULT_FLUSH_MS, maxBufferChars = DEFAULT_MAX_BUFFER_CHARS) {
|
|
675
|
-
this.sink = sink;
|
|
676
|
-
this.getDims = getDims;
|
|
677
|
-
this.flushMs = flushMs;
|
|
678
|
-
this.maxBufferChars = maxBufferChars;
|
|
679
|
-
}
|
|
680
|
-
sink;
|
|
681
|
-
getDims;
|
|
682
|
-
flushMs;
|
|
683
|
-
maxBufferChars;
|
|
684
|
-
buffer = "";
|
|
685
|
-
timer = null;
|
|
686
|
-
disposed = false;
|
|
687
|
-
/** Buffer a chunk; flushes synchronously when the size threshold is hit. */
|
|
688
|
-
write(data) {
|
|
689
|
-
if (this.disposed || data === "") return;
|
|
690
|
-
this.buffer += data;
|
|
691
|
-
if (this.buffer.length >= this.maxBufferChars) {
|
|
692
|
-
this.flush();
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
if (!this.timer) {
|
|
696
|
-
this.timer = setTimeout(() => {
|
|
697
|
-
this.flush();
|
|
698
|
-
}, this.flushMs);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
/** Ship the buffered bytes now (no-op when empty). Dims are read at flush
|
|
702
|
-
* time so a mid-buffer resize stamps the frame with the dims it will render
|
|
703
|
-
* under. */
|
|
704
|
-
flush() {
|
|
705
|
-
if (this.timer) {
|
|
706
|
-
clearTimeout(this.timer);
|
|
707
|
-
this.timer = null;
|
|
708
|
-
}
|
|
709
|
-
if (this.buffer === "") return;
|
|
710
|
-
const data = this.buffer;
|
|
711
|
-
this.buffer = "";
|
|
712
|
-
this.sink(data, this.getDims());
|
|
713
|
-
}
|
|
714
|
-
/** Final flush, then drop any future writes (session torn down). Idempotent. */
|
|
715
|
-
dispose() {
|
|
716
|
-
if (this.disposed) return;
|
|
717
|
-
this.flush();
|
|
718
|
-
this.disposed = true;
|
|
719
|
-
}
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
// src/harness/pty/tool-server.ts
|
|
723
|
-
import { createServer as createServer2 } from "http";
|
|
724
|
-
import { writeFile as writeFile2 } from "fs/promises";
|
|
725
|
-
import { join as join2 } from "path";
|
|
726
|
-
import { randomBytes } from "crypto";
|
|
727
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
728
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
729
|
-
|
|
730
|
-
// src/harness/pty/mcp-server.ts
|
|
731
|
-
var PtyMcpServer = class {
|
|
732
|
-
constructor(name, tools) {
|
|
733
|
-
this.name = name;
|
|
734
|
-
this.tools = tools;
|
|
735
|
-
}
|
|
736
|
-
name;
|
|
737
|
-
tools;
|
|
738
|
-
getTool(name) {
|
|
739
|
-
return this.tools.find((tool2) => tool2.name === name);
|
|
740
|
-
}
|
|
741
|
-
invokeTool(name, input) {
|
|
742
|
-
const tool2 = this.getTool(name);
|
|
743
|
-
if (!tool2) return Promise.reject(new Error(`Unknown tool: ${name}`));
|
|
744
|
-
return tool2.handler(input);
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
// src/harness/pty/tool-server.ts
|
|
749
|
-
var LOOPBACK = "127.0.0.1";
|
|
750
|
-
var PtyToolServer = class {
|
|
751
|
-
constructor(name, tools) {
|
|
752
|
-
this.name = name;
|
|
753
|
-
this.tools = tools;
|
|
754
|
-
}
|
|
755
|
-
name;
|
|
756
|
-
tools;
|
|
757
|
-
http = null;
|
|
758
|
-
transport = null;
|
|
759
|
-
mcp = null;
|
|
760
|
-
token = randomBytes(24).toString("base64url");
|
|
761
|
-
async start() {
|
|
762
|
-
const mcp = new McpServer({ name: this.name, version: "1.0.0" });
|
|
763
|
-
const register = mcp.tool.bind(mcp);
|
|
764
|
-
for (const tool2 of this.tools) {
|
|
765
|
-
register(tool2.name, tool2.description, tool2.schema, (args) => tool2.handler(args));
|
|
766
|
-
}
|
|
767
|
-
const transport = new StreamableHTTPServerTransport({
|
|
768
|
-
sessionIdGenerator: () => randomBytes(16).toString("hex"),
|
|
769
|
-
enableJsonResponse: true
|
|
770
|
-
});
|
|
771
|
-
await mcp.connect(transport);
|
|
772
|
-
const server = createServer2((req, res) => {
|
|
773
|
-
void this.handle(req, res, transport);
|
|
774
|
-
});
|
|
775
|
-
await new Promise((resolve, reject) => {
|
|
776
|
-
server.once("error", reject);
|
|
777
|
-
server.listen(0, LOOPBACK, () => resolve());
|
|
778
|
-
});
|
|
779
|
-
const address = server.address();
|
|
780
|
-
const port = address && typeof address === "object" ? address.port : 0;
|
|
781
|
-
this.http = server;
|
|
782
|
-
this.transport = transport;
|
|
783
|
-
this.mcp = mcp;
|
|
784
|
-
return { url: `http://${LOOPBACK}:${port}/mcp`, token: this.token };
|
|
785
|
-
}
|
|
786
|
-
async handle(req, res, transport) {
|
|
787
|
-
if (req.headers.authorization !== `Bearer ${this.token}`) {
|
|
788
|
-
res.writeHead(401).end();
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
try {
|
|
792
|
-
await transport.handleRequest(req, res);
|
|
793
|
-
} catch {
|
|
794
|
-
if (res.headersSent) res.end();
|
|
795
|
-
else res.writeHead(500).end();
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
async close() {
|
|
799
|
-
try {
|
|
800
|
-
await this.transport?.close();
|
|
801
|
-
} catch {
|
|
802
|
-
}
|
|
803
|
-
this.transport = null;
|
|
804
|
-
try {
|
|
805
|
-
await this.mcp?.close();
|
|
806
|
-
} catch {
|
|
807
|
-
}
|
|
808
|
-
this.mcp = null;
|
|
809
|
-
const http = this.http;
|
|
810
|
-
this.http = null;
|
|
811
|
-
if (http) {
|
|
812
|
-
await new Promise((resolve) => {
|
|
813
|
-
http.close(() => resolve());
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
async function startToolServers(mcpServers, tempDir) {
|
|
819
|
-
const servers = [];
|
|
820
|
-
const config = {};
|
|
821
|
-
for (const [name, handle] of Object.entries(mcpServers)) {
|
|
822
|
-
const tools = handle instanceof PtyMcpServer ? handle.tools : [];
|
|
823
|
-
if (tools.length === 0) continue;
|
|
824
|
-
const server = new PtyToolServer(name, tools);
|
|
825
|
-
const { url, token } = await server.start();
|
|
826
|
-
servers.push(server);
|
|
827
|
-
config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
|
|
828
|
-
}
|
|
829
|
-
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
830
|
-
const mcpConfigPath = join2(tempDir, "mcp-config.json");
|
|
831
|
-
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
832
|
-
return { servers, mcpConfigPath };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// src/harness/pty/session.ts
|
|
836
|
-
var MAX_DIAGNOSTIC_OUTPUT = 4e3;
|
|
837
|
-
function isRecord2(value) {
|
|
838
|
-
return typeof value === "object" && value !== null;
|
|
839
|
-
}
|
|
840
|
-
function extractSpawn(mod) {
|
|
841
|
-
if (!isRecord2(mod)) return null;
|
|
842
|
-
if (typeof mod.spawn === "function") return mod.spawn;
|
|
843
|
-
const def = mod.default;
|
|
844
|
-
if (isRecord2(def) && typeof def.spawn === "function") return def.spawn;
|
|
845
|
-
return null;
|
|
846
|
-
}
|
|
847
|
-
async function loadPtySpawn() {
|
|
848
|
-
const mod = await import("node-pty");
|
|
849
|
-
const spawn = extractSpawn(mod);
|
|
850
|
-
if (!spawn) throw new Error("node-pty: spawn export not found");
|
|
851
|
-
return spawn;
|
|
852
|
-
}
|
|
853
|
-
function inheritedEnv(socketPath) {
|
|
854
|
-
const env = {};
|
|
855
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
856
|
-
if (typeof value === "string") env[key] = value;
|
|
857
|
-
}
|
|
858
|
-
env.CONVEYOR_HOOK_SOCKET = socketPath;
|
|
859
|
-
return env;
|
|
860
|
-
}
|
|
861
|
-
function buildPromptBytes(text) {
|
|
862
|
-
return `\x1B[200~${text}\x1B[201~`;
|
|
863
|
-
}
|
|
864
|
-
var SUBMIT_SETTLE_MS = 300;
|
|
865
|
-
var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
|
|
866
|
-
var SUBMIT_NUDGE_MAX_PRESSES = 5;
|
|
867
|
-
function sleep(ms) {
|
|
868
|
-
return new Promise((resolve) => {
|
|
869
|
-
setTimeout(resolve, ms);
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
async function transcriptSize(path) {
|
|
873
|
-
try {
|
|
874
|
-
return (await stat(path)).size;
|
|
875
|
-
} catch {
|
|
876
|
-
return 0;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
var PtySession = class {
|
|
880
|
-
constructor(prompt, options, resume, bridge) {
|
|
881
|
-
this.prompt = prompt;
|
|
882
|
-
this.options = options;
|
|
883
|
-
this.resume = resume;
|
|
884
|
-
this.bridge = bridge;
|
|
885
|
-
}
|
|
886
|
-
prompt;
|
|
887
|
-
options;
|
|
888
|
-
resume;
|
|
889
|
-
bridge;
|
|
890
|
-
queue = new AsyncEventQueue();
|
|
891
|
-
socket = null;
|
|
892
|
-
tailer = null;
|
|
893
|
-
pty = null;
|
|
894
|
-
tempDir = "";
|
|
895
|
-
sawResult = false;
|
|
896
|
-
// Rolling tail of raw PTY output, retained only to enrich the error when the
|
|
897
|
-
// CLI exits before emitting a result (the bytes are otherwise relayed to S5
|
|
898
|
-
// and never become events). Trimmed to MAX_DIAGNOSTIC_OUTPUT on every write.
|
|
899
|
-
recentOutput = "";
|
|
900
|
-
coalescer = null;
|
|
901
|
-
_toreDown = false;
|
|
902
|
-
exitListeners = [];
|
|
903
|
-
idleListeners = [];
|
|
904
|
-
abortHandler = null;
|
|
905
|
-
cols = 120;
|
|
906
|
-
rows = 40;
|
|
907
|
-
unsubInput = null;
|
|
908
|
-
unsubResize = null;
|
|
909
|
-
// In-process HTTP MCP servers exposing the harness tools to the spawned CLI,
|
|
910
|
-
// plus the path to the `--mcp-config` that points at them.
|
|
911
|
-
toolServers = [];
|
|
912
|
-
mcpConfigPath = null;
|
|
913
|
-
// Plan-dialog auto-accept state (see armPlanDialogAutoAccept).
|
|
914
|
-
pendingPlanApproval = false;
|
|
915
|
-
planApprovalTimer = null;
|
|
916
|
-
// Submit-nudge state (see armSubmitNudge).
|
|
917
|
-
pendingSubmitNudge = false;
|
|
918
|
-
submitNudgeTimer = null;
|
|
919
|
-
onIdle(listener) {
|
|
920
|
-
this.idleListeners.push(listener);
|
|
921
|
-
}
|
|
922
|
-
onExit(listener) {
|
|
923
|
-
this.exitListeners.push(listener);
|
|
924
|
-
}
|
|
925
|
-
get isToreDown() {
|
|
926
|
-
return this._toreDown;
|
|
927
|
-
}
|
|
928
|
-
get hookSocketPath() {
|
|
929
|
-
return this.socket ? this.socket.socketPath : null;
|
|
930
|
-
}
|
|
931
|
-
events() {
|
|
932
|
-
return this.queue.drain();
|
|
933
|
-
}
|
|
934
|
-
async start() {
|
|
935
|
-
const sessionId = this.resume ?? this.options.sessionId;
|
|
936
|
-
if (!sessionId) {
|
|
937
|
-
throw new Error("PtySession requires options.sessionId or a resume target");
|
|
938
|
-
}
|
|
939
|
-
const signal = this.options.abortController?.signal;
|
|
940
|
-
if (signal?.aborted) {
|
|
941
|
-
await this.teardown();
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
try {
|
|
945
|
-
this.tempDir = await mkdtemp(join3(tmpdir(), "conveyor-pty-"));
|
|
946
|
-
const socketPath = join3(this.tempDir, "hook.sock");
|
|
947
|
-
this.socket = new HookSocketServer(
|
|
948
|
-
socketPath,
|
|
949
|
-
(progress) => this.handleProgress(progress),
|
|
950
|
-
(request) => this.handlePreToolUse(request)
|
|
951
|
-
);
|
|
952
|
-
await this.socket.listen();
|
|
953
|
-
const { settingsPath } = await writeHookSettings(this.tempDir);
|
|
954
|
-
await this.setupToolServers();
|
|
955
|
-
const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
|
|
956
|
-
await mkdir2(dirname(transcriptPath), { recursive: true });
|
|
957
|
-
const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
|
|
958
|
-
this.tailer = new JsonlTailer(transcriptPath, (event) => this.handleTranscriptEvent(event));
|
|
959
|
-
this.tailer.start(startOffset);
|
|
960
|
-
await this.spawn(settingsPath, socketPath);
|
|
961
|
-
if (signal) {
|
|
962
|
-
this.abortHandler = () => {
|
|
963
|
-
void this.teardown();
|
|
964
|
-
};
|
|
965
|
-
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
966
|
-
if (signal.aborted) {
|
|
967
|
-
await this.teardown();
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (this.bridge) {
|
|
972
|
-
this.unsubInput = this.bridge.onInput((data) => this.writeStdin(data));
|
|
973
|
-
this.unsubResize = this.bridge.onResize((cols, rows) => this.resizePty(cols, rows));
|
|
974
|
-
}
|
|
975
|
-
await this.feedPrompt();
|
|
976
|
-
} catch (err) {
|
|
977
|
-
await this.teardown();
|
|
978
|
-
throw err;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
writeStdin(text) {
|
|
982
|
-
this.pty?.write(text);
|
|
983
|
-
}
|
|
984
|
-
/** Apply a relayed resize to the live pty (reconciled dims from the server). */
|
|
985
|
-
resizePty(cols, rows) {
|
|
986
|
-
if (cols <= 0 || rows <= 0) return;
|
|
987
|
-
this.cols = cols;
|
|
988
|
-
this.rows = rows;
|
|
989
|
-
try {
|
|
990
|
-
this.pty?.resize(cols, rows);
|
|
991
|
-
} catch {
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
async teardown() {
|
|
995
|
-
if (this._toreDown) return;
|
|
996
|
-
this._toreDown = true;
|
|
997
|
-
this.disarmPlanDialogAutoAccept();
|
|
998
|
-
this.disarmSubmitNudge();
|
|
999
|
-
this.unsubInput?.();
|
|
1000
|
-
this.unsubInput = null;
|
|
1001
|
-
this.unsubResize?.();
|
|
1002
|
-
this.unsubResize = null;
|
|
1003
|
-
if (this.abortHandler) {
|
|
1004
|
-
this.options.abortController?.signal.removeEventListener("abort", this.abortHandler);
|
|
1005
|
-
this.abortHandler = null;
|
|
1006
|
-
}
|
|
1007
|
-
try {
|
|
1008
|
-
this.pty?.kill();
|
|
1009
|
-
} catch {
|
|
1010
|
-
}
|
|
1011
|
-
this.pty = null;
|
|
1012
|
-
this.coalescer?.dispose();
|
|
1013
|
-
this.coalescer = null;
|
|
1014
|
-
this.tailer?.close();
|
|
1015
|
-
this.tailer = null;
|
|
1016
|
-
if (this.socket) {
|
|
1017
|
-
await this.socket.close();
|
|
1018
|
-
this.socket = null;
|
|
1019
|
-
}
|
|
1020
|
-
for (const toolServer of this.toolServers) {
|
|
1021
|
-
try {
|
|
1022
|
-
await toolServer.close();
|
|
1023
|
-
} catch {
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
this.toolServers = [];
|
|
1027
|
-
this.mcpConfigPath = null;
|
|
1028
|
-
this.queue.close();
|
|
1029
|
-
if (this.tempDir) {
|
|
1030
|
-
await rm(this.tempDir, { recursive: true, force: true });
|
|
1031
|
-
this.tempDir = "";
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Serve the harness's in-process tools to the spawned CLI over loopback HTTP
|
|
1036
|
-
* (handlers run in THIS process against the live task-token connection) and
|
|
1037
|
-
* record the `--mcp-config` path for spawn(). No-op when the harness was
|
|
1038
|
-
* constructed without tools (e.g. SDK-only callers).
|
|
1039
|
-
*/
|
|
1040
|
-
async setupToolServers() {
|
|
1041
|
-
const { servers, mcpConfigPath } = await startToolServers(
|
|
1042
|
-
this.options.mcpServers ?? {},
|
|
1043
|
-
this.tempDir
|
|
1044
|
-
);
|
|
1045
|
-
this.toolServers = servers;
|
|
1046
|
-
this.mcpConfigPath = mcpConfigPath;
|
|
1047
|
-
}
|
|
1048
|
-
async spawn(settingsPath, socketPath) {
|
|
1049
|
-
const args = buildSpawnArgs({
|
|
1050
|
-
resume: this.resume,
|
|
1051
|
-
sessionId: this.options.sessionId,
|
|
1052
|
-
model: this.options.model,
|
|
1053
|
-
permissionMode: this.options.permissionMode,
|
|
1054
|
-
settingsPath,
|
|
1055
|
-
...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
|
|
1056
|
-
// Only this config: ignore the user's ~/.claude.json / project .mcp.json
|
|
1057
|
-
// so the agent's tool set is deterministic (and a stale personal conveyor
|
|
1058
|
-
// server doesn't load).
|
|
1059
|
-
...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath, strictMcpConfig: true } : {}
|
|
1060
|
-
});
|
|
1061
|
-
const spawn = await loadPtySpawn();
|
|
1062
|
-
const pty = spawn(resolveClaudeBinary(), args, {
|
|
1063
|
-
name: "xterm-color",
|
|
1064
|
-
cols: this.cols,
|
|
1065
|
-
rows: this.rows,
|
|
1066
|
-
cwd: this.options.cwd,
|
|
1067
|
-
env: inheritedEnv(socketPath)
|
|
1068
|
-
});
|
|
1069
|
-
const bridge = this.bridge;
|
|
1070
|
-
if (bridge) {
|
|
1071
|
-
this.coalescer = new PtyOutputCoalescer(
|
|
1072
|
-
(data, dims) => {
|
|
1073
|
-
bridge.sendOutput(data, dims);
|
|
1074
|
-
},
|
|
1075
|
-
() => ({ cols: this.cols, rows: this.rows })
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
pty.onData((data) => {
|
|
1079
|
-
this.coalescer?.write(data);
|
|
1080
|
-
this.recentOutput = (this.recentOutput + data).slice(-MAX_DIAGNOSTIC_OUTPUT);
|
|
1081
|
-
});
|
|
1082
|
-
pty.onExit((event) => {
|
|
1083
|
-
void this.finalizeOnExit(event.exitCode);
|
|
1084
|
-
});
|
|
1085
|
-
this.pty = pty;
|
|
1086
|
-
}
|
|
1087
|
-
async deliverPrompt(text) {
|
|
1088
|
-
this.writeStdin(buildPromptBytes(text));
|
|
1089
|
-
if (this.options.promptDelivery === "prefill") return;
|
|
1090
|
-
await sleep(SUBMIT_SETTLE_MS);
|
|
1091
|
-
if (this._toreDown) return;
|
|
1092
|
-
this.writeStdin("\r");
|
|
1093
|
-
this.armSubmitNudge();
|
|
1094
|
-
}
|
|
1095
|
-
async feedPrompt() {
|
|
1096
|
-
if (typeof this.prompt === "string") {
|
|
1097
|
-
await this.deliverPrompt(this.prompt);
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
for await (const message of this.prompt) {
|
|
1101
|
-
const content = message.message.content;
|
|
1102
|
-
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
1103
|
-
await this.deliverPrompt(text);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Even as a separate write, the submitting Enter can race CLI startup and
|
|
1108
|
-
* be swallowed, leaving the pasted prompt parked in the input box with
|
|
1109
|
-
* nothing to un-park it (the 45s parked-TUI watchdog only reports status).
|
|
1110
|
-
* Until the first transcript record proves the turn started, re-press Enter
|
|
1111
|
-
* on a bounded interval. Enter on an empty or mid-turn input box is a no-op.
|
|
1112
|
-
* Deliberate trade-off (same as the plan-dialog auto-accept below): a press
|
|
1113
|
-
* can accept the default of an unexpected startup dialog — in a headless
|
|
1114
|
-
* pod that unblocks the run rather than parking it forever.
|
|
1115
|
-
*/
|
|
1116
|
-
armSubmitNudge() {
|
|
1117
|
-
if (this.submitNudgeTimer) return;
|
|
1118
|
-
this.pendingSubmitNudge = true;
|
|
1119
|
-
let presses = 0;
|
|
1120
|
-
const press = () => {
|
|
1121
|
-
if (this._toreDown || !this.pendingSubmitNudge || presses >= SUBMIT_NUDGE_MAX_PRESSES) {
|
|
1122
|
-
this.disarmSubmitNudge();
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
presses++;
|
|
1126
|
-
this.writeStdin("\r");
|
|
1127
|
-
};
|
|
1128
|
-
this.submitNudgeTimer = setInterval(press, SUBMIT_NUDGE_INTERVAL_MS);
|
|
1129
|
-
}
|
|
1130
|
-
disarmSubmitNudge() {
|
|
1131
|
-
this.pendingSubmitNudge = false;
|
|
1132
|
-
if (this.submitNudgeTimer) {
|
|
1133
|
-
clearInterval(this.submitNudgeTimer);
|
|
1134
|
-
this.submitNudgeTimer = null;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
handleProgress(progress) {
|
|
1138
|
-
this.queue.push({
|
|
1139
|
-
type: "tool_progress",
|
|
1140
|
-
...progress.tool_name === void 0 ? {} : { tool_name: progress.tool_name },
|
|
1141
|
-
...progress.elapsed_time_seconds === void 0 ? {} : { elapsed_time_seconds: progress.elapsed_time_seconds }
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* PreToolUse request from the hook helper → the runner's `canUseTool`
|
|
1146
|
-
* verdict. Only ExitPlanMode is matched by the settings hook, so this is
|
|
1147
|
-
* the Conveyor plan-exit gate (validation + identification trigger run
|
|
1148
|
-
* inside the callback). Absent callback → allow (mirrors the SDK default).
|
|
1149
|
-
*/
|
|
1150
|
-
async handlePreToolUse(request) {
|
|
1151
|
-
const canUseTool = this.options.canUseTool;
|
|
1152
|
-
let verdict;
|
|
1153
|
-
if (canUseTool) {
|
|
1154
|
-
const result = await canUseTool(request.tool_name, request.tool_input);
|
|
1155
|
-
verdict = result.behavior === "allow" ? { decision: "allow" } : { decision: "deny", reason: result.message };
|
|
1156
|
-
} else {
|
|
1157
|
-
verdict = { decision: "allow" };
|
|
1158
|
-
}
|
|
1159
|
-
if (verdict.decision === "allow" && request.tool_name === "ExitPlanMode" && this.options.planDialogAutoAccept) {
|
|
1160
|
-
this.armPlanDialogAutoAccept();
|
|
1161
|
-
}
|
|
1162
|
-
return verdict;
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* A hook "allow" does not bypass the CLI's plan-approval dialog — it renders
|
|
1166
|
-
* right after the verdict. Press Enter (default: "Yes, auto-accept edits")
|
|
1167
|
-
* until the turn resumes; the next transcript record only lands once the
|
|
1168
|
-
* dialog is answered, so it doubles as the stop signal.
|
|
1169
|
-
*/
|
|
1170
|
-
armPlanDialogAutoAccept() {
|
|
1171
|
-
if (this.planApprovalTimer) return;
|
|
1172
|
-
this.pendingPlanApproval = true;
|
|
1173
|
-
let presses = 0;
|
|
1174
|
-
const press = () => {
|
|
1175
|
-
if (this._toreDown || !this.pendingPlanApproval || presses >= 6) {
|
|
1176
|
-
this.disarmPlanDialogAutoAccept();
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
presses++;
|
|
1180
|
-
this.writeStdin("\r");
|
|
1181
|
-
};
|
|
1182
|
-
this.planApprovalTimer = setInterval(press, 1500);
|
|
1183
|
-
setTimeout(press, 700);
|
|
1184
|
-
}
|
|
1185
|
-
disarmPlanDialogAutoAccept() {
|
|
1186
|
-
this.pendingPlanApproval = false;
|
|
1187
|
-
if (this.planApprovalTimer) {
|
|
1188
|
-
clearInterval(this.planApprovalTimer);
|
|
1189
|
-
this.planApprovalTimer = null;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
handleTranscriptEvent(event) {
|
|
1193
|
-
if (this.pendingPlanApproval) this.disarmPlanDialogAutoAccept();
|
|
1194
|
-
if (this.pendingSubmitNudge) this.disarmSubmitNudge();
|
|
1195
|
-
this.queue.push(event);
|
|
1196
|
-
if (event.type === "result") {
|
|
1197
|
-
this.sawResult = true;
|
|
1198
|
-
for (const listener of this.idleListeners) listener();
|
|
1199
|
-
this.queue.close();
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
async finalizeOnExit(exitCode) {
|
|
1203
|
-
this.coalescer?.flush();
|
|
1204
|
-
if (this._toreDown) return;
|
|
1205
|
-
if (this.tailer) {
|
|
1206
|
-
this.tailer.close();
|
|
1207
|
-
await this.tailer.flush();
|
|
1208
|
-
}
|
|
1209
|
-
if (!this.sawResult) {
|
|
1210
|
-
this.queue.push({
|
|
1211
|
-
type: "result",
|
|
1212
|
-
subtype: "error",
|
|
1213
|
-
errors: buildExitErrors(exitCode, this.recentOutput, resolveClaudeBinary())
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
for (const listener of this.exitListeners) listener(exitCode);
|
|
1217
|
-
this.queue.close();
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
|
|
1221
|
-
// src/harness/pty/credentials.ts
|
|
1222
|
-
import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1223
|
-
import { homedir as homedir2 } from "os";
|
|
1224
|
-
import { join as join4 } from "path";
|
|
1225
|
-
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1226
|
-
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1227
|
-
function claudeCredentialsPath() {
|
|
1228
|
-
return join4(claudeConfigHome(), ".credentials.json");
|
|
1229
|
-
}
|
|
1230
|
-
function isConveyorCloudEnv(env = process.env) {
|
|
1231
|
-
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
1232
|
-
}
|
|
1233
|
-
function parseClaudeAiOauth(raw) {
|
|
1234
|
-
if (!raw || raw.trim() === "") return null;
|
|
1235
|
-
try {
|
|
1236
|
-
const parsed = JSON.parse(raw);
|
|
1237
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1238
|
-
const oauth = parsed.claudeAiOauth;
|
|
1239
|
-
if (typeof oauth !== "object" || oauth === null) return null;
|
|
1240
|
-
const record = oauth;
|
|
1241
|
-
return {
|
|
1242
|
-
accessToken: record.accessToken,
|
|
1243
|
-
refreshToken: record.refreshToken,
|
|
1244
|
-
expiresAt: record.expiresAt
|
|
1245
|
-
};
|
|
1246
|
-
} catch {
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
function buildSynthesizedCredentials(token, now) {
|
|
1251
|
-
return JSON.stringify({
|
|
1252
|
-
claudeAiOauth: {
|
|
1253
|
-
accessToken: token,
|
|
1254
|
-
expiresAt: now + SYNTH_TOKEN_TTL_MS,
|
|
1255
|
-
scopes: ["user:inference", "user:profile"],
|
|
1256
|
-
subscriptionType: "max"
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
function planCredentialsWrite(input) {
|
|
1261
|
-
if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
|
|
1262
|
-
if (!input.token) return { action: "skip", reason: "no-token" };
|
|
1263
|
-
const contents = buildSynthesizedCredentials(input.token, input.now);
|
|
1264
|
-
const existing = parseClaudeAiOauth(input.existingRaw);
|
|
1265
|
-
if (!existing) return { action: "write", contents };
|
|
1266
|
-
if (typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
1267
|
-
return { action: "skip", reason: "foreign-credentials" };
|
|
1268
|
-
}
|
|
1269
|
-
const fresh = existing.accessToken === input.token && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
|
|
1270
|
-
if (fresh) return { action: "skip", reason: "current" };
|
|
1271
|
-
return { action: "write", contents };
|
|
1272
|
-
}
|
|
1273
|
-
async function readRaw(path) {
|
|
1274
|
-
try {
|
|
1275
|
-
return await readFile(path, "utf8");
|
|
1276
|
-
} catch {
|
|
1277
|
-
return null;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
async function ensureClaudeCredentials(env = process.env) {
|
|
1281
|
-
try {
|
|
1282
|
-
const path = claudeCredentialsPath();
|
|
1283
|
-
const plan = planCredentialsWrite({
|
|
1284
|
-
isCloud: isConveyorCloudEnv(env),
|
|
1285
|
-
token: env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
1286
|
-
existingRaw: await readRaw(path),
|
|
1287
|
-
now: Date.now()
|
|
1288
|
-
});
|
|
1289
|
-
if (plan.action === "skip") return;
|
|
1290
|
-
await mkdir3(claudeConfigHome(), { recursive: true });
|
|
1291
|
-
await writeFile3(path, plan.contents, { encoding: "utf8", mode: 384 });
|
|
1292
|
-
await chmod2(path, 384).catch(() => {
|
|
1293
|
-
});
|
|
1294
|
-
} catch (err) {
|
|
1295
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1296
|
-
process.stderr.write(`[conveyor-agent] claude credentials sync failed: ${message}
|
|
1297
|
-
`);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
function claudeJsonPath() {
|
|
1301
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
1302
|
-
return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
|
|
1303
|
-
}
|
|
1304
|
-
function asRecord(value) {
|
|
1305
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1306
|
-
}
|
|
1307
|
-
function parseClaudeJson(existingRaw) {
|
|
1308
|
-
if (!existingRaw || existingRaw.trim() === "") return {};
|
|
1309
|
-
try {
|
|
1310
|
-
return asRecord(JSON.parse(existingRaw));
|
|
1311
|
-
} catch {
|
|
1312
|
-
return {};
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
function seedWorkspaceTrust(config, trustCwd) {
|
|
1316
|
-
const projects = asRecord(config.projects);
|
|
1317
|
-
const entry = asRecord(projects[trustCwd]);
|
|
1318
|
-
if (entry.hasTrustDialogAccepted === true) return false;
|
|
1319
|
-
entry.hasTrustDialogAccepted = true;
|
|
1320
|
-
projects[trustCwd] = entry;
|
|
1321
|
-
config.projects = projects;
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
function planClaudeJsonSeed(existingRaw, trustCwd) {
|
|
1325
|
-
const config = parseClaudeJson(existingRaw);
|
|
1326
|
-
const isFresh = Object.keys(config).length === 0;
|
|
1327
|
-
let changed = false;
|
|
1328
|
-
if (config.hasCompletedOnboarding !== true) {
|
|
1329
|
-
config.hasCompletedOnboarding = true;
|
|
1330
|
-
changed = true;
|
|
1331
|
-
}
|
|
1332
|
-
if (config.bypassPermissionsModeAccepted !== true) {
|
|
1333
|
-
config.bypassPermissionsModeAccepted = true;
|
|
1334
|
-
changed = true;
|
|
1335
|
-
}
|
|
1336
|
-
if (isFresh && typeof config.theme !== "string") {
|
|
1337
|
-
config.theme = "dark";
|
|
1338
|
-
changed = true;
|
|
1339
|
-
}
|
|
1340
|
-
if (trustCwd && seedWorkspaceTrust(config, trustCwd)) {
|
|
1341
|
-
changed = true;
|
|
1342
|
-
}
|
|
1343
|
-
return changed ? JSON.stringify(config) : null;
|
|
1344
|
-
}
|
|
1345
|
-
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
1346
|
-
try {
|
|
1347
|
-
if (!isConveyorCloudEnv(env)) return;
|
|
1348
|
-
const path = claudeJsonPath();
|
|
1349
|
-
const contents = planClaudeJsonSeed(await readRaw(path), trustCwd);
|
|
1350
|
-
if (contents === null) return;
|
|
1351
|
-
await writeFile3(path, contents, "utf8");
|
|
1352
|
-
} catch (err) {
|
|
1353
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1354
|
-
process.stderr.write(`[conveyor-agent] claude onboarding seed failed: ${message}
|
|
1355
|
-
`);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
async function removeConveyorCredentials(env = process.env) {
|
|
1359
|
-
try {
|
|
1360
|
-
if (!isConveyorCloudEnv(env)) return;
|
|
1361
|
-
const path = claudeCredentialsPath();
|
|
1362
|
-
const existing = parseClaudeAiOauth(await readRaw(path));
|
|
1363
|
-
if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
await rm2(path, { force: true });
|
|
1367
|
-
} catch (err) {
|
|
1368
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1369
|
-
process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
|
|
1370
|
-
`);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// src/harness/pty/index.ts
|
|
1375
|
-
var PtyHarness = class {
|
|
1376
|
-
/**
|
|
1377
|
-
* `bridge` relays raw terminal I/O to/from the S2 server (and on to the S5
|
|
1378
|
-
* terminal). It is undefined for SDK-only callers and PTY runs that never
|
|
1379
|
-
* attach a relay; the session simply discards stdout in that case.
|
|
1380
|
-
*/
|
|
1381
|
-
constructor(bridge) {
|
|
1382
|
-
this.bridge = bridge;
|
|
1383
|
-
}
|
|
1384
|
-
bridge;
|
|
1385
|
-
async *executeQuery(opts) {
|
|
1386
|
-
const session = new PtySession(
|
|
1387
|
-
opts.prompt,
|
|
1388
|
-
opts.options,
|
|
1389
|
-
opts.resume ?? opts.options.resume,
|
|
1390
|
-
this.bridge
|
|
1391
|
-
);
|
|
1392
|
-
await ensureClaudeCredentials();
|
|
1393
|
-
await ensureClaudeOnboarding(process.env, opts.options.cwd);
|
|
1394
|
-
await session.start();
|
|
1395
|
-
try {
|
|
1396
|
-
for await (const event of session.events()) {
|
|
1397
|
-
yield event;
|
|
1398
|
-
}
|
|
1399
|
-
} finally {
|
|
1400
|
-
await session.teardown();
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
createMcpServer(config) {
|
|
1404
|
-
return new PtyMcpServer(config.name, config.tools);
|
|
1405
|
-
}
|
|
1406
|
-
};
|
|
1407
|
-
|
|
1408
|
-
// src/harness/index.ts
|
|
1409
|
-
function createHarness(kind = "sdk", ptyBridge) {
|
|
1410
|
-
return kind === "pty" ? new PtyHarness(ptyBridge) : new ClaudeCodeHarness();
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// src/utils/logger.ts
|
|
1414
|
-
function createServiceLogger(service) {
|
|
1415
|
-
const prefix = `[conveyor-agent:${service}]`;
|
|
1416
|
-
return {
|
|
1417
|
-
info(message, data) {
|
|
1418
|
-
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
1419
|
-
process.stderr.write(`${prefix} ${message}${extra}
|
|
1420
|
-
`);
|
|
1421
|
-
},
|
|
1422
|
-
warn(message, data) {
|
|
1423
|
-
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
1424
|
-
process.stderr.write(`${prefix} WARN ${message}${extra}
|
|
1425
|
-
`);
|
|
1426
|
-
},
|
|
1427
|
-
error(message, data) {
|
|
1428
|
-
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
1429
|
-
process.stderr.write(`${prefix} ERROR ${message}${extra}
|
|
1430
|
-
`);
|
|
1431
|
-
}
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
export {
|
|
1436
|
-
defineTool,
|
|
1437
|
-
sessionTranscriptPath,
|
|
1438
|
-
configHomePlansDir,
|
|
1439
|
-
ensureClaudeCredentials,
|
|
1440
|
-
removeConveyorCredentials,
|
|
1441
|
-
createHarness,
|
|
1442
|
-
createServiceLogger
|
|
1443
|
-
};
|
|
1444
|
-
//# sourceMappingURL=chunk-TN2YYT2V.js.map
|