@heretyc/subagent-mcp 2.8.2 → 2.8.5

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,478 @@
1
+ import { spawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { PassThrough } from "node:stream";
4
+ class LogicalProcess extends EventEmitter {
5
+ pid;
6
+ stdout = new PassThrough();
7
+ stderr = new PassThrough();
8
+ killed = false;
9
+ exitCode = null;
10
+ closed = false;
11
+ spawned = false;
12
+ constructor(pid, autoSpawn = false) {
13
+ super();
14
+ this.pid = pid;
15
+ this.on("error", () => { });
16
+ if (autoSpawn)
17
+ setImmediate(() => this.emit("spawn"));
18
+ }
19
+ emit(eventName, ...args) {
20
+ if (eventName === "spawn")
21
+ this.spawned = true;
22
+ return super.emit(eventName, ...args);
23
+ }
24
+ once(eventName, listener) {
25
+ if (eventName === "spawn" && this.spawned) {
26
+ queueMicrotask(() => listener.call(this));
27
+ return this;
28
+ }
29
+ return super.once(eventName, listener);
30
+ }
31
+ close(code, signal = null) {
32
+ if (this.closed)
33
+ return;
34
+ this.closed = true;
35
+ this.exitCode = code;
36
+ this.emit("exit", code, signal);
37
+ this.stdout.end();
38
+ this.stderr.end();
39
+ this.emit("close", code, signal);
40
+ }
41
+ fail(error) {
42
+ if (this.closed)
43
+ return;
44
+ this.emit("error", error);
45
+ this.close(-1);
46
+ }
47
+ kill(signal = "SIGKILL") {
48
+ if (this.closed)
49
+ return false;
50
+ this.killed = true;
51
+ this.close(null, signal);
52
+ return true;
53
+ }
54
+ }
55
+ class AsyncInputQueue {
56
+ maxDepth;
57
+ items = [];
58
+ takers = [];
59
+ closed = false;
60
+ constructor(maxDepth = 32) {
61
+ this.maxDepth = maxDepth;
62
+ }
63
+ push(item) {
64
+ if (this.closed)
65
+ return Promise.reject(new Error("provider input stream is closed"));
66
+ if (this.items.length >= this.maxDepth) {
67
+ return Promise.reject(new Error(`provider input queue is full (${this.maxDepth})`));
68
+ }
69
+ const taker = this.takers.shift();
70
+ if (taker)
71
+ taker({ value: item, done: false });
72
+ else
73
+ this.items.push(item);
74
+ return Promise.resolve();
75
+ }
76
+ close() {
77
+ if (this.closed)
78
+ return;
79
+ this.closed = true;
80
+ for (const taker of this.takers.splice(0))
81
+ taker({ value: undefined, done: true });
82
+ }
83
+ [Symbol.asyncIterator]() {
84
+ return {
85
+ next: () => {
86
+ const item = this.items.shift();
87
+ if (item !== undefined)
88
+ return Promise.resolve({ value: item, done: false });
89
+ if (this.closed)
90
+ return Promise.resolve({ value: undefined, done: true });
91
+ return new Promise((resolve) => this.takers.push(resolve));
92
+ },
93
+ };
94
+ }
95
+ }
96
+ function writeLine(stdin, payload) {
97
+ return new Promise((resolve, reject) => {
98
+ if (!stdin || stdin.destroyed) {
99
+ reject(new Error("provider input stream is closed"));
100
+ return;
101
+ }
102
+ const text = `${JSON.stringify(payload)}\n`;
103
+ const writable = stdin;
104
+ writable.write(text, (err) => {
105
+ if (err)
106
+ reject(err);
107
+ else
108
+ resolve();
109
+ });
110
+ });
111
+ }
112
+ function userMessage(text) {
113
+ return {
114
+ type: "user",
115
+ message: { role: "user", content: text },
116
+ parent_tool_use_id: null,
117
+ };
118
+ }
119
+ function textInput(text) {
120
+ return { type: "text", text, text_elements: [] };
121
+ }
122
+ class MockJsonlDriver {
123
+ child;
124
+ provider;
125
+ process;
126
+ queue = Promise.resolve();
127
+ turn = 0;
128
+ constructor(child, provider) {
129
+ this.child = child;
130
+ this.provider = provider;
131
+ this.process = new LogicalProcess(child.pid);
132
+ child.once("spawn", () => this.process.emit("spawn"));
133
+ child.once("error", (err) => this.process.fail(err));
134
+ child.once("close", (code, signal) => {
135
+ if (!this.process.killed)
136
+ this.process.close(code, signal);
137
+ });
138
+ child.stdout?.on("data", (chunk) => this.process.stdout.write(chunk));
139
+ child.stderr?.on("data", (chunk) => this.process.stderr.write(chunk));
140
+ // Swallow stdin pipe errors (e.g. EPIPE writing to an exited child): the
141
+ // writeLine() callback already surfaces the failure; without this listener
142
+ // Node throws the socket's unhandled 'error' event and crashes the process.
143
+ child.stdin?.on("error", () => { });
144
+ }
145
+ get closed() {
146
+ return this.process.killed || this.child.killed || this.child.exitCode !== null;
147
+ }
148
+ start(message) {
149
+ return this.send(message);
150
+ }
151
+ send(message) {
152
+ const next = this.queue.then(async () => {
153
+ if (this.closed)
154
+ throw new Error("provider driver is closed");
155
+ this.turn += 1;
156
+ await writeLine(this.child.stdin, {
157
+ type: "turn.start",
158
+ provider: this.provider,
159
+ turn: this.turn,
160
+ message,
161
+ });
162
+ });
163
+ this.queue = next.catch(() => { });
164
+ return next;
165
+ }
166
+ kill() {
167
+ this.process.killed = true;
168
+ this.child.stdin?.destroy();
169
+ this.child.kill("SIGKILL");
170
+ this.process.kill("SIGKILL");
171
+ }
172
+ }
173
+ export class CodexAppServerDriver {
174
+ child;
175
+ options;
176
+ process;
177
+ pending = new Map();
178
+ queuedTurns = [];
179
+ nextId = 1;
180
+ stdoutBuf = "";
181
+ threadId = null;
182
+ activeTurnId = null;
183
+ drainActive = false;
184
+ initialized = false;
185
+ turnInFlight = false;
186
+ maxQueueDepth = 32;
187
+ constructor(child, options) {
188
+ this.child = child;
189
+ this.options = options;
190
+ this.process = new LogicalProcess(child.pid);
191
+ child.once("spawn", () => this.process.emit("spawn"));
192
+ child.once("error", (err) => this.fail(err));
193
+ child.once("close", (code, signal) => {
194
+ if (!this.process.killed)
195
+ this.process.close(code, signal);
196
+ this.rejectPending(new Error(`codex app-server exited before responding (code=${code ?? signal})`));
197
+ });
198
+ child.stdout?.on("data", (chunk) => this.onStdout(chunk.toString()));
199
+ child.stderr?.on("data", (chunk) => this.process.stderr.write(chunk));
200
+ // Swallow stdin pipe errors (e.g. EPIPE writing to an exited child): the
201
+ // writeLine() callback already surfaces the failure; without this listener
202
+ // Node throws the socket's unhandled 'error' event and crashes the process.
203
+ child.stdin?.on("error", () => { });
204
+ }
205
+ get closed() {
206
+ return this.process.killed || this.child.killed || this.child.exitCode !== null;
207
+ }
208
+ async start(message) {
209
+ await this.request("initialize", {
210
+ clientInfo: { name: "subagent-mcp", title: "subagent-mcp", version: "0.0.0" },
211
+ capabilities: null,
212
+ });
213
+ await this.notify("initialized");
214
+ const thread = await this.request("thread/start", {
215
+ model: this.options.model,
216
+ cwd: this.options.cwd,
217
+ approvalPolicy: "never",
218
+ sandbox: "danger-full-access",
219
+ ephemeral: true,
220
+ threadSource: "subagent",
221
+ });
222
+ this.threadId = String(thread.result?.thread?.id ?? "");
223
+ if (!this.threadId)
224
+ throw new Error("codex app-server thread/start returned no thread id");
225
+ this.initialized = true;
226
+ await this.startTurn(message);
227
+ }
228
+ send(message) {
229
+ if (this.closed)
230
+ return Promise.reject(new Error("provider driver is closed"));
231
+ if (!this.initialized || !this.threadId) {
232
+ return Promise.reject(new Error("codex app-server thread is not initialized"));
233
+ }
234
+ if (this.queuedTurns.length >= this.maxQueueDepth) {
235
+ return Promise.reject(new Error(`provider input queue is full (${this.maxQueueDepth})`));
236
+ }
237
+ this.queuedTurns.push(message);
238
+ void this.drainQueuedTurns();
239
+ return Promise.resolve();
240
+ }
241
+ kill() {
242
+ this.process.killed = true;
243
+ this.child.stdin?.destroy();
244
+ this.child.kill("SIGKILL");
245
+ this.process.kill("SIGKILL");
246
+ this.rejectPending(new Error("codex app-server driver was killed"));
247
+ this.queuedTurns.length = 0;
248
+ }
249
+ async drainQueuedTurns() {
250
+ if (this.drainActive)
251
+ return;
252
+ this.drainActive = true;
253
+ try {
254
+ while (!this.closed && !this.turnInFlight && this.queuedTurns.length > 0) {
255
+ const next = this.queuedTurns.shift();
256
+ if (next !== undefined)
257
+ await this.startTurn(next);
258
+ }
259
+ }
260
+ catch (error) {
261
+ const msg = error instanceof Error ? error.message : String(error);
262
+ this.process.stderr.write(`[codex app-server driver] ${msg}\n`);
263
+ this.process.close(1);
264
+ }
265
+ finally {
266
+ this.drainActive = false;
267
+ }
268
+ }
269
+ async startTurn(message) {
270
+ if (this.closed)
271
+ throw new Error("provider driver is closed");
272
+ if (!this.threadId)
273
+ throw new Error("codex app-server thread is not initialized");
274
+ this.turnInFlight = true;
275
+ try {
276
+ const response = await this.request("turn/start", {
277
+ threadId: this.threadId,
278
+ input: [textInput(message)],
279
+ cwd: this.options.cwd,
280
+ model: this.options.model,
281
+ effort: this.options.effort,
282
+ approvalPolicy: "never",
283
+ sandboxPolicy: { type: "dangerFullAccess" },
284
+ });
285
+ const turn = response.result?.turn;
286
+ this.activeTurnId = typeof turn?.id === "string" ? turn.id : this.activeTurnId;
287
+ }
288
+ catch (error) {
289
+ this.turnInFlight = false;
290
+ throw error;
291
+ }
292
+ }
293
+ async request(method, params) {
294
+ const id = this.nextId++;
295
+ const promise = new Promise((resolve, reject) => {
296
+ const timer = setTimeout(() => {
297
+ this.pending.delete(id);
298
+ reject(new Error(`codex app-server request timed out: ${method}`));
299
+ }, 15000);
300
+ this.pending.set(id, { resolve, reject, timer });
301
+ });
302
+ await writeLine(this.child.stdin, { id, method, params });
303
+ return promise;
304
+ }
305
+ notify(method, params) {
306
+ return writeLine(this.child.stdin, params === undefined ? { method } : { method, params });
307
+ }
308
+ onStdout(chunk) {
309
+ this.stdoutBuf += chunk;
310
+ const lines = this.stdoutBuf.split("\n");
311
+ this.stdoutBuf = lines.pop() ?? "";
312
+ for (const line of lines) {
313
+ if (!line.trim())
314
+ continue;
315
+ this.process.stdout.write(line.replace(/\r$/, "") + "\n");
316
+ this.handleProtocolLine(line);
317
+ }
318
+ }
319
+ handleProtocolLine(line) {
320
+ let message;
321
+ try {
322
+ message = JSON.parse(line);
323
+ }
324
+ catch {
325
+ return;
326
+ }
327
+ if (typeof message.id === "number" && this.pending.has(message.id)) {
328
+ const pending = this.pending.get(message.id);
329
+ this.pending.delete(message.id);
330
+ clearTimeout(pending.timer);
331
+ if (message.error && typeof message.error === "object") {
332
+ const err = message.error;
333
+ pending.reject(new Error(String(err.message ?? "codex app-server error")));
334
+ }
335
+ else {
336
+ pending.resolve(message);
337
+ }
338
+ }
339
+ if (message.method === "turn/started" && message.params && typeof message.params === "object") {
340
+ const turn = (message.params.turn ?? {});
341
+ if (typeof turn.id === "string")
342
+ this.activeTurnId = turn.id;
343
+ }
344
+ if (message.method === "turn/completed") {
345
+ this.activeTurnId = null;
346
+ this.turnInFlight = false;
347
+ void this.drainQueuedTurns();
348
+ }
349
+ }
350
+ fail(error) {
351
+ this.process.fail(error);
352
+ this.rejectPending(error);
353
+ }
354
+ rejectPending(error) {
355
+ for (const pending of this.pending.values()) {
356
+ clearTimeout(pending.timer);
357
+ pending.reject(error);
358
+ }
359
+ this.pending.clear();
360
+ this.turnInFlight = false;
361
+ }
362
+ }
363
+ export class ClaudeSdkDriver {
364
+ queryFn;
365
+ process = new LogicalProcess(undefined, true);
366
+ input = new AsyncInputQueue();
367
+ abortController = new AbortController();
368
+ queue = Promise.resolve();
369
+ queryHandle = null;
370
+ closedFlag = false;
371
+ constructor(queryFn) {
372
+ this.queryFn = queryFn;
373
+ }
374
+ get closed() {
375
+ return this.closedFlag || this.process.killed;
376
+ }
377
+ open(options) {
378
+ const sdkOptions = {
379
+ abortController: this.abortController,
380
+ cwd: options.cwd,
381
+ env: options.env,
382
+ model: options.model,
383
+ pathToClaudeCodeExecutable: options.command,
384
+ permissionMode: "bypassPermissions",
385
+ allowDangerouslySkipPermissions: true,
386
+ tools: { type: "preset", preset: "claude_code" },
387
+ maxTurns: 50,
388
+ includePartialMessages: true,
389
+ };
390
+ if (options.effort !== "none" && options.effort !== "ultracode") {
391
+ sdkOptions.effort = options.effort;
392
+ }
393
+ if (options.ucSettingsPath)
394
+ sdkOptions.settings = options.ucSettingsPath;
395
+ const query = this.queryFn({ prompt: this.input, options: sdkOptions });
396
+ this.queryHandle = query;
397
+ void this.pump(query);
398
+ }
399
+ start(message) {
400
+ return this.send(message);
401
+ }
402
+ send(message) {
403
+ const next = this.queue.then(() => {
404
+ if (this.closed)
405
+ throw new Error("provider driver is closed");
406
+ return this.input.push(userMessage(message));
407
+ });
408
+ this.queue = next.catch(() => { });
409
+ return next;
410
+ }
411
+ kill() {
412
+ if (this.closedFlag)
413
+ return;
414
+ this.closedFlag = true;
415
+ this.input.close();
416
+ this.abortController.abort();
417
+ this.queryHandle?.close?.();
418
+ this.process.kill("SIGKILL");
419
+ }
420
+ async pump(query) {
421
+ try {
422
+ for await (const message of query) {
423
+ this.process.stdout.write(`${JSON.stringify(message)}\n`);
424
+ }
425
+ this.closedFlag = true;
426
+ this.process.close(0);
427
+ }
428
+ catch (error) {
429
+ if (!this.process.killed) {
430
+ this.closedFlag = true;
431
+ this.process.stderr.write(error instanceof Error ? error.message : String(error));
432
+ this.process.close(1);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ export async function createProviderDriver(options) {
438
+ if ((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
439
+ (options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl")) {
440
+ const mockScript = process.env.SUBAGENT_MOCK_DRIVER_SCRIPT;
441
+ const child = mockScript
442
+ ? spawn(process.execPath, [mockScript, options.provider], {
443
+ cwd: options.cwd,
444
+ env: options.env,
445
+ stdio: ["pipe", "pipe", "pipe"],
446
+ windowsHide: true,
447
+ })
448
+ : spawn(options.command, [], {
449
+ cwd: options.cwd,
450
+ env: options.env,
451
+ stdio: ["pipe", "pipe", "pipe"],
452
+ windowsHide: true,
453
+ });
454
+ return new MockJsonlDriver(child, options.provider);
455
+ }
456
+ if (options.provider === "claude") {
457
+ let sdk;
458
+ try {
459
+ sdk = (await import("@anthropic-ai/claude-agent-sdk"));
460
+ }
461
+ catch {
462
+ throw new Error("Claude interactive driver requires @anthropic-ai/claude-agent-sdk; no one-shot CLI fallback is available");
463
+ }
464
+ if (typeof sdk.query !== "function") {
465
+ throw new Error("Claude Agent SDK does not expose the query() streaming API");
466
+ }
467
+ const driver = new ClaudeSdkDriver(sdk.query);
468
+ driver.open(options);
469
+ return driver;
470
+ }
471
+ const child = spawn(options.command, options.args, {
472
+ cwd: options.cwd,
473
+ env: options.env,
474
+ stdio: ["pipe", "pipe", "pipe"],
475
+ windowsHide: true,
476
+ });
477
+ return new CodexAppServerDriver(child, options);
478
+ }
package/dist/effort.js CHANGED
@@ -38,11 +38,11 @@ export function resolveEffort(provider, model, effort) {
38
38
  }
39
39
  return { kind: "flag", value: "high" };
40
40
  }
41
- export function buildCommand(provider, model, effort, prompt, cwd) {
41
+ export function buildCommand(provider, model, effort, cwd) {
42
42
  const mapped = mapModel(provider, model);
43
43
  const er = resolveEffort(provider, model, effort);
44
44
  if (provider === "claude") {
45
- const args = ["-p", "--model", mapped];
45
+ const args = ["--model", mapped];
46
46
  if (er.kind === "flag") {
47
47
  args.push("--effort", er.value);
48
48
  }
@@ -50,28 +50,19 @@ export function buildCommand(provider, model, effort, prompt, cwd) {
50
50
  const ucSettingsPath = join(tmpdir(), `subagent-uc-${randomUUID()}.json`);
51
51
  writeFileSync(ucSettingsPath, '{"ultracode":true}');
52
52
  args.push("--settings", ucSettingsPath);
53
- args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50", "--output-format", "stream-json", "--verbose");
53
+ args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
54
54
  return { args, ucSettingsPath };
55
55
  }
56
- args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50", "--output-format", "stream-json", "--verbose");
56
+ args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
57
57
  return { args };
58
58
  }
59
59
  else {
60
60
  // codex
61
- const effortValue = er.value;
61
+ void er.value;
62
62
  return {
63
63
  args: [
64
- "exec",
65
- "-C",
66
- cwd,
67
- "-m",
68
- "gpt-5.5",
69
- "-c",
70
- `model_reasoning_effort="${effortValue}"`,
71
- "--dangerously-bypass-approvals-and-sandbox",
72
- "--skip-git-repo-check",
73
- "--json",
74
- prompt,
64
+ "app-server",
65
+ "--stdio",
75
66
  ],
76
67
  };
77
68
  }
@@ -43,7 +43,8 @@ export const claudeAdapter = {
43
43
  return countJsonlType(transcriptPath, "user");
44
44
  },
45
45
  fullDirectiveFile: "orchestration-claude.md",
46
- offTurnFile: "off-turn-reminder.md",
46
+ shortOnFile: "short-on.md",
47
+ shortOffFile: "short-off.md",
47
48
  carryoverDirectiveFile: "carryover-claude.md",
48
49
  reminderOnFile: "reminder-on.md",
49
50
  reminderOffFile: "reminder-off-claude.md",
@@ -58,7 +58,8 @@ export const codexAdapter = {
58
58
  return countJsonlType(transcriptPath, "turn_context");
59
59
  },
60
60
  fullDirectiveFile: "orchestration-codex.md",
61
- offTurnFile: "off-turn-reminder.md",
61
+ shortOnFile: "short-on.md",
62
+ shortOffFile: "short-off.md",
62
63
  carryoverDirectiveFile: "carryover-codex.md",
63
64
  reminderOnFile: "reminder-on.md",
64
65
  reminderOffFile: "reminder-off-codex.md",