UNPKG

@agentproto/driver-agent-cli

Version:

@agentproto/driver-agent-cli — AIP-45 AGENT-CLI.md reference implementation. Multi-protocol runner that loads an AGENT-CLI.md manifest, spawns the binary, and dispatches turns through an ACP / MCP / proprietary arm. ACP arm delegates to @agentproto/acp; e

2,402 lines • 95.7 kB
import { z } from 'zod';
import { Writable, Readable } from 'stream';
import { createAcpClient } from '@agentproto/acp/client';
import { spawn, execFile } from 'child_process';
import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, rmdirSync, realpathSync, mkdtempSync } from 'fs';
import { createInterface } from 'readline';
import { join, isAbsolute, resolve, delimiter, dirname, basename } from 'path';
import { tmpdir, homedir } from 'os';
import { loadAdapterSpawnSandboxConfig, resolveCommandSandbox } from '@agentproto/command-sandbox';
import { pathToFileURL } from 'url';
import { randomUUID, createHash } from 'crypto';
import { createDoctype } from '@agentproto/define-doctype';

/**
 * @agentproto/driver-agent-cli v0.1.0-alpha
 * AIP-45 AGENT-CLI.md `defineAgentCli` reference implementation.
 */

var ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
var SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
var TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var SEMVER_RANGE_PATTERN = /^[><=^~ \d.\-+\w*xX|, ]+$/;
var ADAPTER_PATTERN = /^@?[a-z0-9][a-z0-9-./]*$/;
var BIN_PATTERN = /^[A-Za-z0-9._\-/]+$/;
var MODE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var OPTION_ID_PATTERN = /^[a-z0-9][a-z0-9_]{0,63}$/;
var CONTINUATION_STRATEGY_IDS = [
  "none",
  "pinned-session",
  "transcript",
  "native-resume"
];
var CONTINUATION_KEY_SCOPES = [
  "conversation",
  "operator",
  "user",
  "guild",
  "workspace"
];
var installMethodSchema = z.discriminatedUnion("method", [
  z.object({ method: z.literal("brew"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("apt"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("dnf"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("pacman"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("choco"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("scoop"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("npm"), package: z.string(), global: z.boolean().optional(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("pip"), package: z.string(), user: z.boolean().optional(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("cargo"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({ method: z.literal("go"), package: z.string(), experimental: z.boolean().optional() }).strict(),
  z.object({
    method: z.literal("curl"),
    url: z.string().url(),
    verify_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
    experimental: z.boolean().optional()
  }).strict(),
  z.object({
    method: z.literal("download"),
    url: z.string().url(),
    extract_bin: z.string(),
    verify_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
    experimental: z.boolean().optional()
  }).strict(),
  z.object({ method: z.literal("vendored"), path: z.string(), experimental: z.boolean().optional() }).strict()
]);
var versionCheckSchema = z.object({
  cmd: z.string(),
  parse: z.string().describe("ECMAScript regex with at least one capture group."),
  range: z.string().regex(SEMVER_RANGE_PATTERN).describe("npm-style semver range."),
  timeout_ms: z.number().int().min(100).default(5e3)
}).strict();
var SETUP_STEP_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
var SECRET_SLUG_PATTERN = /^([a-z][a-z0-9-]*[a-z0-9]\/)?[a-z][a-z0-9-]*[a-z0-9]$/;
var skipIfSchema = z.object({
  cmd: z.string(),
  exit_code: z.number().int().default(0),
  timeout_ms: z.number().int().min(100).default(5e3)
}).strict();
var persistEnvSchema = z.object({ env: z.string().regex(/^[A-Z][A-Z0-9_]*$/) }).strict();
var persistSecretSchema = z.object({ secret_slug: z.string().regex(SECRET_SLUG_PATTERN).min(2).max(80) }).strict();
var persistCmdSchema = z.object({ cmd: z.string() }).strict();
var setupPersistSchema = z.union([
  persistEnvSchema,
  persistSecretSchema,
  persistCmdSchema
]);
var setupStepSchema = z.discriminatedUnion("kind", [
  z.object({
    id: z.string().regex(SETUP_STEP_ID_PATTERN),
    kind: z.literal("cmd"),
    cmd: z.string(),
    description: z.string().max(2e3).optional(),
    skip_if: skipIfSchema.optional(),
    timeout_ms: z.number().int().min(100).default(6e4),
    interactive: z.boolean().default(false),
    persist: setupPersistSchema.optional()
  }).strict(),
  z.object({
    id: z.string().regex(SETUP_STEP_ID_PATTERN),
    kind: z.literal("prompt"),
    prompt: z.string().min(1),
    description: z.string().max(2e3).optional(),
    type: z.enum(["text", "select", "boolean", "secret"]).default("text"),
    default: z.string().optional(),
    options: z.union([
      z.array(z.string().min(1)).min(1),
      z.object({
        cmd: z.string(),
        timeout_ms: z.number().int().min(100).default(3e4)
      }).strict()
    ]).optional(),
    skip_if: skipIfSchema.optional(),
    persist: setupPersistSchema.optional()
  }).strict(),
  z.object({
    id: z.string().regex(SETUP_STEP_ID_PATTERN),
    kind: z.literal("oauth"),
    secret_slug: z.string().regex(SECRET_SLUG_PATTERN).min(2).max(80),
    description: z.string().max(2e3).optional(),
    skip_if: skipIfSchema.optional()
  }).strict(),
  z.object({
    id: z.string().regex(SETUP_STEP_ID_PATTERN),
    kind: z.literal("external"),
    url: z.string().url(),
    description: z.string().max(2e3).optional(),
    callback: z.object({
      param: z.string().min(1).optional(),
      timeout_ms: z.number().int().min(1e3).default(6e5)
    }).strict().optional(),
    skip_if: skipIfSchema.optional(),
    persist: setupPersistSchema.optional()
  }).strict()
]);
var authSchema = z.object({
  ref: z.string().optional(),
  state: z.object({
    paths: z.array(z.string()).optional(),
    env: z.array(z.string()).optional()
  }).strict().optional(),
  login: z.object({
    cmd: z.string(),
    interactive: z.boolean().optional(),
    requires_callback_url: z.boolean().optional()
  }).strict().optional(),
  refresh: z.object({
    cmd: z.string(),
    interval_s: z.number().int().positive().optional()
  }).strict().optional(),
  expiry: z.object({
    parse: z.string().optional(),
    grace_s: z.number().int().nonnegative().optional()
  }).strict().optional()
}).strict();
var authSubscriptionEntrySchema = z.object({
  setEnv: z.string().min(1).optional(),
  external: z.boolean().optional(),
  conflictEnv: z.array(z.string()).optional(),
  unsetEnvAdd: z.array(z.string()).optional(),
  // Provider scope for a multi-provider adapter's bearer surface (pi's
  // ANTHROPIC_OAUTH_TOKEN is anthropic-only) — see AgentCliAuthSubscription.
  provider: z.string().min(1).optional()
}).strict().superRefine((v, ctx) => {
  if (v.external) {
    if (v.setEnv !== void 0) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "authSubscription.external: true must NOT declare setEnv \u2014 an external (file-based) subscription injects no bearer; the CLI reads its own login file.",
        path: ["setEnv"]
      });
    }
  } else if (v.setEnv === void 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "authSubscription requires setEnv unless external: true (the bearer must be injected into some env var).",
      path: ["setEnv"]
    });
  }
});
var authSubscriptionSchema = z.union([authSubscriptionEntrySchema, z.array(authSubscriptionEntrySchema).min(1)]).superRefine((v, ctx) => {
  if (!Array.isArray(v)) return;
  const seen = /* @__PURE__ */ new Set();
  for (const entry of v) {
    const key = entry.provider ?? "\0unscoped";
    if (seen.has(key)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: entry.provider ? `authSubscription: duplicate provider scope "${entry.provider}" \u2014 two entries claim the same provider, which is ambiguous at spawn time.` : "authSubscription: more than one unscoped entry (no `provider`) \u2014 ambiguous which one applies at spawn time."
      });
    }
    seen.add(key);
  }
});
var sessionSchema = z.object({
  mode: z.enum(["ephemeral", "persistent", "resumable"]).default("ephemeral"),
  idle_timeout_ms: z.number().int().min(1e3).default(6e5),
  // No default: undefined disables the per-turn watchdog entirely. Only
  // adapters known to sometimes drop the final `prompt` response (e.g.
  // hermes) should declare this — applying it broadly would risk
  // false-positiving on another adapter's legitimately long turns.
  turn_idle_timeout_ms: z.number().int().min(1e3).optional(),
  max_turns: z.number().int().positive().optional(),
  context_carryover: z.boolean().default(true)
}).strict();
var modelEntrySchema = z.union([
  z.string(),
  z.object({
    id: z.string(),
    provider: z.string().optional(),
    mode: z.string().optional()
  }).strict()
]);
var modelsSchema = z.object({
  default: z.string().optional(),
  allowed: z.array(modelEntrySchema).optional(),
  // Model-id patterns the adapter must never route to (case-insensitive,
  // trailing `*` = prefix match). Enforced at compose time — see
  // AgentCliModels.deny. Reserves premium providers for dedicated adapters.
  deny: z.array(z.string()).optional(),
  env: z.record(z.string(), z.string()).optional(),
  // How a model is selected at session start: "config" (ACP
  // set_config_option, default) | "command" (a `/model <id>` control turn,
  // for agents like hermes that ignore the ACP session model config) |
  // "arg" (a CLI argument composed into bin_args at spawn).
  apply: z.enum(["config", "command", "arg"]).optional(),
  // Argv template for apply:"arg" — {model} interpolates the requested
  // model id. See AgentCliModels.bin_args_template.
  bin_args_template: z.array(z.string()).optional()
}).strict();
var capabilitiesSchema = z.object({
  streaming: z.boolean().optional(),
  tool_calls: z.boolean().optional(),
  sub_agents: z.boolean().optional(),
  file_io: z.boolean().optional(),
  multimodal: z.boolean().optional(),
  resumable: z.boolean().optional(),
  bidirectional: z.boolean().optional(),
  /**
   * Adapter has a verified native CLI resume into a real terminal/TUI
   * session (e.g. `claude --resume <id>` or `hermes --resume <id> --tui`).
   * When true, the host MAY restart this adapter via its native resume argv
   * as a PTY session. Distinct from ACP-level `resumable`.
   */
  nativeTerminalResume: z.boolean().optional(),
  /**
   * Adapter can ingest a filesystem path that the host UI just placed
   * on disk (host-side drag-drop into a terminal pastes the path
   * here). Implies the adapter has a Read-file tool wired so the
   * dropped path actually does something. Hosts gate drag-drop UI on
   * this flag — defaults to false (conservative).
   */
  file_attach: z.boolean().optional()
}).strict();
var modeSchema = z.object({
  id: z.string().regex(MODE_ID_PATTERN),
  description: z.string().optional(),
  // Which orthogonal session-config axis this mode expresses (SPEC-tracked in
  // @agentproto/runtime's session-config.ts). Narrowed to only "context" (SPEC
  // §3.4a): route is derived from the model catalog (@route), not a manifest
  // mode, and posture comes from the harness's own ACP mode registry
  // (SessionModeState.availableModes) — neither is a `modes[]` entry anymore.
  // The legacy "posture"/"route" classification still exists, but only inside
  // the daemon's back-compat `decomposeMode` shim, not this manifest surface.
  kind: z.enum(["context"]).optional(),
  bin_args_prepend: z.array(z.string()).optional(),
  bin_args_append: z.array(z.string()).optional(),
  env: z.record(z.string(), z.string()).optional(),
  // Env keys to DELETE from the spawn env (auth hygiene for gateway
  // modes — see AgentCliMode.env_unset). Applied at the runtime merge
  // point, not as a static set.
  env_unset: z.array(z.string()).optional(),
  // Honest support status surfaced to clients. Absent ⇒ "active".
  status: z.enum(["active", "noop", "planned"]).optional(),
  status_note: z.string().optional(),
  // How the host activates this mode: "bin_args" (default, argv/env
  // composed at spawn) | "config" (no CLI surface — forwarded to the ACP
  // arm's connect({mode}) and applied via session/set_config_option
  // configId:"mode", e.g. opencode).
  apply: z.enum(["bin_args", "config"]).optional()
}).strict();
var optionSchema = z.object({
  id: z.string().regex(OPTION_ID_PATTERN),
  type: z.enum(["boolean", "integer", "string", "enum"]),
  description: z.string().optional(),
  enum: z.array(z.string()).min(1).optional(),
  default: z.union([z.boolean(), z.number(), z.string()]).optional(),
  min: z.number().int().optional(),
  max: z.number().int().optional(),
  bin_args_prepend: z.array(z.string()).optional(),
  bin_args_template: z.array(z.string()).optional(),
  bin_args_append_when_true: z.array(z.string()).optional(),
  env: z.record(z.string(), z.string()).optional(),
  // Env keys to DELETE from the spawn env when this option is active
  // (auth hygiene for value-bearing gateway options — see
  // AgentCliOption.env_unset). Symmetric with modeSchema.env_unset.
  env_unset: z.array(z.string()).optional()
}).strict().refine(
  // type === "enum" REQUIRES an `enum` array. The JSON Schema enforces
  // this via `if/then`; the zod side mirrors with a refine so both
  // validation paths reject the same shapes.
  (o) => o.type !== "enum" || Array.isArray(o.enum) && o.enum.length > 0,
  { message: "option.type === 'enum' requires a non-empty `enum` array" }
);
var presetSchema = z.object({
  id: z.string().min(1),
  label: z.string().min(1),
  description: z.string().optional(),
  schemaFlavor: z.enum(["anthropic", "openai"]),
  baseUrl: z.string().min(1),
  keyEnv: z.string().min(1),
  scrubEnv: z.array(z.string()).optional(),
  defaultModel: z.string().optional(),
  homepage: z.string().optional()
}).strict();
var continuationStrategyIdSchema = z.enum(CONTINUATION_STRATEGY_IDS);
var continuationSchema = z.object({
  default: continuationStrategyIdSchema,
  supported: z.array(continuationStrategyIdSchema).min(1),
  pinned_session: z.object({
    idle_timeout_ms: z.number().int().min(1e3).default(18e5),
    key_scope: z.array(z.enum(CONTINUATION_KEY_SCOPES)).min(1).default(["conversation", "operator"])
  }).strict().optional()
}).strict().refine(
  // The chosen `default` must appear in `supported` — otherwise the
  // host has no valid strategy to fall back to.
  (c) => c.supported.includes(c.default),
  {
    message: "continuation.default must be a member of continuation.supported"
  }
);
var mcpBlockSchema = z.object({
  command: z.string().optional(),
  args: z.array(z.string()).optional(),
  transport: z.enum(["stdio", "http", "sse"]),
  url: z.string().url().optional()
}).strict();
var printConfigSchema = z.object({
  prompt_flag: z.string().optional(),
  output_format: z.array(z.string()).optional(),
  pre_prompt: z.array(z.string()).optional(),
  resume: z.object({
    flag: z.string(),
    kind: z.enum(["value", "boolean"])
  }).strict().optional(),
  event_schema: z.enum(["claude-stream-json", "mastra-jsonl", "antigravity-stream-json", "jcode-ndjson"]).optional()
}).strict().optional();
var agentCliFrontmatterSchema = z.object({
  name: z.string().min(1).max(80),
  id: z.string().regex(ID_PATTERN),
  description: z.string().min(1).max(2e3),
  version: z.string().regex(SEMVER_PATTERN),
  bin: z.string().regex(BIN_PATTERN),
  bin_args: z.array(z.string()).optional(),
  // Always-on spawn env (merged before mode/option env, which can
  // override). See AgentCliDefinition.env — used by generic ACP agents.
  env: z.record(z.string(), z.string()).optional(),
  install: z.array(installMethodSchema).min(1),
  version_check: versionCheckSchema,
  setup: z.array(setupStepSchema).optional(),
  auth: authSchema.optional(),
  // Billing-auth resolver inputs (DECISION 3). `provider` is a CatalogProvider
  // id (validated against the catalog by the runtime resolver, not here, to
  // keep this generic doctype decoupled from @agentproto/model-catalog).
  provider: z.string().min(1).optional(),
  authSubscription: authSubscriptionSchema.optional(),
  // How this adapter receives a GATEWAY-routed bearer credential — see the
  // doc on AgentCliDefinition.gatewayAuth. Distinct from a gateway preset's
  // `keyEnv` (the providers-store lookup key) and from
  // authSubscription.setEnv (this adapter's own native bearer).
  gatewayAuth: z.object({
    setEnv: z.string().min(1)
  }).strict().optional(),
  // Auth-derivation axis (DECISION 3): true when the adapter's api-key auth
  // is derived from the requested model rather than a fixed `provider`
  // (e.g. `pi`, `opencode`, `mastracode`). When set, the runtime resolver
  // allows `"api-key"` mode on the model-derived direct endpoint and
  // includes it in eligibility manifests for by-model routers. DISTINCT
  // from `routeSelection` (the UI-facing route-choice axis).
  modelDerivedApiKey: z.boolean().optional(),
  // UI-facing route-choice axis (AIP-45 launch-menu drill-down, WP1).
  // Absent ⇒ "free" (route is an independent choice); "derived-from-model"
  // means the endpoint falls out of the model id's vendor prefix. DISTINCT
  // from `modelDerivedApiKey` (the auth-derivation axis) — see the field
  // doc on AgentCliDefinition.routeSelection.
  routeSelection: z.enum(["free", "derived-from-model"]).optional(),
  authEnforce: z.enum(["always", "when-configured"]).optional(),
  sandbox: z.union([z.string(), z.record(z.string(), z.unknown())]),
  runner: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
  protocol: z.enum(["acp", "mcp", "proprietary", "print"]),
  acp: z.string().optional(),
  mcp: mcpBlockSchema.optional(),
  adapter: z.string().regex(ADAPTER_PATTERN).optional(),
  print: printConfigSchema,
  session: sessionSchema.optional(),
  models: modelsSchema.optional(),
  capabilities: capabilitiesSchema.optional(),
  modes: z.array(modeSchema).optional(),
  options: z.array(optionSchema).optional(),
  // AIP-45 gateway presets this adapter can drive (merged into the
  // provider-preset catalog; built-in wins on id collision).
  presets: z.array(presetSchema).optional(),
  continuation: continuationSchema.optional(),
  requires: z.object({
    os: z.array(z.enum(["darwin", "linux", "windows"])).optional(),
    arch: z.array(z.enum(["x64", "arm64", "x86", "arm"])).optional(),
    min_disk_mb: z.number().int().nonnegative().optional(),
    min_memory_mb: z.number().int().nonnegative().optional()
  }).strict().optional(),
  examples: z.array(z.object({
    goal: z.string(),
    prompt: z.string(),
    note: z.string().optional()
  }).strict()).optional(),
  tags: z.array(z.string().regex(TAG_PATTERN)).optional(),
  metadata: z.record(z.string(), z.unknown()).optional()
}).strict().superRefine((m, ctx) => {
  if (m.modes) {
    const seen = /* @__PURE__ */ new Set();
    for (const mode of m.modes) {
      if (seen.has(mode.id)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["modes"],
          message: `Duplicate mode id '${mode.id}' \u2014 mode ids must be unique within a manifest.`
        });
      }
      seen.add(mode.id);
    }
  }
  if (m.setup) {
    const seen = /* @__PURE__ */ new Set();
    for (const step of m.setup) {
      if (seen.has(step.id)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["setup"],
          message: `Duplicate setup step id '${step.id}' \u2014 step ids must be unique within a manifest.`
        });
      }
      seen.add(step.id);
    }
  }
  if (m.options) {
    const seen = /* @__PURE__ */ new Set();
    for (const opt of m.options) {
      if (seen.has(opt.id)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["options"],
          message: `Duplicate option id '${opt.id}' \u2014 option ids must be unique within a manifest.`
        });
      }
      seen.add(opt.id);
    }
  }
  if (m.continuation?.default === "native-resume" && !m.capabilities?.resumable) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ["continuation", "default"],
      message: "continuation.default === 'native-resume' requires capabilities.resumable: true."
    });
  }
}).describe(
  "Validates the YAML frontmatter portion of an AIP-45 AGENT-CLI.md manifest."
);
var runtimeConfigSchema = z.object({
  mode: z.string().optional(),
  options: z.record(z.string(), z.union([z.boolean(), z.number(), z.string()])).optional(),
  continuation: continuationStrategyIdSchema.optional()
}).strict();
var autoAllowPermissionHandler = ({
  options
}) => {
  if (!options || options.length === 0) {
    return { outcome: { outcome: "cancelled" } };
  }
  const allow = options.find((o) => typeof o.kind === "string" && o.kind.startsWith("allow_")) ?? options[0];
  return { outcome: { outcome: "selected", optionId: allow.optionId } };
};
var PLAN_MODE_EXIT_TOOL_CALL_KIND = "switch_mode";
var planModePermissionHandler = (params) => {
  const { options, toolCall } = params;
  if (!options || options.length === 0) {
    return { outcome: { outcome: "cancelled" } };
  }
  if (toolCall?.kind === PLAN_MODE_EXIT_TOOL_CALL_KIND) {
    const keepPlanning = options.find((o) => o.kind === "reject_once");
    return keepPlanning ? { outcome: { outcome: "selected", optionId: keepPlanning.optionId } } : { outcome: { outcome: "cancelled" } };
  }
  return autoAllowPermissionHandler(params);
};
function defaultPermissionHandlerForMode(requestedMode) {
  return requestedMode === "plan" ? planModePermissionHandler : autoAllowPermissionHandler;
}
function createAcpProtocolArm(options) {
  const { child, cwd } = options;
  if (!child.stdin || !child.stdout) {
    throw new Error(
      "AcpProtocolArm: subprocess must be spawned with piped stdin + stdout"
    );
  }
  const output = Writable.toWeb(child.stdin);
  const input = Readable.toWeb(child.stdout);
  let client = null;
  let session = null;
  const pendingByTurn = /* @__PURE__ */ new Map();
  const earlyDisconnectListeners = [];
  return {
    get sessionId() {
      return session?.sessionId;
    },
    get availableConfigOptions() {
      return session?.availableConfigOptions ?? [];
    },
    get modelApplyRejection() {
      return session?.modelApplyRejection;
    },
    get availableModes() {
      return session?.availableModes ?? [];
    },
    get currentModeId() {
      return session?.currentModeId;
    },
    isConnected() {
      return client?.isConnected() ?? true;
    },
    onDisconnect(listener) {
      if (client) client.onDisconnect(listener);
      else earlyDisconnectListeners.push(listener);
    },
    async connect(opts) {
      const permissionHandler = options.onPermissionRequest ?? defaultPermissionHandlerForMode(options.requestedMode);
      client = await createAcpClient({
        output,
        input,
        clientInfo: options.clientInfo,
        capabilities: {
          fs: { readTextFile: true, writeTextFile: true }
        },
        onActivity: opts.onActivity,
        turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
        // Permission-hold mode: surface + park requests for the daemon inbox
        // instead of auto-answering them in-arm (see AcpProtocolOptions).
        ...options.permissionHold ? { permissionHold: true } : {},
        // Wire the permission handler so the agent's `session/request_permission`
        // callbacks get a real answer instead of bubbling up as
        // "AcpClient.requestPermission: no handler configured" → which
        // surfaces in the chat as an opaque "Internal error" when the
        // agent tries to Write / Bash anything gated.
        // `params` is contextually typed as the SDK's `RequestPermissionRequest`
        // (via `AcpClientOptions.handlers`), which is assignable to
        // `AcpPermissionRequestParams` — so no cast is needed on either side.
        handlers: {
          requestPermission: async (params) => permissionHandler(params)
        }
      });
      for (const listener of earlyDisconnectListeners.splice(0)) {
        client.onDisconnect(listener);
      }
      if (opts.resumeSessionId) {
        session = await client.loadSession({
          sessionId: opts.resumeSessionId,
          cwd,
          mcpServers: opts.mcpServers
        });
      } else {
        session = await client.newSession({
          cwd,
          mcpServers: opts.mcpServers,
          ...opts.model ? { model: opts.model } : {},
          ...opts.effort ? { effort: opts.effort } : {},
          ...opts.mode ? { mode: opts.mode } : {}
        });
      }
    },
    async send(turnId, message) {
      if (!session) throw new Error("AcpProtocolArm.send: not connected");
      const messages = Array.isArray(message) ? message : [message];
      const stream = session.prompt({ messages });
      pendingByTurn.set(turnId, stream);
    },
    async *events() {
      const last = Array.from(pendingByTurn.values()).at(-1);
      if (!last) return;
      for await (const evt of last) yield evt;
    },
    async cancel(_turnId) {
      if (!session) return;
      await session.cancel();
    },
    async setConfigOption(configId, value) {
      if (!session) return { applied: false, reason: "not-connected" };
      return session.setConfigOption(configId, value);
    },
    async setSessionMode(modeId) {
      if (!session) return { applied: false, reason: "not-connected" };
      return session.setSessionMode(modeId);
    },
    onOutOfTurnEvent(listener) {
      if (!session) return () => {
      };
      return session.onOutOfTurnEvent(listener);
    },
    get steeringSupported() {
      return session?.steeringSupported ?? false;
    },
    async steer(content) {
      if (!session) return "promptRequired";
      return session.steer(content);
    },
    respondPermission(requestId, resolution) {
      return client?.respondPermission(requestId, resolution) ?? false;
    },
    async close() {
      if (session) await session.close();
      if (client) await client.close();
    }
  };
}

// src/mcp-servers.ts
function toFileBasedMcpServers(servers) {
  if (!servers || servers.length === 0) return void 0;
  const out = {};
  for (const s of servers) {
    out[s.name] = s.transport === "stdio" ? {
      command: s.ref ?? "",
      ...s.args !== void 0 ? { args: s.args } : {},
      ...s.env !== void 0 ? { env: s.env } : {}
    } : { url: s.ref ?? "", ...s.headers !== void 0 ? { headers: s.headers } : {} };
  }
  return out;
}
var DEFAULT_GRACE_MS = 3e3;
var DEFAULT_POLL_MS = 100;
function descendantsOf(rootPid, rows) {
  const byParent = /* @__PURE__ */ new Map();
  for (const r of rows) {
    const list = byParent.get(r.ppid);
    if (list) list.push(r.pid);
    else byParent.set(r.ppid, [r.pid]);
  }
  const out = [];
  const seen = /* @__PURE__ */ new Set([rootPid]);
  const queue = [rootPid];
  while (queue.length > 0) {
    const pid = queue.shift();
    for (const kid of byParent.get(pid) ?? []) {
      if (seen.has(kid)) continue;
      seen.add(kid);
      out.push(kid);
      queue.push(kid);
    }
  }
  return out;
}
async function terminateChildTree(child, opts = {}) {
  const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
  const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
  const list = opts.listProcesses ?? listProcesses;
  const rootPid = child.pid;
  const rootAlive = () => child.exitCode === null && child.signalCode === null;
  if (typeof rootPid !== "number" || rootPid <= 0 || rootPid === process.pid || !rootAlive()) {
    return { descendants: [], killed: [] };
  }
  const descendants = descendantsOf(rootPid, await list()).filter((p) => p !== process.pid);
  const tracked = new Set(descendants);
  try {
    child.kill("SIGTERM");
  } catch {
  }
  for (const pid of tracked) signal(pid, "SIGTERM");
  const anyAlive = () => rootAlive() || [...tracked].some(isAlive);
  const deadline = Date.now() + graceMs;
  while (anyAlive() && Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, pollMs));
  }
  if (!anyAlive()) return { descendants, killed: [] };
  const rows = await list();
  const survivors = [...tracked].filter(isAlive);
  for (const from of rootAlive() ? [rootPid, ...survivors] : survivors) {
    for (const pid of descendantsOf(from, rows)) {
      if (pid !== process.pid) tracked.add(pid);
    }
  }
  const killed = [];
  if (rootAlive()) {
    try {
      child.kill("SIGKILL");
      killed.push(rootPid);
    } catch {
    }
  }
  for (const pid of tracked) {
    if (isAlive(pid) && signal(pid, "SIGKILL")) killed.push(pid);
  }
  return { descendants, killed };
}
function signal(pid, sig) {
  try {
    process.kill(pid, sig);
    return true;
  } catch {
    return false;
  }
}
function isAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch (err) {
    return err.code === "EPERM";
  }
}
function listProcesses() {
  return new Promise((resolve2) => {
    execFile("ps", ["-axo", "pid=,ppid="], { maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
      if (err) return resolve2([]);
      const out = [];
      for (const line of stdout.split("\n")) {
        const m = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
        if (m) out.push({ pid: Number(m[1]), ppid: Number(m[2]) });
      }
      resolve2(out);
    });
  });
}
function isWindowsBatchFile(exe, deps) {
  if ((process.platform) !== "win32") return false;
  return /\.(?:bat|cmd)$/i.test(exe);
}
function windowsBatchShellOption(exe, deps) {
  if (isWindowsBatchFile(exe)) return { shell: true };
  return {};
}
function resolveWindowsBatchSpawn(bin, args, deps) {
  if ((process.platform) !== "win32") return void 0;
  if (!isWindowsBatchFile(bin)) return void 0;
  const stem = basename(bin).replace(/\.(?:bat|cmd)$/i, "");
  if (stem !== "npx" && stem !== "npm") return void 0;
  const script = join(dirname(bin), "node_modules", "npm", "bin", `${stem}-cli.js`);
  if (!(existsSync)(script)) return void 0;
  return { bin: process.execPath, args: [script, ...args] };
}
function defaultToolchainWritePaths() {
  const home = homedir();
  return [
    join(home, "Library", "pnpm", ".tools"),
    // macOS pnpm self-managed toolchain
    join(home, ".local", "share", "pnpm"),
    // Linux pnpm self-managed toolchain
    join(home, ".npm"),
    // npm cache — self-update / global installs
    join(home, ".claude", "session-env")
    // claude-code's own Bash-tool session bookkeeping
  ];
}
function defaultToolchainReadPaths() {
  const home = homedir();
  return [
    join(home, ".npmrc"),
    join(home, "Library", "Preferences", "pnpm", "rc"),
    join(home, ".gitconfig"),
    // git's own global config (credential.helper, user.*, …)
    join(home, ".config", "git"),
    // XDG git config fallback
    join(home, ".config", "gh"),
    // gh's config.yml + hosts.yml
    join(home, "Library", "Keychains")
    // macOS Keychain DB — git/gh credential retrieval (read-only; see doc above)
  ];
}
async function wrapAgentCliSpawn(bin, args, opts) {
  const cfg = await loadAdapterSpawnSandboxConfig(opts.cwd);
  const mode = opts.zones ? opts.mode ?? "workspace" : opts.mode ?? cfg.mode;
  if (mode === void 0) return [bin, args];
  if (mode === "off" && opts.zones) {
    throw new Error(
      `agent-cli '${opts.label}': filesystem zones (an app boundary) cannot be combined with commandSandbox "off" \u2014 the zones are only enforceable through the OS sandbox. Refusing to spawn.`
    );
  }
  if (mode === "off") {
    console.error(
      `[agent-cli] \u26A0 spawning '${opts.label}' UNCONFINED \u2014 no OS-level sandbox is active (commandSandbox mode is "off").`
    );
    return [bin, args];
  }
  const backend = resolveCommandSandbox();
  if (!backend) {
    throw new Error(
      `agent-cli '${opts.label}': commandSandbox mode="${mode}" is configured but no sandbox backend is available on ${process.platform} (macOS needs sandbox-exec, Linux needs bwrap installed). Refusing to spawn unconfined \u2014 pass commandSandbox: "off" to explicitly accept unconfined execution, or install the missing backend.`
    );
  }
  const fileCfg = opts.zones ? void 0 : cfg;
  const policy = {
    workspace: opts.cwd,
    extraReadPaths: [
      ...defaultToolchainReadPaths(),
      ...fileCfg?.extraReadPaths ?? [],
      ...opts.extraReadPaths ?? []
    ],
    extraWritePaths: [
      ...defaultToolchainWritePaths(),
      ...fileCfg?.extraWritePaths ?? [],
      ...opts.extraWritePaths ?? []
    ],
    ...opts.zones ? { zones: opts.zones } : {},
    network: mode === "strict" || cfg.network === "deny" ? "deny" : "allow"
  };
  const wrapped = backend.wrap([bin, ...args], policy);
  return [wrapped[0] ?? bin, wrapped.slice(1)];
}

// src/protocol/print-arm.ts
function printModelMismatch(requested, reported) {
  const basename3 = (id) => {
    const noSuffix = id.split("@")[0] ?? id;
    const segs = noSuffix.split("/");
    return (segs[segs.length - 1] ?? noSuffix).trim().toLowerCase();
  };
  return basename3(requested) !== basename3(reported);
}
var DEFAULT_OUTPUT = ["--output-format", "stream-json"];
var DEFAULT_PRE_PROMPT = ["--no-interactive"];
var DEFAULT_RESUME = { flag: "--resume", kind: "value" };
function createPrintSession(opts) {
  const config = opts.printConfig;
  const outputFormat = config?.output_format ?? DEFAULT_OUTPUT;
  const prePrompt = config?.pre_prompt ?? DEFAULT_PRE_PROMPT;
  const promptFlag = config?.prompt_flag;
  const resumeCfg = config?.resume ?? DEFAULT_RESUME;
  const eventSchema = config?.event_schema ?? "claude-stream-json";
  let sessionId = opts.resumeSessionId ?? "";
  let activeChild = null;
  const mcpRestore = setupMcpConfigFile(opts, eventSchema);
  return {
    get sessionId() {
      return sessionId;
    },
    async *send(message) {
      const prompt = extractPromptText(message);
      const args = [
        ...opts.baseArgs,
        ...outputFormat,
        ...prePrompt
      ];
      if (sessionId) {
        args.push(resumeCfg.flag);
        if (resumeCfg.kind === "value") args.push(sessionId);
      }
      if (promptFlag) {
        args.push(promptFlag, prompt);
      } else {
        args.push(prompt);
      }
      const [execBin, execArgs] = await wrapAgentCliSpawn(opts.bin, args, {
        mode: opts.commandSandbox,
        cwd: opts.cwd,
        ...opts.extraWritePaths ? { extraWritePaths: opts.extraWritePaths } : {},
        ...opts.extraReadPaths ? { extraReadPaths: opts.extraReadPaths } : {},
        ...opts.fsZones ? { zones: opts.fsZones } : {},
        label: "print-arm"
      });
      const child = spawn(execBin, execArgs, {
        cwd: opts.cwd,
        env: opts.env,
        stdio: ["ignore", "pipe", "pipe"],
        // Win32: only reached for a `.cmd`/`.bat` bin that
        // `resolveWindowsBatchSpawn` couldn't rewrite to its node entry
        // (see `win32-spawn.ts` — Node refuses direct batch spawning,
        // CVE-2024-27980, the recap-10 `spawn EINVAL`). POSIX: `{}`.
        ...windowsBatchShellOption(execBin)
      });
      activeChild = child;
      let spawnFailure;
      await new Promise((resolve2) => {
        child.once("spawn", () => resolve2());
        child.once("error", (err) => {
          spawnFailure = err;
          resolve2();
        });
      });
      if (spawnFailure) {
        activeChild = null;
        yield {
          kind: "error",
          error: { message: `failed to spawn '${execBin} ${execArgs.join(" ")}': ${spawnFailure.message}` }
        };
        return;
      }
      const stderrLines = [];
      const STDERR_KEEP = 80;
      child.stderr?.setEncoding("utf8");
      child.stderr?.on("data", (chunk) => {
        for (const line of chunk.split(/\r?\n/)) {
          if (!line) continue;
          stderrLines.push(line);
          if (stderrLines.length > STDERR_KEEP) stderrLines.shift();
        }
      });
      try {
        if (!child.stdout)
          throw new Error("print-arm: child has no stdout pipe");
        let capturedSessionId = "";
        const mastraState = eventSchema === "mastra-jsonl" ? createMastraMapperState() : void 0;
        const jcodeState = eventSchema === "jcode-ndjson" ? createJcodeMapperState() : void 0;
        const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
        const queue = new EventQueue();
        let modelMismatchAborted = false;
        rl.on("line", (line) => {
          if (!line.trim()) return;
          if (modelMismatchAborted) return;
          let evt;
          try {
            evt = JSON.parse(line);
          } catch {
            return;
          }
          if (opts.expectedModel && eventSchema === "jcode-ndjson" && evt.type === "start" && typeof evt.model === "string" && printModelMismatch(opts.expectedModel, evt.model)) {
            modelMismatchAborted = true;
            const provider = typeof evt.provider === "string" ? ` (provider ${evt.provider})` : "";
            queue.push({
              kind: "error",
              error: {
                message: `model mismatch: requested "${opts.expectedModel}" but the agent started on "${evt.model}"${provider} \u2014 its CLI silently fell back to a default it resolved itself. This adapter routes AND bills by the model id, so the turn was aborted instead of silently running (and billing) a model you didn't ask for. Check the id against the adapter's model menu.`
              }
            });
            queue.end();
            child.kill("SIGTERM");
            return;
          }
          const csid = captureSessionId(evt, eventSchema);
          if (csid) capturedSessionId = csid;
          const sid = capturedSessionId || sessionId || "";
          const mapped = mapEvent(evt, sid, stderrLines, eventSchema, mastraState, jcodeState);
          if (Array.isArray(mapped)) {
            for (const m of mapped) queue.push(m);
          } else if (mapped) {
            queue.push(mapped);
          }
        });
        rl.once("close", () => queue.end());
        for await (const mapped of queue) {
          yield mapped;
        }
        const exitCode = await waitForExit(child);
        if (capturedSessionId) sessionId = capturedSessionId;
        if (exitCode !== 0 && exitCode !== null) {
          const binLabel = eventSchema === "mastra-jsonl" ? "mastracode" : eventSchema === "antigravity-stream-json" ? "agy" : eventSchema === "jcode-ndjson" ? "jcode" : "claude";
          const errEvt = {
            kind: "error",
            error: {
              message: `${binLabel} exited with code ${exitCode}`,
              ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
            }
          };
          yield errEvt;
        }
      } finally {
        activeChild = null;
      }
    },
    async cancel() {
      activeChild?.kill("SIGTERM");
    },
    // The print arm spawns a fresh subprocess PER TURN from `baseArgs`,
    // which already has the model baked in (composed once, before this
    // session object was even constructed — see `bin_args_template`
    // handling in define-agent-cli.ts/compose.ts). There is no live
    // connection to apply a mid-session config change to, and rebuilding
    // `baseArgs` would mean a fresh session, not a live switch — so this
    // is honestly `requires-restart`, same as the `arg` model-apply
    // strategy for ACP arms.
    async setModel() {
      return { applied: false, reason: "requires-restart" };
    },
    // No ACP session — no wrapper-advertised capability read-surface and
    // no `session/set_mode` RPC to switch live. Mirrors the "arm has no
    // mid-session config surface" case `define-agent-cli.ts` reports for
    // `setModel` on other arms.
    availableConfigOptions: [],
    availableModes: [],
    currentModeId: void 0,
    async setSessionMode() {
      return { applied: false, reason: "not-supported" };
    },
    // Same as `setSessionMode`: a print arm has no live ACP session to apply
    // `set_config_option(configId:"effort")` against — effort is baked into
    // the spawn argv/env, so a live change is honestly `not-supported`.
    async setEffort() {
      return { applied: false, reason: "not-supported" };
    },
    async close() {
      const closing = activeChild ? terminateChildTree(activeChild) : void 0;
      mcpRestore?.();
      await closing;
    }
  };
}
function setupMcpConfigFile(opts, eventSchema) {
  if (eventSchema !== "mastra-jsonl") {
    const credentialed = (opts.mcpServers ?? []).filter((s) => s.transport !== "stdio" && s.headers !== void 0 && Object.keys(s.headers).length > 0).map((s) => s.name);
    if (credentialed.length > 0) {
      console.error(
        `[agentproto] WARNING: mcpServers ${credentialed.map((n) => `"${n}"`).join(", ")} declare headers but the "${eventSchema}" print adapter has no MCP mounting path \u2014 the mounts (and their credentials) will NOT reach the agent.`
      );
    }
    return void 0;
  }
  if (!opts.mcpServers || opts.mcpServers.length === 0) return void 0;
  const dir = join(opts.cwd, ".mastracode");
  const file = join(dir, "mcp.json");
  const injected = toFileBasedMcpServers(opts.mcpServers);
  if (!injected) return void 0;
  const fileExisted = existsSync(file);
  let originalContent;
  if (fileExisted) {
    try {
      originalContent = readFileSync(file, "utf8");
    } catch {
      originalContent = void 0;
    }
  }
  const dirExisted = existsSync(dir);
  if (!dirExisted) {
    mkdirSync(dir, { recursive: true });
  }
  let existingServers = {};
  if (originalContent !== void 0) {
    try {
      const parsed = JSON.parse(originalContent);
      if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
        existingServers = parsed.mcpServers;
      }
    } catch {
      existingServers = {};
    }
  }
  const merged = { ...existingServers, ...injected };
  const output = JSON.stringify({ mcpServers: merged }, null, 2);
  writeFileSync(file, output, "utf8");
  return () => {
    try {
      if (!fileExisted) {
        rmSync(file, { force: true });
        if (!dirExisted) {
          try {
            rmdirSync(dir);
          } catch {
          }
        }
      } else if (originalContent !== void 0) {
        let current = {};
        try {
          const parsed = JSON.parse(readFileSync(file, "utf8"));
          if (parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null) {
            current = parsed.mcpServers;
          }
        } catch {
          writeFileSync(file, originalContent, "utf8");
          return;
        }
        for (const key of Object.keys(injected)) {
          delete current[key];
        }
        if (Object.keys(current).length === 0) {
          writeFileSync(file, originalContent, "utf8");
        } else {
          writeFileSync(file, JSON.stringify({ mcpServers: current }, null, 2), "utf8");
        }
      }
    } catch {
    }
  };
}
function extractPromptText(message) {
  if (typeof message === "string") return message;
  const blocks = Array.isArray(message) ? message : [message];
  const texts = [];
  for (const block of blocks) {
    if (block !== null && typeof block === "object") {
      const b = block;
      if (b.type === "text" && typeof b.text === "string") {
        texts.push(b.text);
        continue;
      }
      if (b.type === void 0 && typeof b.text === "string") {
        texts.push(b.text);
        continue;
      }
    }
    const kind = block !== null && typeof block === "object" && typeof block.type === "string" ? block.type : typeof block;
    throw new Error(
      `this adapter has no multimodal input (capabilities.multimodal: false) \u2014 got a "${kind}" content block it can't send. Upload the file via POST /files/upload and reference its path in the prompt text instead.`
    );
  }
  return texts.join("\n");
}
function captureSessionId(evt, schema) {
  switch (schema) {
    case "claude-stream-json":
      if (evt.type === "result" && typeof evt.session_id === "string" && evt.session_id)
        return evt.session_id;
      return null;
    case "antigravity-stream-json": {
      if (evt.event === "init" && typeof evt.conversation_id === "string" && evt.conversation_id)
        return evt.conversation_id;
      if (evt.event === "result") {
        const r = evt.result;
        if (typeof r?.conversation_id === "string" && r.conversation_id)
          return r.conversation_id;
      }
      return null;
    }
    case "mastra-jsonl":
      if (evt.type === "result" && typeof evt.threadId === "string" && evt.threadId)
        return evt.threadId;
      return null;
    case "jcode-ndjson":
      if ((evt.type === "start" || evt.type === "done") && typeof evt.session_id === "string" && evt.session_id)
        return evt.session_id;
      return null;
  }
}
function mapEvent(evt, sessionId, stderrLines, schema, mastraState, jcodeState) {
  switch (schema) {
    case "claude-stream-json":
      return mapClaudeEvent(evt, sessionId, stderrLines);
    case "antigravity-stream-json":
      return mapAntigravityEvent(evt, sessionId, stderrLines);
    case "mastra-jsonl": {
      if (!mastraState) {
        return mapClaudeEvent(evt, sessionId, stderrLines);
      }
      return mapMastraEvent(evt, sessionId, stderrLines, mastraState);
    }
    case "jcode-ndjson":
      return mapJcodeEvent(evt, sessionId, jcodeState ?? createJcodeMapperState());
    default:
      return mapClaudeEvent(evt, sessionId, stderrLines);
  }
}
function mapClaudeEvent(evt, sessionId, stderrLines) {
  switch (evt.type) {
    case "text":
      return typeof evt.text === "string" ? { kind: "text-delta", sessionId, text: evt.text } : null;
    case "thinking":
      return typeof evt.thinking === "string" ? { kind: "thought", sessionId, text: evt.thinking } : null;
    case "tool_use":
      return {
        kind: "tool-call",
        sessionId,
        toolCallId: typeof evt.id === "string" ? evt.id : "",
        toolName: typeof evt.name === "string" ? evt.name : "?",
        arguments: evt.input ?? {}
      };
    case "tool_result":
      return {
        kind: "tool-result",
        sessionId,
        toolCallId: typeof evt.tool_use_id === "string" ? evt.tool_use_id : "",
        result: evt.content ?? null,
        isError: evt.is_error === true
      };
    case "result":
      if (evt.subtype === "error_during_execution" || evt.is_error === true) {
        return {
          kind: "error",
          sessionId,
          error: {
            message: typeof evt.error === "string" ? evt.error : "Unknown error",
            ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
          }
        };
      }
      if (evt.subtype === "success") {
        return { kind: "turn-end", sessionId, reason: "completed" };
      }
      return null;
    case "system":
    case "assistant":
      return null;
    default:
      return null;
  }
}
function mapAntigravityEvent(evt, sessionId, stderrLines) {
  switch (evt.event) {
    case "step_update": {
      const su = evt.step_update;
      if (!su) return null;
      const stepType = su.step_type;
      if (stepType === "agent_response" && typeof su.text_delta === "string" && su.text_delta) {
        return { kind: "text-delta", sessionId, text: su.text_delta };
      }
      if (stepType === "tool" && su.state === "DONE") {
        const ti = su.tool_info;
        const toolName = typeof su.tool_name === "string" && su.tool_name ? su.tool_name : typeof ti?.name === "string" && ti.name ? ti.name : "?";
        const toolCallId = typeof su.step_index === "number" ? `step-${su.step_index}` : "";
        const err = ti?.error;
        const call = {
          kind: "tool-call",
          sessionId,
          toolCallId,
          toolName,
          arguments: ti?.parameters ?? {}
        };
        const result = {
          kind: "tool-result",
          sessionId,
          toolCallId,
          result: err ? typeof err.message === "string" ? err.message : err : ti?.output ?? null,
          isError: !!err
        };
        return [call, result];
      }
      return null;
    }
    case "result": {
      const r = evt.result;
      if (!r) return null;
      const status = typeof r.status === "string" ? r.status : "";
      const out = [];
      const usage = mapAntigravityUsage(r.usage, sessionId);
      if (usage) out.push(usage);
      if (status === "ERROR" || status === "INVALID") {
        out.push({
          kind: "error",
          sessionId,
          error: {
            message: typeof r.error === "string" && r.error ? r.error : `agy run ended with status ${status || "ERROR"}`,
            ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
          }
        });
      } else {
        out.push({
          kind: "turn-end",
          sessionId,
          reason: mapAntigravityStatus(status)
        });
      }
      return out.length ? out : null;
    }
    // init and any unknown event carry no host-facing StreamEvent.
    default:
      return null;
  }
}
function mapAntigravityStatus(raw) {
  switch (raw) {
    case "CANCELED":
    case "INTERRUPTED":
      return "cancelled";
    // SUCCESS, WAITING, RUNNING (or anything else non-error): the turn ended.
    default:
      return "completed";
  }
}
function mapAntigravityUsage(usage, sessionId) {
  if (usage === null || typeof usage !== "object") return null;
  const u = usage;
  const tokensIn = typeof u.input_tokens === "number" ? u.input_tokens : void 0;
  const tokensOut = typeof u.output_tokens === "number" ? u.output_tokens : void 0;
  if (tokensIn === void 0 && tokensOut === void 0) return null;
  return {
    kind: "usage_update",
    sessionId,
    size: 0,
    used: 0,
    ...tokensIn !== void 0 ? { tokensIn } : {},
    ...tokensOut !== void 0 ? { tokensOut } : {}
  };
}
function createJcodeMapperState() {
  return {};
}
function mapJcodeEvent(evt, sessionId, state) {
  switch (evt.type) {
    case "text_delta":
      return typeof evt.text === "string" ? { kind: "text-delta", sessionId, text: evt.text } : null;
    case "tool_start":
      state.pending = {
        id: typeof evt.id === "string" ? evt.id : "",
        name: typeof evt.name === "string" ? evt.name : "?",
        input: ""
      };
      return null;
    case "tool_input":
      if (state.pending && typeof evt.delta === "string")
        state.pending.input += evt.delta;
      return null;
    case "tool_exec":
      return flushPendingJcodeCall(state, sessionId);
    case "tool_done": {
      const call = flushPendingJcodeCall(state, sessionId);
      const result = {
        kind: "tool-result",
        sessionId,
        toolCallId: typeof evt.id === "string" ? evt.id : "",
        result: evt.output ?? null,
        isError: evt.error !== null && evt.error !== void 0
      };
      return call ? [call, result] : result;
    }
    case "tokens": {
      const tokensIn = typeof evt.input === "number" ? evt.input : void 0;
      const tokensOut = typeof evt.output === "number" ? evt.output : void 0;
      if (tokensIn === void 0 && tokensOut === void 0) return null;
      return {
        kind: "usage_update",
        sessionId,
        size: 0,
        used: 0,
        ...tokensIn !== void 0 ? { tokensIn } : {},
        ...tokensOut !== void 0 ? { tokensOut } : {}
      };
    }
    case "done":
      return { kind: "turn-end", sessionId, reason: "completed" };
    default:
      return null;
  }
}
function flushPendingJcodeCall(state, sessionId) {
  const pending = state.pending;
  state.pending = void 0;
  if (!pending) return null;
  let args;
  try {
    args = JSON.parse(pending.input);
  } catch {
    args = { raw: pending.input };
  }
  return {
    kind: "tool-call",
    sessionId,
    toolCallId: pending.id,
    toolName: pending.name,
    arguments: args
  };
}
function createMastraMapperState() {
  return { lastTextLength: 0 };
}
function extractTextFromBlocks(content) {
  let blocks = content;
  if (blocks !== null && typeof blocks === "object" && !Array.isArray(blocks) && Array.isArray(blocks.parts)) {
    blocks = blocks.parts;
  }
  if (!Array.isArray(blocks)) return "";
  return blocks.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
}
function mapMastraEvent(evt, sessionId, stderrLines, state) {
  switch (evt.type) {
    // ── Text streaming ──────────────────────────────────────────
    case "message_update": {
      const content = evt.message?.content;
      const fullText = extractTextFromBlocks(content);
      if (fullText.length > state.lastTextLength) {
        const delta = fullText.slice(state.lastTextLength);
        state.lastTextLength = fullText.length;
        return delta ? { kind: "text-delta", sessionId, text: delta } : null;
      }
      return null;
    }
    case "message_end": {
      const msg = evt.message;
      if (msg?.role !== "assistant") return null;
      const fullText = extractTextFromBlocks(msg?.content);
      if (fullText.length > state.lastTextLength) {
        const delta = fullText.slice(state.lastTextLength);
        state.lastTextLength = fullText.length;
        return delta ? { kind: "text-delta", sessionId, text: delta } : null;
      }
      return null;
    }
    // ── Tool calls ──────────────────────────────────────────────
    case "tool_start":
      return {
        kind: "tool-call",
        sessionId,
        toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
        toolName: typeof evt.toolName === "string" ? evt.toolName : "?",
        // Mastra's controller event carries the tool call payload under
        // `args`, not `input` (that's the Claude Code stream-json field
        // name used by the sibling mapClaudeEvent tool_use case below).
        arguments: evt.args ?? {}
      };
    case "tool_end":
      return {
        kind: "tool-result",
        sessionId,
        toolCallId: typeof evt.toolCallId === "string" ? evt.toolCallId : "",
        result: evt.result ?? null,
        isError: evt.isError === true
      };
    // ── Turn end ────────────────────────────────────────────────
    case "agent_end": {
      const rawReason = typeof evt.reason === "string" ? evt.reason : "complete";
      const reason = mapMastraFinishReason(rawReason);
      return { kind: "turn-end", sessionId, reason };
    }
    // ── Final result line ────────────────────────────────────────
    // Always written last, once, whether or not `agent_end` fired —
    // pre-flight failures (bad model, unresolvable thread, missing API
    // key) call straight into this without ever streaming an
    // `agent_end` or `error` event. `turn-end` already came from
    // `agent_end` on the success path, so this only needs to surface
    // an error for the pre-flight-failure path.
    case "result": {
      const err = evt.error;
      if (!err) return null;
      return {
        kind: "error",
        sessionId,
        error: {
          message: typeof err.message === "string" ? err.message : "Unknown error",
          ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
        }
      };
    }
    // ── Errors ─────────────────────────────────────────────────
    case "error": {
      const err = evt.error;
      return {
        kind: "error",
        sessionId,
        error: {
          message: typeof err?.message === "string" ? err.message : "Unknown error",
          ...stderrLines.length ? { data: { stderr: stderrLines.join("\n") } } : {}
        }
      };
    }
    // ── Token usage ─────────────────────────────────────────────
    // Mastra Code DOES expose usage: its `AgentController` emits a native
    // `usage_update` event ({ type: "usage_update", usage: TokenUsage })
    // on every model step-finish — verified against @mastra/core 1.48.0's
    // AgentControllerEvent union and its emit site
    // (`this.#session.emit({ type: "usage_update", usage: stepUsage })`).
    // `TokenUsage` carries `{ promptTokens, completionTokens, totalTokens }`.
    // Map it to this repo's `usage_update` StreamEvent (the SAME shape
    // claude-code emits over ACP) so mastracode sessions — both the print
    // arm and the in-process arm, which share this mapper — carry token
    // telemetry in the transcript. mastracode reports these cumulatively
    // (its own headless `runMC` result-usage is last-write-wins over these
    // events), and last-write-wins on the descriptor matches that. It
    // carries no context-window size/used or per-turn cost here, so those
    // stay 0 / absent (projectEvent guards on >0, never clobbering a real
    // size).
    case "usage_update": {
      const usage = evt.usage;
      const promptTokens = typeof usage?.promptTokens === "number" ? usage.promptTokens : void 0;
      const completionTokens = typeof usage?.completionTokens === "number" ? usage.completionTokens : void 0;
      if (promptTokens === void 0 && completionTokens === void 0)
        return null;
      return {
        kind: "usage_update",
        sessionId,
        size: 0,
        used: 0,
        ...promptTokens !== void 0 ? { tokensIn: promptTokens } : {},
        ...completionTokens !== void 0 ? { tokensOut: completionTokens } : {}
      };
    }
    // ── Shell output (tool-like) ────────────────────────────────
    case "shell_output":
      return typeof evt.output === "string" ? {
        kind: "tool-result",
        sessionId,
        toolCallId: "",
        result: evt.output,
        isError: false
      } : null;
    // ── Skipped events ─────────────────────────────────────────
    case "agent_start":
    case "subagent_start":
    case "subagent_end":
    case "tool_approval_required":
    case "tool_suspended":
      return null;
    default:
      return null;
  }
}
var QUEUE_HIGH_WATER_MARK = 1e4;
var EventQueue = class {
  buffer = [];
  resolveNext = null;
  ended = false;
  highWaterWarned = false;
  push(evt) {
    this.buffer.push(evt);
    if (this.buffer.length > QUEUE_HIGH_WATER_MARK && !this.highWaterWarned) {
      this.highWaterWarned = true;
      console.warn(
        `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.`
      );
    }
    const resolve2 = this.resolveNext;
    this.resolveNext = null;
    resolve2?.();
  }
  /** Signal that no further events will be pushed. */
  end() {
    this.ended = true;
    const resolve2 = this.resolveNext;
    this.resolveNext = null;
    resolve2?.();
  }
  async *[Symbol.asyncIterator]() {
    for (; ; ) {
      const evt = this.buffer.shift();
      if (evt !== void 0) {
        yield evt;
        continue;
      }
      if (this.ended) return;
      await new Promise((resolve2) => {
        this.resolveNext = resolve2;
      });
    }
  }
};
function mapMastraFinishReason(raw) {
  switch (raw) {
    case "complete":
      return "completed";
    case "aborted":
    case "suspended":
      return "cancelled";
    case "error":
      return "error";
    default:
      return "completed";
  }
}
function waitForExit(child) {
  return new Promise((resolve2) => {
    child.once("exit", (code) => resolve2(code));
    child.once("error", () => resolve2(null));
  });
}
async function createProprietaryProtocolArm(options) {
  let mod;
  try {
    const target = isAbsolute(options.adapter) ? pathToFileURL(options.adapter).href : options.adapter;
    mod = await import(target);
  } catch (err) {
    const cause = err instanceof Error ? err.message : String(err);
    throw new Error(
      `createProprietaryProtocolArm: could not load adapter package '${options.adapter}'. Install it with: npm i ${options.adapter}
  cause: ${cause}`
    );
  }
  const candidate = "createAgentCliClient" in mod ? mod.createAgentCliClient : mod.default;
  if (typeof candidate !== "function") {
    throw new Error(
      `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>.`
    );
  }
  return await candidate(options.definition);
}

// src/legacy-modes.ts
var LEGACY_GATEWAY_MODE_IDS = /* @__PURE__ */ new Set([
  "moonshot",
  "openrouter",
  "requesty",
  "deepseek"
]);
var LEGACY_POSTURE_MODE_IDS = /* @__PURE__ */ new Set([
  "default",
  "plan",
  "accept-edits",
  "bypass-permissions",
  "read-only",
  "full-access",
  "build"
]);
function inferLegacyModeKind(modeId) {
  if (LEGACY_GATEWAY_MODE_IDS.has(modeId)) return "route";
  if (LEGACY_POSTURE_MODE_IDS.has(modeId)) return "posture";
  return "context";
}
function isLegacyExtractedModeId(modeId) {
  return LEGACY_GATEWAY_MODE_IDS.has(modeId) || LEGACY_POSTURE_MODE_IDS.has(modeId);
}

// src/manifest/compose.ts
var RuntimeConfigError = class extends Error {
  code;
  path;
  constructor(code, path, message) {
    super(`[${code} at ${path}] ${message}`);
    this.code = code;
    this.path = path;
    this.name = "RuntimeConfigError";
  }
};
function composeSpawn(handle, config, opts) {
  if (!config)
    return {
      binArgs: [...handle.bin_args ?? []],
      env: { ...handle.env ?? {} },
      envUnset: []
    };
  const prepend = [];
  const append = [];
  const env = { ...handle.env ?? {} };
  const envUnset = [];
  if (config.mode !== void 0) {
    const mode = (handle.modes ?? []).find((m) => m.id === config.mode);
    if (!mode) {
      if (!isLegacyExtractedModeId(config.mode)) {
        const known = (handle.modes ?? []).map((m) => m.id).join(", ") || "(none)";
        throw new RuntimeConfigError(
          "unknown_mode",
          "config.mode",
          `Mode '${config.mode}' is not declared by manifest '${handle.id}'. Known modes: ${known}`
        );
      }
    } else if ((mode.apply ?? "bin_args") === "bin_args") {
      if (mode.bin_args_prepend) prepend.push(...mode.bin_args_prepend);
      if (mode.bin_args_append) append.push(...mode.bin_args_append);
      if (mode.env) Object.assign(env, mode.env);
      if (mode.env_unset) envUnset.push(...mode.env_unset);
    }
  }
  if (config.options && Object.keys(config.options).length > 0) {
    const declared = handle.options ?? [];
    const declaredById = new Map(declared.map((o) => [o.id, o]));
    for (const id of Object.keys(config.options)) {
      if (!declaredById.has(id)) {
        const known = declared.map((o) => o.id).join(", ") || "(none)";
        throw new RuntimeConfigError(
          "unknown_option",
          `config.options.${id}`,
          `Option '${id}' is not declared by manifest '${handle.id}'. Known options: ${known}`
        );
      }
    }
    for (const option of declared) {
      if (!(option.id in config.options)) continue;
      const value = config.options[option.id];
      validateOptionValue(option, value);
      const patch = renderOptionPatch(option, value);
      prepend.push(...patch.prepend);
      append.push(...patch.append);
      Object.assign(env, patch.env);
      if (patch.envUnset.length) envUnset.push(...patch.envUnset);
    }
  }
  const requestedModel = config.options?.model;
  if (typeof requestedModel === "string" && handle.models?.deny?.length) {
    const denyHit = handle.models.deny.find(
      (pattern) => matchesModelPattern(pattern, requestedModel)
    );
    if (denyHit) {
      throw new RuntimeConfigError(
        "model_denied",
        "config.options.model",
        `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.`
      );
    }
  }
  if (typeof requestedModel === "string" && handle.models?.apply === "arg" && handle.models.bin_args_template && !opts?.skipModelArg) {
    append.push(
      ...handle.models.bin_args_template.map(
        (token) => token.replace("{model}", requestedModel)
      )
    );
  }
  if (config.continuation !== void 0 && handle.continuation) {
    if (!handle.continuation.supported.includes(config.continuation)) {
      throw new RuntimeConfigError(
        "unsupported_continuation",
        "config.continuation",
        `Continuation strategy '${config.continuation}' is not in manifest '${handle.id}'.continuation.supported (${handle.continuation.supported.join(", ")}).`
      );
    }
  }
  return {
    binArgs: [...prepend, ...handle.bin_args ?? [], ...append],
    env,
    envUnset
  };
}
function resolveContinuationStrategy(handle, config) {
  if (config?.continuation) return config.continuation;
  return handle.continuation?.default ?? "none";
}
function matchesModelPattern(pattern, modelId) {
  const p = pattern.toLowerCase();
  const m = modelId.toLowerCase();
  return p.endsWith("*") ? m.startsWith(p.slice(0, -1)) : m === p;
}
function validateOptionValue(option, value) {
  const path = `config.options.${option.id}`;
  switch (option.type) {
    case "boolean":
      if (typeof value !== "boolean") {
        throw new RuntimeConfigError(
          "option_type_mismatch",
          path,
          `Expected boolean, got ${typeof value} (${JSON.stringify(value)}).`
        );
      }
      return;
    case "integer":
      if (typeof value !== "number" || !Number.isInteger(value)) {
        throw new RuntimeConfigError(
          "option_type_mismatch",
          path,
          `Expected integer, got ${typeof value === "number" ? "non-integer number" : typeof value} (${JSON.stringify(value)}).`
        );
      }
      if (option.min !== void 0 && value < option.min) {
        throw new RuntimeConfigError(
          "option_bounds_violation",
          path,
          `Value ${value} is below the declared minimum ${option.min}.`
        );
      }
      if (option.max !== void 0 && value > option.max) {
        throw new RuntimeConfigError(
          "option_bounds_violation",
          path,
          `Value ${value} is above the declared maximum ${option.max}.`
        );
      }
      return;
    case "string":
      if (typeof value !== "string") {
        throw new RuntimeConfigError(
          "option_type_mismatch",
          path,
          `Expected string, got ${typeof value} (${JSON.stringify(value)}).`
        );
      }
      return;
    case "enum":
      if (typeof value !== "string") {
        throw new RuntimeConfigError(
          "option_type_mismatch",
          path,
          `Expected string (enum value), got ${typeof value} (${JSON.stringify(value)}).`
        );
      }
      if (!(option.enum ?? []).includes(value)) {
        throw new RuntimeConfigError(
          "option_enum_violation",
          path,
          `Value '${value}' is not in the declared enum (${(option.enum ?? []).join(", ")}).`
        );
      }
      return;
  }
}
function renderOptionPatch(option, value) {
  const stringValue = String(value);
  if (option.type === "boolean") {
    if (value !== true) return { prepend: [], append: [], env: {}, envUnset: [] };
    return {
      prepend: [],
      append: option.bin_args_append_when_true ? [...option.bin_args_append_when_true] : [],
      env: option.env ? interpolateEnv(option.env, stringValue) : {},
      envUnset: option.env_unset ? [...option.env_unset] : []
    };
  }
  return {
    prepend: option.bin_args_prepend ? option.bin_args_prepend.map(
      (token) => token.replace(/\{value\}/g, stringValue)
    ) : [],
    append: option.bin_args_template ? option.bin_args_template.map(
      (token) => token.replace(/\{value\}/g, stringValue)
    ) : [],
    env: option.env ? interpolateEnv(option.env, stringValue) : {},
    envUnset: option.env_unset ? [...option.env_unset] : []
  };
}
function interpolateEnv(env, value) {
  const out = {};
  for (const [k, v] of Object.entries(env)) {
    out[k] = v.replace(/\{value\}/g, value);
  }
  return out;
}
function agentCliSupportsHostContextIsolation(adapterId) {
  return adapterId === "claude-code";
}
function escapeGlob(p) {
  return p.replace(/[\\*?[\]{}()!+@]/g, "\\$&");
}
function ancestors(dir) {
  const out = [];
  let cur = dirname(dir);
  for (; ; ) {
    out.push(cur);
    const parent = dirname(cur);
    if (parent === cur) return out;
    cur = parent;
  }
}
function hostContextExcludes(cwd) {
  const abs = resolve(cwd);
  let real = abs;
  try {
    real = realpathSync(abs);
  } catch {
  }
  const dirs = /* @__PURE__ */ new Set([...ancestors(abs), ...ancestors(real)]);
  const out = [];
  for (const d of dirs) {
    const base = d === "/" ? "" : escapeGlob(d);
    out.push(
      `${base}/CLAUDE.md`,
      `${base}/CLAUDE.local.md`,
      `${base}/.claude/CLAUDE.md`,
      `${base}/.claude/rules/**`
    );
  }
  return out;
}
function createArmSessionControls(arm, definition) {
  const modelApply = definition.models?.apply ?? "config";
  return {
    // Capability read-surface (SPEC §3.9/§3.4a) — snapshotted once
    // `arm.connect()` has resolved. Not live-updated by a later
    // `setModel`/`setSessionMode` call.
    availableConfigOptions: arm.availableConfigOptions ?? [],
    availableModes: arm.availableModes ?? [],
    currentModeId: arm.currentModeId,
    /**
     * Mid-session model switch — the runtime counterpart to the
     * spawn-time model apply. Dispatches on the same
     * `definition.models.apply` strategy so a live switch behaves
     * exactly like the spawn-time apply would have, just against an
     * already-running session.
     */
    async setModel(modelId) {
      if (modelApply === "arg") {
        return { applied: false, reason: "requires-restart" };
      }
      if (modelApply === "command") {
        return applyModelCommand(arm, modelId);
      }
      if (!arm.setConfigOption) {
        return { applied: false, reason: "not-supported" };
      }
      const result = await arm.setConfigOption("model", modelId);
      return result.applied ? { applied: true, model: modelId } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
    },
    /**
     * Mid-session posture switch — the native-mode counterpart to
     * `setModel`, wired directly to the arm's `setSessionMode` (only the
     * ACP arm implements it).
     */
    async setSessionMode(modeId) {
      if (!arm.setSessionMode) {
        return { applied: false, reason: "not-supported" };
      }
      const result = await arm.setSessionMode(modeId);
      return result.applied ? { applied: true, modeId } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
    },
    /**
     * Mid-session effort switch — the effort-axis counterpart to
     * `setModel`'s `"config"` strategy, wired to the same
     * `arm.setConfigOption` surface with `configId:"effort"`. Best-effort:
     * an effort label the current model rejects resolves
     * `{applied:false, reason}` (SPEC risk R7).
     */
    async setEffort(effort) {
      if (!arm.setConfigOption) {
        return { applied: false, reason: "not-supported" };
      }
      const result = await arm.setConfigOption("effort", effort);
      return result.applied ? { applied: true, effort } : { applied: false, ...result.reason ? { reason: result.reason } : {} };
    }
  };
}
var MODEL_SWITCH_ACK_RE = /switch|model\s+set|now using/i;
function isModelSwitchAcknowledgement(evt) {
  return MODEL_SWITCH_ACK_RE.test(JSON.stringify(evt));
}
function parseModelSwitchCommand(text) {
  return /^\s*\/model\s+(\S+)/i.exec(text)?.[1];
}
async function applyModelCommand(arm, modelId) {
  const turnId = randomUUID();
  let acked = false;
  try {
    for await (const evt of promptTurn(arm, turnId, {
      type: "text",
      text: `/model ${modelId}`
    })) {
      if (isModelSwitchAcknowledgement(evt)) acked = true;
    }
  } catch (err) {
    const reason = err instanceof Error ? err.message : String(err);
    console.warn(
      `[agent-cli] /model ${modelId} control turn failed (continuing on default):`,
      err instanceof Error ? err.message : err
    );
    return { applied: false, reason };
  }
  if (!acked) {
    const reason = "no switch acknowledgement \u2014 agent may be on its default model";
    console.warn(`[agent-cli] /model ${modelId}: ${reason}`);
    return { applied: false, reason };
  }
  return { applied: true, model: modelId };
}
async function* promptTurn(arm, turnId, message) {
  await arm.send(turnId, message);
  const stderrTail = arm._stderrTail;
  for await (const evt of arm.events()) {
    if (evt.kind === "error" && typeof stderrTail === "function") {
      const tail = stderrTail();
      if (tail) {
        const existing = evt.error.data ?? {};
        evt.error.data = { ...existing, stderr: tail };
      }
    }
    yield evt;
  }
}
function npxCacheKey(packages) {
  return createHash("sha512").update([...packages].sort((a, b) => a.localeCompare(b, "en")).join("\n")).digest("hex").slice(0, 16);
}
function parseExactSpec(spec) {
  const m = /^((?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*)@(\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)$/i.exec(spec);
  return m ? { name: m[1], version: m[2] } : void 0;
}
function binNameFromManifest(name, bin) {
  const unscoped = name.replace(/^@[^/]+\//, "");
  if (typeof bin === "string") return unscoped;
  if (!bin || typeof bin !== "object") return void 0;
  const names = Object.keys(bin);
  if (names.length === 1) return names[0];
  return names.includes(unscoped) ? unscoped : void 0;
}
function resolveNpxFastPath(bin, args, env, deps) {
  if ((process.platform) === "win32") return void 0;
  if (basename(bin) !== "npx") return void 0;
  const [yes, spec, ...rest] = args;
  if (yes !== "-y" && yes !== "--yes") return void 0;
  if (spec === void 0) return void 0;
  const parsed = parseExactSpec(spec);
  if (!parsed) return void 0;
  const exists = existsSync;
  const readFile = ((p) => readFileSync(p, "utf8"));
  const cacheRoot = env["npm_config_cache"] ?? env["NPM_CONFIG_CACHE"] ?? join(homedir(), ".npm");
  const installDir = join(cacheRoot, "_npx", npxCacheKey([spec]));
  try {
    const pkg = JSON.parse(readFile(join(installDir, "node_modules", parsed.name, "package.json")));
    if (pkg.version !== parsed.version) return void 0;
    const binName = binNameFromManifest(parsed.name, pkg.bin);
    if (!binName) return void 0;
    const binDir = join(installDir, "node_modules", ".bin");
    const binPath = join(binDir, binName);
    if (!exists(binPath)) return void 0;
    return { bin: binPath, args: rest, binDir };
  } catch {
    return void 0;
  }
}

// src/define-agent-cli.ts
var defineAgentCli = createDoctype(
  {
    aip: 45,
    name: "agent-cli",
    readIdentity: (def) => def.id,
    validate(def) {
      const result = agentCliFrontmatterSchema.safeParse(def);
      if (!result.success) {
        throw new Error(
          `defineAgentCli (AIP-45): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
        );
      }
      const data = result.data;
      if (data.protocol === "acp" && !data.acp) {
        throw new Error(
          "defineAgentCli (AIP-45): `acp` ref is required when protocol=acp"
        );
      }
      if (data.protocol === "mcp" && !data.mcp) {
        throw new Error(
          "defineAgentCli (AIP-45): `mcp` block is required when protocol=mcp"
        );
      }
      if (data.protocol === "proprietary" && !data.adapter) {
        throw new Error(
          "defineAgentCli (AIP-45): `adapter` package is required when protocol=proprietary"
        );
      }
      if (data.session?.mode === "resumable" && data.capabilities?.resumable !== true) {
        throw new Error(
          "defineAgentCli (AIP-45): session.mode=resumable requires capabilities.resumable: true"
        );
      }
      if (data.models?.apply === "arg" && !data.models.bin_args_template?.length) {
        throw new Error(
          'defineAgentCli (AIP-45): models.apply="arg" requires models.bin_args_template'
        );
      }
    },
    build(def) {
      return { ...def };
    }
  }
);
function resolveSpawnBin(bin, deps) {
  const execPath = process.execPath;
  if (bin === "node") return execPath;
  if (bin === "npx" || bin === "npm") {
    const exists = existsSync;
    const sibling = join(
      dirname(execPath),
      process.platform === "win32" ? `${bin}.cmd` : bin
    );
    if (exists(sibling)) return sibling;
  }
  return bin;
}
function ensureExecDirOnPath(env, execPath = process.execPath) {
  const execDir = dirname(execPath);
  const parts = (env.PATH ?? "").split(delimiter).filter(Boolean);
  if (parts.includes(execDir)) return;
  env.PATH = [...parts, execDir].join(delimiter);
}
function createAgentCliRuntime(definition) {
  return {
    definition,
    async start(opts) {
      const cwd = opts?.cwd ?? process.cwd();
      const resolvedBin = resolveSpawnBin(definition.bin);
      const contextMode = opts?.contextProfile ? (definition.modes ?? []).find(
        (mode) => mode.id === opts.contextProfile && mode.kind === "context"
      ) : void 0;
      const config = opts?.config?.mode || !contextMode ? opts?.config : { ...opts?.config, mode: contextMode.id };
      const composed = composeSpawn(definition, config);
      const env = {
        ...filterStringEnv(process.env),
        ...composed.env
      };
      for (const k of composed.envUnset) delete env[k];
      const authSpec = opts?.auth;
      const hasGatewayAuthToken = "ANTHROPIC_AUTH_TOKEN" in composed.env;
      const hasGatewayBaseUrl = "ANTHROPIC_BASE_URL" in composed.env;
      const resolverCoupledGateway = authSpec?.baseUrl !== void 0;
      const engageAuth = !!authSpec && !hasGatewayAuthToken && (resolverCoupledGateway || !hasGatewayBaseUrl) && (authSpec.enforce === "always" || authSpec.explicit === true);
      if (authSpec && engageAuth) {
        for (const key of authSpec.unsetEnv) {
          if (!(key in composed.env)) delete env[key];
        }
        if (!authSpec.externalCredential) {
          if (!authSpec.credential) {
            const slug = definition.id;
            const providerLabel = definition.provider ?? "<provider>";
            const configBlock = `{"defaults":{"adapters":{"${slug}":{"auth":{"mode":"api-key"}}}}} in ~/.agentproto/config.json`;
            const storeHint = authSpec.ignoredApiKeyInStore ? ` (providers.json already has a stored key for ${providerLabel} \u2014 ignored until that block exists)` : "";
            const supportsSub = !!definition.authSubscription;
            const message = authSpec.neitherConfigured && supportsSub ? `agent-cli '${slug}': no billing auth. Subscription \u2192 \`claude setup-token\`. Api-key \u2192 \`agentproto auth provider set ${providerLabel} sk-\u2026\`${storeHint}. Add ${configBlock}. Never inherited from the shell.` : authSpec.mode === "subscription" ? `agent-cli '${slug}': run \`claude setup-token\` and configure the result \u2014 bills the Max/Pro subscription, not API credits. Never inherited from the shell.` : `agent-cli '${slug}': run \`agentproto auth provider set ${providerLabel} sk-\u2026\`${storeHint}, then add ${configBlock}. Never inherited from the shell.`;
            throw new RuntimeConfigError("missing_auth_credential", "opts.auth.credential", message);
          }
          env[authSpec.setEnv] = authSpec.credential;
        }
      }
      Object.assign(env, opts?.env ?? {});
      ensureExecDirOnPath(env);
      const npxFast = resolveNpxFastPath(resolvedBin, composed.binArgs, env);
      if (npxFast) {
        env.PATH = [npxFast.binDir, ...(env.PATH ?? "").split(delimiter).filter(Boolean)].join(delimiter);
      }
      const spawnBin = npxFast?.bin ?? resolvedBin;
      const spawnArgs = npxFast?.args ?? composed.binArgs;
      const batchRewrite = resolveWindowsBatchSpawn(spawnBin, spawnArgs);
      const finalBin = batchRewrite?.bin ?? spawnBin;
      const finalArgs = batchRewrite?.args ?? spawnArgs;
      if (opts?.additionalReadPaths?.length) {
        env.AGENTPROTO_ADDITIONAL_READ_PATHS = JSON.stringify(opts.additionalReadPaths);
      }
      const permissionMode = resolveClaudeCodePermissionMode(
        definition,
        opts?.posture,
        config?.mode
      );
      const isolateClaudeCodeConfig = definition.id === "claude-code";
      let claudeConfigDir;
      if (isolateClaudeCodeConfig) {
        if (opts?.configDir) {
          claudeConfigDir = opts.configDir;
          mkdirSync(claudeConfigDir, { recursive: true });
        } else {
          claudeConfigDir = mkdtempSync(
            join(tmpdir(), "agentproto-claude-config-")
          );
        }
        const globalConfigPath = join(claudeConfigDir, ".claude.json");
        let priorGlobalConfig = {};
        try {
          const parsed = JSON.parse(readFileSync(globalConfigPath, "utf8"));
          if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
            priorGlobalConfig = parsed;
          }
        } catch {
        }
        writeFileSync(
          globalConfigPath,
          JSON.stringify({ ...priorGlobalConfig, mcpServers: {} })
        );
        const isolatedSettings = {
          attribution: { commit: "", pr: "", sessionUrl: false }
        };
        const isolatedPermissions = {};
        if (permissionMode) isolatedPermissions.defaultMode = permissionMode;
        if (opts?.fsZones?.writable.length) {
          isolatedPermissions.additionalDirectories = opts.fsZones.writable;
        }
        if (Object.keys(isolatedPermissions).length > 0) {
          isolatedSettings.permissions = isolatedPermissions;
        }
        if (opts?.isolateHostContext) {
          isolatedSettings.claudeMdExcludes = hostContextExcludes(cwd);
        }
        writeFileSync(
          join(claudeConfigDir, "settings.json"),
          JSON.stringify(isolatedSettings)
        );
        env.CLAUDE_CONFIG_DIR = claudeConfigDir;
      }
      if (definition.protocol === "print") {
        const printModel = config?.options?.model;
        return createPrintSession({
          bin: finalBin,
          baseArgs: finalArgs,
          cwd,
          env,
          ...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
          ...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
          printConfig: definition.print,
          commandSandbox: opts?.commandSandbox,
          ...opts?.fsZones ? { fsZones: opts.fsZones } : {},
          ...claudeConfigDir ? { extraWritePaths: [claudeConfigDir] } : {},
          // Exact-file read grant — see the ACP arm's wrapAgentCliSpawn call.
          ...opts?.additionalReadPaths?.length ? { extraReadPaths: opts.additionalReadPaths } : {},
          ...definition.routeSelection === "derived-from-model" && printModel ? { expectedModel: String(printModel) } : {}
        });
      }
      let child;
      const stderrBuf = [];
      if (definition.protocol !== "proprietary") {
        const [execBin, execArgs] = await wrapAgentCliSpawn(
          finalBin,
          finalArgs,
          {
            mode: opts?.commandSandbox,
            cwd,
            ...opts?.fsZones ? { zones: opts.fsZones } : {},
            ...claudeConfigDir ? { extraWritePaths: [claudeConfigDir] } : {},
            // Exact-file read grant (see additionalReadPaths above):
            // READ-only exceptions to the confinement boundary, never writes.
            ...opts?.additionalReadPaths?.length ? { extraReadPaths: opts.additionalReadPaths } : {},
            label: definition.id
          }
        );
        const spawned = spawn(execBin, execArgs, {
          cwd,
          env,
          stdio: ["pipe", "pipe", "pipe"],
          signal: opts?.signal,
          // Win32: a `.cmd`/`.bat` bin that `resolveWindowsBatchSpawn`
          // couldn't rewrite to its node entry needs the shell — Node
          // refuses to spawn batch files directly (CVE-2024-27980, the
          // recap-10 `spawn EINVAL`). POSIX: `{}` — unchanged.
          ...windowsBatchShellOption(execBin)
        });
        child = spawned;
        let spawnFailure;
        await new Promise((resolve2) => {
          spawned.once("spawn", () => resolve2());
          spawned.once("error", (err) => {
            spawnFailure = err;
            resolve2();
          });
        });
        if (spawnFailure) {
          const isEnoent = spawnFailure.code === "ENOENT";
          const cwdMissing = isEnoent && !existsSync(cwd);
          const enoentHint = !isEnoent ? "" : cwdMissing ? `
cwd '${cwd}' does not exist \u2014 Node reports a missing working directory with this same ENOENT. Recreate the directory, or pass a valid cwd/workspace (is the workspace path stale, or its worktree removed?).` : `
'${execBin}' was not found on the daemon's PATH. If it works in your own terminal, the daemon's environment may be stale \u2014 restart the daemon from a shell where \`which ${definition.bin}\` resolves (\`agentproto daemon restart\`).`;
          throw new Error(
            `agent-cli '${definition.id}': failed to spawn '${execBin} ${execArgs.join(" ")}': ${spawnFailure.message}${enoentHint}`
          );
        }
        spawned.on("error", (err) => {
          stderrBuf.push(`[spawn error] ${err.message}`);
        });
        const STDERR_KEEP_LINES = 80;
        child.stderr?.setEncoding("utf8");
        child.stderr?.on("data", (chunk) => {
          for (const line of chunk.split(/\r?\n/)) {
            if (!line) continue;
            stderrBuf.push(line);
            if (stderrBuf.length > STDERR_KEEP_LINES) stderrBuf.shift();
          }
        });
      }
      const arm = await buildProtocolArm(
        definition,
        child,
        cwd,
        config?.mode,
        opts?.permissionHold ?? false
      );
      arm._stderrTail = () => stderrBuf.join("\n");
      const abortController = new AbortController();
      if (opts?.signal) {
        opts.signal.addEventListener("abort", () => abortController.abort(), {
          once: true
        });
      }
      const optModel = config?.options?.model;
      const optEffort = config?.options?.effort;
      const modelApply = definition.models?.apply ?? "config";
      const configModel = optModel && modelApply === "config" ? String(optModel) : void 0;
      const requestedModeId = config?.mode;
      const requestedModeDecl = requestedModeId ? (definition.modes ?? []).find((m) => m.id === requestedModeId) : void 0;
      const configMode = requestedModeDecl?.apply === "config" ? requestedModeId : void 0;
      const turnIdleTimeoutMs = opts?.turnIdleTimeoutMs ?? definition.session?.turn_idle_timeout_ms;
      await arm.connect({
        cwd,
        env,
        abortSignal: abortController.signal,
        ...opts?.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {},
        ...opts?.mcpServers ? { mcpServers: opts.mcpServers } : {},
        ...configModel ? { model: configModel } : {},
        ...optEffort ? { effort: String(optEffort) } : {},
        ...configMode ? { mode: configMode } : {},
        ...opts?.onActivity ? { onActivity: opts.onActivity } : {},
        ...turnIdleTimeoutMs !== void 0 ? { turnIdleTimeoutMs } : {},
        ...opts?.permissionHold ? { permissionHold: true } : {}
      });
      if (configModel && definition.routeSelection === "derived-from-model" && arm.modelApplyRejection) {
        const { requested, reason } = arm.modelApplyRejection;
        await arm.close().catch(() => {
        });
        if (child && !child.killed) child.kill("SIGTERM");
        throw derivedModelRefusalError(definition, requested, reason);
      }
      if (optModel && modelApply === "command") {
        const commandResult = await applyModelCommand(arm, String(optModel));
        if (!commandResult.applied && definition.routeSelection === "derived-from-model") {
          await arm.close().catch(() => {
          });
          if (child && !child.killed) child.kill("SIGTERM");
          throw derivedModelRefusalError(
            definition,
            String(optModel),
            commandResult.reason ?? "model switch not acknowledged"
          );
        }
      }
      const sessionId = arm.sessionId ?? randomUUID();
      let currentTurnId;
      return {
        sessionId,
        pid: child?.pid,
        // Transport liveness, delegated to the arm (see the fields' docs on
        // AgentCliRuntimeSession). An arm that models no long-lived
        // connection reports nothing, which reads as "connected" — there is
        // no stream for it to have lost, and nothing to subscribe to.
        isConnected: () => arm.isConnected?.() ?? true,
        ...arm.onDisconnect ? { onDisconnect: (listener) => arm.onDisconnect(listener) } : {},
        // Live model/mode/effort switches + the capability read-surface,
        // all pure arm delegation — shared with every host that builds
        // its own session over another transport. See session-controls.ts.
        // Snapshotted after `arm.connect()` above resolved.
        ...createArmSessionControls(arm, definition),
        send(message) {
          const turnId = randomUUID();
          currentTurnId = turnId;
          return promptTurn(arm, turnId, message);
        },
        /**
         * Cancel the in-flight turn — the wire equivalent of Ctrl-C at the
         * agent's own TUI, and the primitive the host's interrupt path
         * (`SessionsRegistry.interruptSession`, `{interrupt: true}`) is built
         * on.
         *
         * This used to be `abortController.abort()` alone, which cancelled
         * NOTHING: that controller is the CONNECTION-level signal, handed to
         * `arm.connect({abortSignal})` at spawn, and no arm reads it. So the
         * abort fired into the void while the agent kept working — a turn
         * "cancelled" this way ran happily to completion, and the host's 60s
         * settle-wait then timed out on a turn that was never asked to stop.
         * It only ever LOOKED like it worked for turns that happened to end
         * naturally inside that wait.
         *
         * Both arms have always implemented the real thing — the ACP arm
         * sends `session/cancel`, the print arm SIGTERMs the child — it was
         * simply never called. Call it.
         *
         * Connection-level abort deliberately stays out of this: aborting the
         * connection would end the SESSION, and cancelling a turn must leave
         * it alive and idle for the next prompt. That distinction is the whole
         * point of interrupt-vs-kill.
         */
        async cancel() {
          if (currentTurnId === void 0) return;
          await arm.cancel(currentTurnId);
        },
        ...arm.respondPermission ? {
          respondPermission(requestId, resolution) {
            return arm.respondPermission(requestId, resolution);
          }
        } : {},
        ...arm.onOutOfTurnEvent ? {
          onOutOfTurnEvent(listener) {
            return arm.onOutOfTurnEvent(listener);
          }
        } : {},
        ...arm.steer ? {
          // Snapshotted after `arm.connect()` resolved, like the
          // config/mode read surfaces above.
          steeringSupported: arm.steeringSupported === true,
          steer(content) {
            return arm.steer(content);
          }
        } : {},
        async close() {
          await arm.close();
          if (child) await terminateChildTree(child);
        }
      };
    }
  };
}
async function buildProtocolArm(def, child, cwd, requestedMode, permissionHold) {
  switch (def.protocol) {
    case "acp":
      if (!child) {
        throw new Error("createAgentCliRuntime: acp protocol requires a spawned subprocess");
      }
      return createAcpProtocolArm({
        child,
        cwd,
        clientInfo: { name: def.id, version: def.version },
        requestedMode,
        ...permissionHold ? { permissionHold: true } : {}
      });
    case "mcp":
      throw new Error("createAgentCliRuntime: mcp protocol arm not yet implemented");
    case "proprietary":
      if (!def.adapter) {
        throw new Error(
          "createAgentCliRuntime: proprietary protocol requires manifest.adapter"
        );
      }
      return createProprietaryProtocolArm({ adapter: def.adapter, definition: def });
    case "print":
      throw new Error("createAgentCliRuntime: print arm bypasses buildProtocolArm");
  }
}
function resolveClaudeCodePermissionMode(definition, posture, configMode) {
  if (definition.id !== "claude-code") return void 0;
  const canonicalPermissionMode = {
    plan: "plan",
    "read-only": "plan",
    "accept-edits": "acceptEdits",
    bypass: "bypassPermissions"
  };
  if (posture && canonicalPermissionMode[posture]) {
    return canonicalPermissionMode[posture];
  }
  if (!configMode) return void 0;
  const mode = (definition.modes ?? []).find((m) => m.id === configMode);
  const args = mode?.bin_args_append ?? [];
  const flagIndex = args.indexOf("--permission-mode");
  if (flagIndex === -1 || flagIndex + 1 >= args.length) return void 0;
  return args[flagIndex + 1];
}
function filterStringEnv(env) {
  const out = {};
  for (const [k, v] of Object.entries(env)) {
    if (typeof v === "string") out[k] = v;
  }
  return out;
}
function derivedModelRefusalError(definition, requested, reason) {
  const first = definition.models?.allowed?.[0];
  const example = (typeof first === "string" ? first : first?.id) ?? "openrouter/anthropic/claude-sonnet-4-6";
  return new Error(
    `${definition.bin}: refusing to start on the default model \u2014 the requested model "${requested}" was not applied (${reason}). This adapter routes AND bills by the model id's own provider prefix, so continuing would silently run (and bill) a model you didn't ask for. Use the adapter's "<provider>/<model>" form (e.g. "${example}"), or pick an id from its model menu.`
  );
}

export { LEGACY_GATEWAY_MODE_IDS, LEGACY_POSTURE_MODE_IDS, RuntimeConfigError, agentCliFrontmatterSchema, agentCliSupportsHostContextIsolation, applyModelCommand, autoAllowPermissionHandler, composeSpawn, createAcpProtocolArm, createAgentCliRuntime, createArmSessionControls, createJcodeMapperState, createMastraMapperState, createPrintSession, createProprietaryProtocolArm, defineAgentCli, hostContextExcludes, inferLegacyModeKind, isLegacyExtractedModeId, isModelSwitchAcknowledgement, mapJcodeEvent, mapMastraEvent, parseModelSwitchCommand, planModePermissionHandler, promptTurn, resolveContinuationStrategy, runtimeConfigSchema, toFileBasedMcpServers };
//# sourceMappingURL=chunk-Y3FGLQ4R.mjs.map
//# sourceMappingURL=chunk-Y3FGLQ4R.mjs.map