@agentproto/driver-agent-cli 0.2.0 → 0.4.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.
@@ -2,8 +2,11 @@ import { z } from 'zod';
2
2
  import { Writable, Readable } from 'stream';
3
3
  import { createAcpClient } from '@agentproto/acp/client';
4
4
  import { spawn } from 'child_process';
5
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, rmdirSync, mkdtempSync } from 'fs';
5
6
  import { createInterface } from 'readline';
7
+ import { join } from 'path';
6
8
  import { randomUUID } from 'crypto';
9
+ import { tmpdir } from 'os';
7
10
  import { createDoctype } from '@agentproto/define-doctype';
8
11
 
9
12
  /**
@@ -150,12 +153,21 @@ var authSchema = z.object({
150
153
  var sessionSchema = z.object({
151
154
  mode: z.enum(["ephemeral", "persistent", "resumable"]).default("ephemeral"),
152
155
  idle_timeout_ms: z.number().int().min(1e3).default(6e5),
156
+ // No default: undefined disables the per-turn watchdog entirely. Only
157
+ // adapters known to sometimes drop the final `prompt` response (e.g.
158
+ // hermes) should declare this — applying it broadly would risk
159
+ // false-positiving on another adapter's legitimately long turns.
160
+ turn_idle_timeout_ms: z.number().int().min(1e3).optional(),
153
161
  max_turns: z.number().int().positive().optional(),
154
162
  context_carryover: z.boolean().default(true)
155
163
  }).strict();
156
164
  var modelsSchema = z.object({
157
165
  default: z.string().optional(),
158
166
  allowed: z.array(z.string()).optional(),
167
+ // Model-id patterns the adapter must never route to (case-insensitive,
168
+ // trailing `*` = prefix match). Enforced at compose time — see
169
+ // AgentCliModels.deny. Reserves premium providers for dedicated adapters.
170
+ deny: z.array(z.string()).optional(),
159
171
  env: z.record(z.string(), z.string()).optional(),
160
172
  // How a model is selected at session start: "config" (ACP
161
173
  // set_config_option, default) | "command" (a `/model <id>` control turn,
@@ -182,8 +194,21 @@ var capabilitiesSchema = z.object({
182
194
  var modeSchema = z.object({
183
195
  id: z.string().regex(MODE_ID_PATTERN),
184
196
  description: z.string().optional(),
197
+ bin_args_prepend: z.array(z.string()).optional(),
185
198
  bin_args_append: z.array(z.string()).optional(),
186
- env: z.record(z.string(), z.string()).optional()
199
+ env: z.record(z.string(), z.string()).optional(),
200
+ // Env keys to DELETE from the spawn env (auth hygiene for gateway
201
+ // modes — see AgentCliMode.env_unset). Applied at the runtime merge
202
+ // point, not as a static set.
203
+ env_unset: z.array(z.string()).optional(),
204
+ // Honest support status surfaced to clients. Absent ⇒ "active".
205
+ status: z.enum(["active", "noop", "planned"]).optional(),
206
+ status_note: z.string().optional(),
207
+ // How the host activates this mode: "bin_args" (default, argv/env
208
+ // composed at spawn) | "config" (no CLI surface — forwarded to the ACP
209
+ // arm's connect({mode}) and applied via session/set_config_option
210
+ // configId:"mode", e.g. opencode).
211
+ apply: z.enum(["bin_args", "config"]).optional()
187
212
  }).strict();
188
213
  var optionSchema = z.object({
189
214
  id: z.string().regex(OPTION_ID_PATTERN),
@@ -193,9 +218,14 @@ var optionSchema = z.object({
193
218
  default: z.union([z.boolean(), z.number(), z.string()]).optional(),
194
219
  min: z.number().int().optional(),
195
220
  max: z.number().int().optional(),
221
+ bin_args_prepend: z.array(z.string()).optional(),
196
222
  bin_args_template: z.array(z.string()).optional(),
197
223
  bin_args_append_when_true: z.array(z.string()).optional(),
198
- env: z.record(z.string(), z.string()).optional()
224
+ env: z.record(z.string(), z.string()).optional(),
225
+ // Env keys to DELETE from the spawn env when this option is active
226
+ // (auth hygiene for value-bearing gateway options — see
227
+ // AgentCliOption.env_unset). Symmetric with modeSchema.env_unset.
228
+ env_unset: z.array(z.string()).optional()
199
229
  }).strict().refine(
200
230
  // type === "enum" REQUIRES an `enum` array. The JSON Schema enforces
201
231
  // this via `if/then`; the zod side mirrors with a refine so both
@@ -203,6 +233,17 @@ var optionSchema = z.object({
203
233
  (o) => o.type !== "enum" || Array.isArray(o.enum) && o.enum.length > 0,
204
234
  { message: "option.type === 'enum' requires a non-empty `enum` array" }
205
235
  );
236
+ var presetSchema = z.object({
237
+ id: z.string().min(1),
238
+ label: z.string().min(1),
239
+ description: z.string().optional(),
240
+ schemaFlavor: z.enum(["anthropic", "openai"]),
241
+ baseUrl: z.string().min(1),
242
+ keyEnv: z.string().min(1),
243
+ scrubEnv: z.array(z.string()).optional(),
244
+ defaultModel: z.string().optional(),
245
+ homepage: z.string().optional()
246
+ }).strict();
206
247
  var continuationStrategyIdSchema = z.enum(CONTINUATION_STRATEGY_IDS);
207
248
  var continuationSchema = z.object({
208
249
  default: continuationStrategyIdSchema,
@@ -225,6 +266,16 @@ var mcpBlockSchema = z.object({
225
266
  transport: z.enum(["stdio", "http", "sse"]),
226
267
  url: z.string().url().optional()
227
268
  }).strict();
269
+ var printConfigSchema = z.object({
270
+ prompt_flag: z.string().optional(),
271
+ output_format: z.array(z.string()).optional(),
272
+ pre_prompt: z.array(z.string()).optional(),
273
+ resume: z.object({
274
+ flag: z.string(),
275
+ kind: z.enum(["value", "boolean"])
276
+ }).strict().optional(),
277
+ event_schema: z.enum(["claude-stream-json", "mastra-jsonl"]).optional()
278
+ }).strict().optional();
228
279
  var agentCliFrontmatterSchema = z.object({
229
280
  name: z.string().min(1).max(80),
230
281
  id: z.string().regex(ID_PATTERN),
@@ -242,11 +293,15 @@ var agentCliFrontmatterSchema = z.object({
242
293
  acp: z.string().optional(),
243
294
  mcp: mcpBlockSchema.optional(),
244
295
  adapter: z.string().regex(ADAPTER_PATTERN).optional(),
296
+ print: printConfigSchema,
245
297
  session: sessionSchema.optional(),
246
298
  models: modelsSchema.optional(),
247
299
  capabilities: capabilitiesSchema.optional(),
248
300
  modes: z.array(modeSchema).optional(),
249
301
  options: z.array(optionSchema).optional(),
302
+ // AIP-45 gateway presets this adapter can drive (merged into the
303
+ // provider-preset catalog; built-in wins on id collision).
304
+ presets: z.array(presetSchema).optional(),
250
305
  continuation: continuationSchema.optional(),
251
306
  requires: z.object({
252
307
  os: z.array(z.enum(["darwin", "linux", "windows"])).optional(),
@@ -325,6 +380,21 @@ var autoAllowPermissionHandler = ({
325
380
  const allow = options.find((o) => typeof o.kind === "string" && o.kind.startsWith("allow_")) ?? options[0];
326
381
  return { outcome: { outcome: "selected", optionId: allow.optionId } };
327
382
  };
383
+ var PLAN_MODE_EXIT_TOOL_CALL_KIND = "switch_mode";
384
+ var planModePermissionHandler = (params) => {
385
+ const { options, toolCall } = params;
386
+ if (!options || options.length === 0) {
387
+ return { outcome: { outcome: "cancelled" } };
388
+ }
389
+ if (toolCall?.kind === PLAN_MODE_EXIT_TOOL_CALL_KIND) {
390
+ const keepPlanning = options.find((o) => o.kind === "reject_once");
391
+ return keepPlanning ? { outcome: { outcome: "selected", optionId: keepPlanning.optionId } } : { outcome: { outcome: "cancelled" } };
392
+ }
393
+ return autoAllowPermissionHandler(params);
394
+ };
395
+ function defaultPermissionHandlerForMode(requestedMode) {
396
+ return requestedMode === "plan" ? planModePermissionHandler : autoAllowPermissionHandler;
397
+ }
328
398
  function createAcpProtocolArm(options) {
329
399
  const { child, cwd } = options;
330
400
  if (!child.stdin || !child.stdout) {
@@ -342,7 +412,7 @@ function createAcpProtocolArm(options) {
342
412
  return session?.sessionId;
343
413
  },
344
414
  async connect(opts) {
345
- const permissionHandler = options.onPermissionRequest ?? autoAllowPermissionHandler;
415
+ const permissionHandler = options.onPermissionRequest ?? defaultPermissionHandlerForMode(options.requestedMode);
346
416
  client = await createAcpClient({
347
417
  output,
348
418
  input,
@@ -350,6 +420,8 @@ function createAcpProtocolArm(options) {
350
420
  capabilities: {
351
421
  fs: { readTextFile: true, writeTextFile: true }
352
422
  },
423
+ onActivity: opts.onActivity,
424
+ turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
353
425
  // Wire the permission handler so the agent's `session/request_permission`
354
426
  // callbacks get a real answer instead of bubbling up as
355
427
  // "AcpClient.requestPermission: no handler configured" → which
@@ -370,7 +442,8 @@ function createAcpProtocolArm(options) {
370
442
  cwd,
371
443
  mcpServers: opts.mcpServers,
372
444
  ...opts.model ? { model: opts.model } : {},
373
- ...opts.effort ? { effort: opts.effort } : {}
445
+ ...opts.effort ? { effort: opts.effort } : {},
446
+ ...opts.mode ? { mode: opts.mode } : {}
374
447
  });
375
448
  }
376
449
  },
@@ -395,9 +468,29 @@ function createAcpProtocolArm(options) {
395
468
  }
396
469
  };
397
470
  }
471
+
472
+ // src/mcp-servers.ts
473
+ function toFileBasedMcpServers(servers) {
474
+ if (!servers || servers.length === 0) return void 0;
475
+ const out = {};
476
+ for (const s of servers) {
477
+ out[s.name] = s.transport === "stdio" ? { command: s.ref ?? "" } : { url: s.ref ?? "" };
478
+ }
479
+ return out;
480
+ }
481
+ var DEFAULT_OUTPUT = ["--output-format", "stream-json"];
482
+ var DEFAULT_PRE_PROMPT = ["--no-interactive"];
483
+ var DEFAULT_RESUME = { flag: "--resume", kind: "value" };
398
484
  function createPrintSession(opts) {
485
+ const config = opts.printConfig;
486
+ const outputFormat = config?.output_format ?? DEFAULT_OUTPUT;
487
+ const prePrompt = config?.pre_prompt ?? DEFAULT_PRE_PROMPT;
488
+ const promptFlag = config?.prompt_flag;
489
+ const resumeCfg = config?.resume ?? DEFAULT_RESUME;
490
+ const eventSchema = config?.event_schema ?? "claude-stream-json";
399
491
  let sessionId = opts.resumeSessionId ?? "";
400
492
  let activeChild = null;
493
+ const mcpRestore = setupMcpConfigFile(opts, eventSchema);
401
494
  return {
402
495
  get sessionId() {
403
496
  return sessionId;
@@ -406,13 +499,18 @@ function createPrintSession(opts) {
406
499
  const prompt = extractPromptText(message);
407
500
  const args = [
408
501
  ...opts.baseArgs,
409
- "--print",
410
- "--output-format",
411
- "stream-json",
412
- "--no-interactive",
413
- ...sessionId ? ["--resume", sessionId] : [],
414
- prompt
502
+ ...outputFormat,
503
+ ...prePrompt
415
504
  ];
505
+ if (sessionId) {
506
+ args.push(resumeCfg.flag);
507
+ if (resumeCfg.kind === "value") args.push(sessionId);
508
+ }
509
+ if (promptFlag) {
510
+ args.push(promptFlag, prompt);
511
+ } else {
512
+ args.push(prompt);
513
+ }
416
514
  const child = spawn(opts.bin, args, {
417
515
  cwd: opts.cwd,
418
516
  env: opts.env,
@@ -430,32 +528,38 @@ function createPrintSession(opts) {
430
528
  }
431
529
  });
432
530
  try {
433
- if (!child.stdout) throw new Error("print-arm: child has no stdout pipe");
434
- const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
531
+ if (!child.stdout)
532
+ throw new Error("print-arm: child has no stdout pipe");
435
533
  let capturedSessionId = "";
436
- for await (const line of rl) {
437
- if (!line.trim()) continue;
534
+ const mastraState = eventSchema === "mastra-jsonl" ? createMastraMapperState() : void 0;
535
+ const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
536
+ const queue = new EventQueue();
537
+ rl.on("line", (line) => {
538
+ if (!line.trim()) return;
438
539
  let evt;
439
540
  try {
440
541
  evt = JSON.parse(line);
441
542
  } catch {
442
- continue;
443
- }
444
- if (evt.type === "result" && typeof evt.session_id === "string" && evt.session_id) {
445
- capturedSessionId = evt.session_id;
543
+ return;
446
544
  }
545
+ const csid = captureSessionId(evt, eventSchema);
546
+ if (csid) capturedSessionId = csid;
447
547
  const sid = capturedSessionId || sessionId || "";
448
- const mapped = mapEvent(evt, sid, stderrLines);
449
- if (!mapped) continue;
548
+ const mapped = mapEvent(evt, sid, stderrLines, eventSchema, mastraState);
549
+ if (mapped) queue.push(mapped);
550
+ });
551
+ rl.once("close", () => queue.end());
552
+ for await (const mapped of queue) {
450
553
  yield mapped;
451
554
  }
452
555
  const exitCode = await waitForExit(child);
453
556
  if (capturedSessionId) sessionId = capturedSessionId;
454
557
  if (exitCode !== 0 && exitCode !== null) {
558
+ const binLabel = eventSchema === "mastra-jsonl" ? "mastracode" : "claude";
455
559
  const errEvt = {
456
560
  kind: "error",
457
561
  error: {
458
- message: `claude exited with code ${exitCode}`,
562
+ message: `${binLabel} exited with code ${exitCode}`,
459
563
  ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
460
564
  }
461
565
  };
@@ -470,6 +574,75 @@ function createPrintSession(opts) {
470
574
  },
471
575
  async close() {
472
576
  activeChild?.kill("SIGTERM");
577
+ mcpRestore?.();
578
+ }
579
+ };
580
+ }
581
+ function setupMcpConfigFile(opts, eventSchema) {
582
+ if (eventSchema !== "mastra-jsonl") return void 0;
583
+ if (!opts.mcpServers || opts.mcpServers.length === 0) return void 0;
584
+ const dir = join(opts.cwd, ".mastracode");
585
+ const file = join(dir, "mcp.json");
586
+ const injected = toFileBasedMcpServers(opts.mcpServers);
587
+ if (!injected) return void 0;
588
+ const fileExisted = existsSync(file);
589
+ let originalContent;
590
+ if (fileExisted) {
591
+ try {
592
+ originalContent = readFileSync(file, "utf8");
593
+ } catch {
594
+ originalContent = void 0;
595
+ }
596
+ }
597
+ const dirExisted = existsSync(dir);
598
+ if (!dirExisted) {
599
+ mkdirSync(dir, { recursive: true });
600
+ }
601
+ let existingServers = {};
602
+ if (originalContent !== void 0) {
603
+ try {
604
+ const parsed = JSON.parse(originalContent);
605
+ if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
606
+ existingServers = parsed.mcpServers;
607
+ }
608
+ } catch {
609
+ existingServers = {};
610
+ }
611
+ }
612
+ const merged = { ...existingServers, ...injected };
613
+ const output = JSON.stringify({ mcpServers: merged }, null, 2);
614
+ writeFileSync(file, output, "utf8");
615
+ return () => {
616
+ try {
617
+ if (!fileExisted) {
618
+ rmSync(file, { force: true });
619
+ if (!dirExisted) {
620
+ try {
621
+ rmdirSync(dir);
622
+ } catch {
623
+ }
624
+ }
625
+ } else if (originalContent !== void 0) {
626
+ let current = {};
627
+ try {
628
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
629
+ if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
630
+ current = parsed.mcpServers;
631
+ }
632
+ } catch {
633
+ writeFileSync(file, originalContent, "utf8");
634
+ return;
635
+ }
636
+ for (const key of Object.keys(injected)) {
637
+ delete current[key];
638
+ }
639
+ if (Object.keys(current).length === 0) {
640
+ writeFileSync(file, originalContent, "utf8");
641
+ } else {
642
+ writeFileSync(file, JSON.stringify({ mcpServers: current }, null, 2), "utf8");
643
+ }
644
+ }
645
+ } catch {
473
646
  }
474
647
  };
475
648
  }
@@ -484,7 +657,33 @@ function extractPromptText(message) {
484
657
  }
485
658
  return JSON.stringify(message);
486
659
  }
487
- function mapEvent(evt, sessionId, stderrLines) {
660
+ function captureSessionId(evt, schema) {
661
+ switch (schema) {
662
+ case "claude-stream-json":
663
+ if (evt.type === "result" && typeof evt.session_id === "string" && evt.session_id)
664
+ return evt.session_id;
665
+ return null;
666
+ case "mastra-jsonl":
667
+ if (evt.type === "result" && typeof evt.threadId === "string" && evt.threadId)
668
+ return evt.threadId;
669
+ return null;
670
+ }
671
+ }
672
+ function mapEvent(evt, sessionId, stderrLines, schema, mastraState) {
673
+ switch (schema) {
674
+ case "claude-stream-json":
675
+ return mapClaudeEvent(evt, sessionId, stderrLines);
676
+ case "mastra-jsonl": {
677
+ if (!mastraState) {
678
+ return mapClaudeEvent(evt, sessionId, stderrLines);
679
+ }
680
+ return mapMastraEvent(evt, sessionId, stderrLines, mastraState);
681
+ }
682
+ default:
683
+ return mapClaudeEvent(evt, sessionId, stderrLines);
684
+ }
685
+ }
686
+ function mapClaudeEvent(evt, sessionId, stderrLines) {
488
687
  switch (evt.type) {
489
688
  case "text":
490
689
  return typeof evt.text === "string" ? { kind: "text-delta", sessionId, text: evt.text } : null;
@@ -528,6 +727,197 @@ function mapEvent(evt, sessionId, stderrLines) {
528
727
  return null;
529
728
  }
530
729
  }
730
+ function createMastraMapperState() {
731
+ return { lastTextLength: 0 };
732
+ }
733
+ function extractTextFromBlocks(content) {
734
+ if (!Array.isArray(content)) return "";
735
+ return content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
736
+ }
737
+ function mapMastraEvent(evt, sessionId, stderrLines, state) {
738
+ switch (evt.type) {
739
+ // ── Text streaming ──────────────────────────────────────────
740
+ case "message_update": {
741
+ const content = evt.message?.content;
742
+ const fullText = extractTextFromBlocks(content);
743
+ if (fullText.length > state.lastTextLength) {
744
+ const delta = fullText.slice(state.lastTextLength);
745
+ state.lastTextLength = fullText.length;
746
+ return delta ? { kind: "text-delta", sessionId, text: delta } : null;
747
+ }
748
+ return null;
749
+ }
750
+ case "message_end": {
751
+ const msg = evt.message;
752
+ if (msg?.role !== "assistant") return null;
753
+ const fullText = extractTextFromBlocks(msg?.content);
754
+ if (fullText.length > state.lastTextLength) {
755
+ const delta = fullText.slice(state.lastTextLength);
756
+ state.lastTextLength = fullText.length;
757
+ return delta ? { kind: "text-delta", sessionId, text: delta } : null;
758
+ }
759
+ return null;
760
+ }
761
+ // ── Tool calls ──────────────────────────────────────────────
762
+ case "tool_start":
763
+ return {
764
+ kind: "tool-call",
765
+ sessionId,
766
+ toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
767
+ toolName: typeof evt.toolName === "string" ? evt.toolName : "?",
768
+ // Mastra's controller event carries the tool call payload under
769
+ // `args`, not `input` (that's the Claude Code stream-json field
770
+ // name used by the sibling mapClaudeEvent tool_use case below).
771
+ arguments: evt.args ?? {}
772
+ };
773
+ case "tool_end":
774
+ return {
775
+ kind: "tool-result",
776
+ sessionId,
777
+ toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
778
+ result: evt.result ?? null,
779
+ isError: evt.isError === true
780
+ };
781
+ // ── Turn end ────────────────────────────────────────────────
782
+ case "agent_end": {
783
+ const rawReason = typeof evt.reason === "string" ? evt.reason : "complete";
784
+ const reason = mapMastraFinishReason(rawReason);
785
+ return { kind: "turn-end", sessionId, reason };
786
+ }
787
+ // ── Final result line ────────────────────────────────────────
788
+ // Always written last, once, whether or not `agent_end` fired —
789
+ // pre-flight failures (bad model, unresolvable thread, missing API
790
+ // key) call straight into this without ever streaming an
791
+ // `agent_end` or `error` event. `turn-end` already came from
792
+ // `agent_end` on the success path, so this only needs to surface
793
+ // an error for the pre-flight-failure path.
794
+ case "result": {
795
+ const err = evt.error;
796
+ if (!err) return null;
797
+ return {
798
+ kind: "error",
799
+ sessionId,
800
+ error: {
801
+ message: typeof err.message === "string" ? err.message : "Unknown error",
802
+ ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
803
+ }
804
+ };
805
+ }
806
+ // ── Errors ─────────────────────────────────────────────────
807
+ case "error": {
808
+ const err = evt.error;
809
+ return {
810
+ kind: "error",
811
+ sessionId,
812
+ error: {
813
+ message: typeof err?.message === "string" ? err.message : "Unknown error",
814
+ ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
815
+ }
816
+ };
817
+ }
818
+ // ── Token usage ─────────────────────────────────────────────
819
+ // Mastra Code DOES expose usage: its `AgentController` emits a native
820
+ // `usage_update` event ({ type: "usage_update", usage: TokenUsage })
821
+ // on every model step-finish — verified against @mastra/core 1.48.0's
822
+ // AgentControllerEvent union and its emit site
823
+ // (`this.#session.emit({ type: "usage_update", usage: stepUsage })`).
824
+ // `TokenUsage` carries `{ promptTokens, completionTokens, totalTokens }`.
825
+ // Map it to this repo's `usage_update` StreamEvent (the SAME shape
826
+ // claude-code emits over ACP) so mastracode sessions — both the print
827
+ // arm and the in-process arm, which share this mapper — carry token
828
+ // telemetry in the transcript. mastracode reports these cumulatively
829
+ // (its own headless `runMC` result-usage is last-write-wins over these
830
+ // events), and last-write-wins on the descriptor matches that. It
831
+ // carries no context-window size/used or per-turn cost here, so those
832
+ // stay 0 / absent (projectEvent guards on >0, never clobbering a real
833
+ // size).
834
+ case "usage_update": {
835
+ const usage = evt.usage;
836
+ const promptTokens = typeof usage?.promptTokens === "number" ? usage.promptTokens : void 0;
837
+ const completionTokens = typeof usage?.completionTokens === "number" ? usage.completionTokens : void 0;
838
+ if (promptTokens === void 0 && completionTokens === void 0)
839
+ return null;
840
+ return {
841
+ kind: "usage_update",
842
+ sessionId,
843
+ size: 0,
844
+ used: 0,
845
+ ...promptTokens !== void 0 ? { tokensIn: promptTokens } : {},
846
+ ...completionTokens !== void 0 ? { tokensOut: completionTokens } : {}
847
+ };
848
+ }
849
+ // ── Shell output (tool-like) ────────────────────────────────
850
+ case "shell_output":
851
+ return typeof evt.output === "string" ? {
852
+ kind: "tool-result",
853
+ sessionId,
854
+ toolCallId: "",
855
+ result: evt.output,
856
+ isError: false
857
+ } : null;
858
+ // ── Skipped events ─────────────────────────────────────────
859
+ case "agent_start":
860
+ case "subagent_start":
861
+ case "subagent_end":
862
+ case "tool_approval_required":
863
+ case "tool_suspended":
864
+ return null;
865
+ default:
866
+ return null;
867
+ }
868
+ }
869
+ var QUEUE_HIGH_WATER_MARK = 1e4;
870
+ var EventQueue = class {
871
+ buffer = [];
872
+ resolveNext = null;
873
+ ended = false;
874
+ highWaterWarned = false;
875
+ push(evt) {
876
+ this.buffer.push(evt);
877
+ if (this.buffer.length > QUEUE_HIGH_WATER_MARK && !this.highWaterWarned) {
878
+ this.highWaterWarned = true;
879
+ console.warn(
880
+ `print-arm: event backlog exceeded ${QUEUE_HIGH_WATER_MARK} buffered events \u2014 downstream consumer is not keeping up. The child's stdout is still being drained (no ENOBUFS), but memory will grow until the consumer catches up.`
881
+ );
882
+ }
883
+ const resolve = this.resolveNext;
884
+ this.resolveNext = null;
885
+ resolve?.();
886
+ }
887
+ /** Signal that no further events will be pushed. */
888
+ end() {
889
+ this.ended = true;
890
+ const resolve = this.resolveNext;
891
+ this.resolveNext = null;
892
+ resolve?.();
893
+ }
894
+ async *[Symbol.asyncIterator]() {
895
+ for (; ; ) {
896
+ const evt = this.buffer.shift();
897
+ if (evt !== void 0) {
898
+ yield evt;
899
+ continue;
900
+ }
901
+ if (this.ended) return;
902
+ await new Promise((resolve) => {
903
+ this.resolveNext = resolve;
904
+ });
905
+ }
906
+ }
907
+ };
908
+ function mapMastraFinishReason(raw) {
909
+ switch (raw) {
910
+ case "complete":
911
+ return "completed";
912
+ case "aborted":
913
+ case "suspended":
914
+ return "cancelled";
915
+ case "error":
916
+ return "error";
917
+ default:
918
+ return "completed";
919
+ }
920
+ }
531
921
  function waitForExit(child) {
532
922
  return new Promise((resolve) => {
533
923
  child.once("exit", (code) => resolve(code));
@@ -535,6 +925,27 @@ function waitForExit(child) {
535
925
  });
536
926
  }
537
927
 
928
+ // src/protocol/proprietary.ts
929
+ async function createProprietaryProtocolArm(options) {
930
+ let mod;
931
+ try {
932
+ mod = await import(options.adapter);
933
+ } catch (err) {
934
+ const cause = err instanceof Error ? err.message : String(err);
935
+ throw new Error(
936
+ `createProprietaryProtocolArm: could not load adapter package '${options.adapter}'. Install it with: npm i ${options.adapter}
937
+ cause: ${cause}`
938
+ );
939
+ }
940
+ const candidate = "createAgentCliClient" in mod ? mod.createAgentCliClient : mod.default;
941
+ if (typeof candidate !== "function") {
942
+ throw new Error(
943
+ `createProprietaryProtocolArm: adapter package '${options.adapter}' does not export a 'createAgentCliClient' factory function (or a default export function). A protocol="proprietary" adapter must export createAgentCliClient(definition: AgentCliHandle): AgentCliClient | Promise<AgentCliClient>.`
944
+ );
945
+ }
946
+ return await candidate(options.definition);
947
+ }
948
+
538
949
  // src/manifest/compose.ts
539
950
  var RuntimeConfigError = class extends Error {
540
951
  code;
@@ -547,9 +958,11 @@ var RuntimeConfigError = class extends Error {
547
958
  }
548
959
  };
549
960
  function composeSpawn(handle, config) {
550
- const binArgs = [...handle.bin_args ?? []];
961
+ if (!config) return { binArgs: [...handle.bin_args ?? []], env: {}, envUnset: [] };
962
+ const prepend = [];
963
+ const append = [];
551
964
  const env = {};
552
- if (!config) return { binArgs, env };
965
+ const envUnset = [];
553
966
  if (config.mode !== void 0) {
554
967
  const mode = (handle.modes ?? []).find((m) => m.id === config.mode);
555
968
  if (!mode) {
@@ -560,8 +973,12 @@ function composeSpawn(handle, config) {
560
973
  `Mode '${config.mode}' is not declared by manifest '${handle.id}'. Known modes: ${known}`
561
974
  );
562
975
  }
563
- if (mode.bin_args_append) binArgs.push(...mode.bin_args_append);
564
- if (mode.env) Object.assign(env, mode.env);
976
+ if ((mode.apply ?? "bin_args") === "bin_args") {
977
+ if (mode.bin_args_prepend) prepend.push(...mode.bin_args_prepend);
978
+ if (mode.bin_args_append) append.push(...mode.bin_args_append);
979
+ if (mode.env) Object.assign(env, mode.env);
980
+ if (mode.env_unset) envUnset.push(...mode.env_unset);
981
+ }
565
982
  }
566
983
  if (config.options && Object.keys(config.options).length > 0) {
567
984
  const declared = handle.options ?? [];
@@ -581,8 +998,23 @@ function composeSpawn(handle, config) {
581
998
  const value = config.options[option.id];
582
999
  validateOptionValue(option, value);
583
1000
  const patch = renderOptionPatch(option, value);
584
- binArgs.push(...patch.binArgs);
1001
+ prepend.push(...patch.prepend);
1002
+ append.push(...patch.append);
585
1003
  Object.assign(env, patch.env);
1004
+ if (patch.envUnset.length) envUnset.push(...patch.envUnset);
1005
+ }
1006
+ }
1007
+ const requestedModel = config.options?.model;
1008
+ if (typeof requestedModel === "string" && handle.models?.deny?.length) {
1009
+ const denyHit = handle.models.deny.find(
1010
+ (pattern) => matchesModelPattern(pattern, requestedModel)
1011
+ );
1012
+ if (denyHit) {
1013
+ throw new RuntimeConfigError(
1014
+ "model_denied",
1015
+ "config.options.model",
1016
+ `Model '${requestedModel}' is denied by manifest '${handle.id}' (matches deny pattern '${denyHit}'). This adapter deliberately does not route to that provider \u2014 use a permitted model or a different adapter.`
1017
+ );
586
1018
  }
587
1019
  }
588
1020
  if (config.continuation !== void 0 && handle.continuation) {
@@ -594,12 +1026,21 @@ function composeSpawn(handle, config) {
594
1026
  );
595
1027
  }
596
1028
  }
597
- return { binArgs, env };
1029
+ return {
1030
+ binArgs: [...prepend, ...handle.bin_args ?? [], ...append],
1031
+ env,
1032
+ envUnset
1033
+ };
598
1034
  }
599
1035
  function resolveContinuationStrategy(handle, config) {
600
1036
  if (config?.continuation) return config.continuation;
601
1037
  return handle.continuation?.default ?? "none";
602
1038
  }
1039
+ function matchesModelPattern(pattern, modelId) {
1040
+ const p = pattern.toLowerCase();
1041
+ const m = modelId.toLowerCase();
1042
+ return p.endsWith("*") ? m.startsWith(p.slice(0, -1)) : m === p;
1043
+ }
603
1044
  function validateOptionValue(option, value) {
604
1045
  const path = `config.options.${option.id}`;
605
1046
  switch (option.type) {
@@ -665,17 +1106,23 @@ function validateOptionValue(option, value) {
665
1106
  function renderOptionPatch(option, value) {
666
1107
  const stringValue = String(value);
667
1108
  if (option.type === "boolean") {
668
- if (value !== true) return { binArgs: [], env: {} };
1109
+ if (value !== true) return { prepend: [], append: [], env: {}, envUnset: [] };
669
1110
  return {
670
- binArgs: option.bin_args_append_when_true ? [...option.bin_args_append_when_true] : [],
671
- env: option.env ? interpolateEnv(option.env, stringValue) : {}
1111
+ prepend: [],
1112
+ append: option.bin_args_append_when_true ? [...option.bin_args_append_when_true] : [],
1113
+ env: option.env ? interpolateEnv(option.env, stringValue) : {},
1114
+ envUnset: option.env_unset ? [...option.env_unset] : []
672
1115
  };
673
1116
  }
674
1117
  return {
675
- binArgs: option.bin_args_template ? option.bin_args_template.map(
1118
+ prepend: option.bin_args_prepend ? option.bin_args_prepend.map(
676
1119
  (token) => token.replace(/\{value\}/g, stringValue)
677
1120
  ) : [],
678
- env: option.env ? interpolateEnv(option.env, stringValue) : {}
1121
+ append: option.bin_args_template ? option.bin_args_template.map(
1122
+ (token) => token.replace(/\{value\}/g, stringValue)
1123
+ ) : [],
1124
+ env: option.env ? interpolateEnv(option.env, stringValue) : {},
1125
+ envUnset: option.env_unset ? [...option.env_unset] : []
679
1126
  };
680
1127
  }
681
1128
  function interpolateEnv(env, value) {
@@ -732,35 +1179,55 @@ function createAgentCliRuntime(definition) {
732
1179
  const composed = composeSpawn(definition, opts?.config);
733
1180
  const env = {
734
1181
  ...filterStringEnv(process.env),
735
- ...composed.env,
736
- ...opts?.env ?? {}
1182
+ ...composed.env
737
1183
  };
1184
+ for (const k of composed.envUnset) delete env[k];
1185
+ Object.assign(env, opts?.env ?? {});
1186
+ const permissionMode = resolveClaudeCodePermissionMode(
1187
+ definition,
1188
+ opts?.config?.mode
1189
+ );
1190
+ if (permissionMode) {
1191
+ const configDir = mkdtempSync(
1192
+ join(tmpdir(), "agentproto-claude-config-")
1193
+ );
1194
+ writeFileSync(
1195
+ join(configDir, "settings.json"),
1196
+ JSON.stringify({ permissions: { defaultMode: permissionMode } })
1197
+ );
1198
+ env.CLAUDE_CONFIG_DIR = configDir;
1199
+ }
738
1200
  if (definition.protocol === "print") {
739
1201
  return createPrintSession({
740
1202
  bin: definition.bin,
741
1203
  baseArgs: composed.binArgs,
742
1204
  cwd,
743
1205
  env,
744
- ...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {}
1206
+ ...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
1207
+ ...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
1208
+ printConfig: definition.print
745
1209
  });
746
1210
  }
747
- const child = spawn(definition.bin, composed.binArgs, {
748
- cwd,
749
- env,
750
- stdio: ["pipe", "pipe", "pipe"],
751
- signal: opts?.signal
752
- });
1211
+ let child;
753
1212
  const stderrBuf = [];
754
- const STDERR_KEEP_LINES = 80;
755
- child.stderr?.setEncoding("utf8");
756
- child.stderr?.on("data", (chunk) => {
757
- for (const line of chunk.split(/\r?\n/)) {
758
- if (!line) continue;
759
- stderrBuf.push(line);
760
- if (stderrBuf.length > STDERR_KEEP_LINES) stderrBuf.shift();
761
- }
762
- });
763
- const arm = buildProtocolArm(definition, child, cwd);
1213
+ if (definition.protocol !== "proprietary") {
1214
+ child = spawn(definition.bin, composed.binArgs, {
1215
+ cwd,
1216
+ env,
1217
+ stdio: ["pipe", "pipe", "pipe"],
1218
+ signal: opts?.signal
1219
+ });
1220
+ const STDERR_KEEP_LINES = 80;
1221
+ child.stderr?.setEncoding("utf8");
1222
+ child.stderr?.on("data", (chunk) => {
1223
+ for (const line of chunk.split(/\r?\n/)) {
1224
+ if (!line) continue;
1225
+ stderrBuf.push(line);
1226
+ if (stderrBuf.length > STDERR_KEEP_LINES) stderrBuf.shift();
1227
+ }
1228
+ });
1229
+ }
1230
+ const arm = await buildProtocolArm(definition, child, cwd, opts?.config?.mode);
764
1231
  arm._stderrTail = () => stderrBuf.join("\n");
765
1232
  const abortController = new AbortController();
766
1233
  if (opts?.signal) {
@@ -772,6 +1239,10 @@ function createAgentCliRuntime(definition) {
772
1239
  const optEffort = opts?.config?.options?.effort;
773
1240
  const modelApply = definition.models?.apply ?? "config";
774
1241
  const configModel = optModel && modelApply === "config" ? String(optModel) : void 0;
1242
+ const requestedModeId = opts?.config?.mode;
1243
+ const requestedModeDecl = requestedModeId ? (definition.modes ?? []).find((m) => m.id === requestedModeId) : void 0;
1244
+ const configMode = requestedModeDecl?.apply === "config" ? requestedModeId : void 0;
1245
+ const turnIdleTimeoutMs = opts?.turnIdleTimeoutMs ?? definition.session?.turn_idle_timeout_ms;
775
1246
  await arm.connect({
776
1247
  cwd,
777
1248
  env,
@@ -779,7 +1250,10 @@ function createAgentCliRuntime(definition) {
779
1250
  ...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
780
1251
  ...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
781
1252
  ...configModel ? { model: configModel } : {},
782
- ...optEffort ? { effort: String(optEffort) } : {}
1253
+ ...optEffort ? { effort: String(optEffort) } : {},
1254
+ ...configMode ? { mode: configMode } : {},
1255
+ ...opts?.onActivity ? { onActivity: opts.onActivity } : {},
1256
+ ...turnIdleTimeoutMs !== void 0 ? { turnIdleTimeoutMs } : {}
783
1257
  });
784
1258
  if (optModel && modelApply === "command") {
785
1259
  await applyModelCommand(arm, String(optModel));
@@ -787,6 +1261,7 @@ function createAgentCliRuntime(definition) {
787
1261
  const sessionId = arm.sessionId ?? randomUUID();
788
1262
  return {
789
1263
  sessionId,
1264
+ pid: child?.pid,
790
1265
  send(message) {
791
1266
  const turnId = randomUUID();
792
1267
  return promptTurn(arm, turnId, message);
@@ -796,7 +1271,7 @@ function createAgentCliRuntime(definition) {
796
1271
  },
797
1272
  async close() {
798
1273
  await arm.close();
799
- if (!child.killed) child.kill("SIGTERM");
1274
+ if (child && !child.killed) child.kill("SIGTERM");
800
1275
  }
801
1276
  };
802
1277
  }
@@ -839,24 +1314,39 @@ async function* promptTurn(arm, turnId, message) {
839
1314
  yield evt;
840
1315
  }
841
1316
  }
842
- function buildProtocolArm(def, child, cwd) {
1317
+ async function buildProtocolArm(def, child, cwd, requestedMode) {
843
1318
  switch (def.protocol) {
844
1319
  case "acp":
1320
+ if (!child) {
1321
+ throw new Error("createAgentCliRuntime: acp protocol requires a spawned subprocess");
1322
+ }
845
1323
  return createAcpProtocolArm({
846
1324
  child,
847
1325
  cwd,
848
- clientInfo: { name: def.id, version: def.version }
1326
+ clientInfo: { name: def.id, version: def.version },
1327
+ requestedMode
849
1328
  });
850
1329
  case "mcp":
851
1330
  throw new Error("createAgentCliRuntime: mcp protocol arm not yet implemented");
852
1331
  case "proprietary":
853
- throw new Error(
854
- "createAgentCliRuntime: proprietary protocol arm not yet implemented"
855
- );
1332
+ if (!def.adapter) {
1333
+ throw new Error(
1334
+ "createAgentCliRuntime: proprietary protocol requires manifest.adapter"
1335
+ );
1336
+ }
1337
+ return createProprietaryProtocolArm({ adapter: def.adapter, definition: def });
856
1338
  case "print":
857
1339
  throw new Error("createAgentCliRuntime: print arm bypasses buildProtocolArm");
858
1340
  }
859
1341
  }
1342
+ function resolveClaudeCodePermissionMode(definition, configMode) {
1343
+ if (definition.id !== "claude-code" || !configMode) return void 0;
1344
+ const mode = (definition.modes ?? []).find((m) => m.id === configMode);
1345
+ const args = mode?.bin_args_append ?? [];
1346
+ const flagIndex = args.indexOf("--permission-mode");
1347
+ if (flagIndex === -1 || flagIndex + 1 >= args.length) return void 0;
1348
+ return args[flagIndex + 1];
1349
+ }
860
1350
  function filterStringEnv(env) {
861
1351
  const out = {};
862
1352
  for (const [k, v] of Object.entries(env)) {
@@ -865,6 +1355,6 @@ function filterStringEnv(env) {
865
1355
  return out;
866
1356
  }
867
1357
 
868
- export { RuntimeConfigError, agentCliFrontmatterSchema, autoAllowPermissionHandler, composeSpawn, createAcpProtocolArm, createAgentCliRuntime, createPrintSession, defineAgentCli, resolveContinuationStrategy, runtimeConfigSchema };
869
- //# sourceMappingURL=chunk-YQHVLX6O.mjs.map
870
- //# sourceMappingURL=chunk-YQHVLX6O.mjs.map
1358
+ export { RuntimeConfigError, agentCliFrontmatterSchema, autoAllowPermissionHandler, composeSpawn, createAcpProtocolArm, createAgentCliRuntime, createMastraMapperState, createPrintSession, createProprietaryProtocolArm, defineAgentCli, mapMastraEvent, planModePermissionHandler, resolveContinuationStrategy, runtimeConfigSchema, toFileBasedMcpServers };
1359
+ //# sourceMappingURL=chunk-L6BRNMVP.mjs.map
1360
+ //# sourceMappingURL=chunk-L6BRNMVP.mjs.map