@openape/apes 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/notifications.ts","../src/shell/apes-self-dispatch.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport consola from 'consola'\nimport { quote } from 'shell-quote'\nimport { loadConfig } from './config'\n\nexport interface PendingGrantInfo {\n grantId: string\n approveUrl: string\n command: string\n audience: string\n host: string\n}\n\n/**\n * Resolve the notification command for pending grants. Checks (in order):\n * 1. `APES_NOTIFY_PENDING_COMMAND` env var (highest priority — lets\n * parent programs like openclaw override per invocation)\n * 2. `[notifications] pending_command` in ~/.config/apes/config.toml\n *\n * Returns undefined if no notification command is configured.\n */\nfunction resolvePendingCommand(): string | undefined {\n if (process.env.APES_NOTIFY_PENDING_COMMAND)\n return process.env.APES_NOTIFY_PENDING_COMMAND\n\n const config = loadConfig()\n return config.notifications?.pending_command\n}\n\n/**\n * Escape a value for safe embedding inside a single-quoted shell string.\n * We use `shell-quote` to produce a safe literal, then strip the outer\n * quoting because the template substitution embeds the value inside the\n * user's command template which is itself passed to `sh -c`.\n */\nfunction shellEscape(value: string): string {\n // quote() wraps in single quotes and escapes internal single quotes\n // e.g. \"it's\" → \"'it'\\\\''s'\"\n // We return the raw escaped form so it's safe inside sh -c.\n return quote([value])\n}\n\n/**\n * Substitute template variables in the notification command.\n * All values are shell-escaped to prevent injection.\n */\nfunction renderTemplate(template: string, info: PendingGrantInfo): string {\n return template\n .replace(/\\{grant_id\\}/g, shellEscape(info.grantId))\n .replace(/\\{approve_url\\}/g, shellEscape(info.approveUrl))\n .replace(/\\{command\\}/g, shellEscape(info.command))\n .replace(/\\{audience\\}/g, shellEscape(info.audience))\n .replace(/\\{host\\}/g, shellEscape(info.host))\n}\n\n/**\n * Send a notification that a grant is awaiting human approval.\n *\n * This is **fire-and-forget**: the notification subprocess runs detached\n * and unref'd so it cannot block the grant flow. A 10-second timeout\n * kills it if it hangs (e.g. network issue reaching Telegram API).\n *\n * Only fires when a notification command is configured. Silently returns\n * if not — the grant flow must never depend on notifications.\n *\n * Only call this when the grant **actually requires waiting** (new grant\n * with pending status). Do NOT call when:\n * - An existing timed/always grant was reused (no human action needed)\n * - The grant was instantly approved (no waiting phase)\n */\nexport function notifyGrantPending(info: PendingGrantInfo): void {\n const template = resolvePendingCommand()\n if (!template)\n return\n\n const rendered = renderTemplate(template, info)\n\n try {\n const child = spawn('sh', ['-c', rendered], {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env },\n })\n\n // Don't let the notification process keep the parent alive\n child.unref()\n\n // Kill after 10 seconds if it hasn't exited\n const timeout = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n }\n catch {}\n }, 10_000)\n timeout.unref()\n\n child.on('exit', () => clearTimeout(timeout))\n }\n catch (err) {\n // Never let notification failure break the grant flow\n consola.debug('Notification command failed:', err)\n }\n}\n","import { basename } from 'node:path'\nimport type { ParsedShellCommand } from '../shapes/shell-parser.js'\n\n/**\n * Subset of `apes` subcommands that remain grant-gated even when invoked\n * as self-dispatches from inside an ape-shell context. These are the\n * three categories where the shell-grant layer adds real security value\n * that isn't duplicated by server-side auth gates or local-file-only\n * semantics:\n *\n * - `run` — spawns arbitrary executables, the core of the grant system\n * - `fetch` — forwards the bearer token to a user-specified URL\n * - `mcp` — binds a network port and serves a persistent API\n *\n * Every other `apes <subcmd>` either reads state, mutates the user's own\n * local config, or talks to the IdP through endpoints that are already\n * scoped by the auth token — gating them in the shell is redundant\n * friction, and under 0.9.0's async-default grant flow it actively\n * breaks `apes grants run <id>` via recursion (the polling call itself\n * creates a new grant, cascading indefinitely).\n *\n * This is the single source of truth shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode` (which\n * receives the bash-c-wrapped command after `rewriteApeShellArgs`\n * rewrites `ape-shell -c \"<cmd>\"` into `apes run --shell -- bash -c <cmd>`)\n *\n * Keep this list in sync with the blocklist snapshot test in\n * `shell-grant-dispatch.test.ts` — the tripwire that forces a review\n * decision whenever a new top-level apes subcommand is added.\n */\nexport const APES_GATED_SUBCOMMANDS = new Set(['run', 'fetch', 'mcp'])\n\n/**\n * Returns true if the parsed shell command is an `apes <subcmd>`\n * invocation that should bypass the grant flow entirely. Non-apes\n * binaries, compound lines (pipes, &&, etc.), and subcommands in\n * `APES_GATED_SUBCOMMANDS` all return false so they stay on the normal\n * grant path.\n *\n * The caller (either `requestGrantForShellLine` for the REPL path or\n * `runShellMode` for the one-shot path) is responsible for parsing the\n * input string and passing the resulting ParsedShellCommand here.\n */\nexport function isApesSelfDispatch(parsed: ParsedShellCommand | null | undefined): boolean {\n if (!parsed || parsed.isCompound)\n return false\n const invokedName = basename(parsed.executable)\n if (invokedName !== 'apes' && invokedName !== 'apes.js')\n return false\n const subCommand = parsed.argv[0]\n if (!subCommand)\n return false\n return !APES_GATED_SUBCOMMANDS.has(subCommand)\n}\n\n/**\n * Result of checking a parsed shell command for a leading `sudo` token.\n * The `reason` doubles as a ready-to-print error message so the REPL\n * and one-shot paths emit byte-identical text.\n */\nexport interface SudoRejection {\n reason: string\n}\n\n/**\n * Returns a rejection hint if the parsed line is a simple, non-compound\n * command whose leading executable is `sudo`. `sudo` is not available\n * inside ape-shell (the wrapper user is not in /etc/sudoers by design),\n * so agents and humans should use the explicit\n * `apes run --as root -- <cmd>` flow which routes through the escapes\n * setuid binary.\n *\n * Compound lines (pipes, &&, etc.) return null so the downstream\n * session-grant path can still negotiate a grant and bash surfaces the\n * real sudo error. We only short-circuit the leading-sudo case which is\n * the agent footgun.\n *\n * Shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode`\n */\nexport function checkSudoRejection(parsed: ParsedShellCommand | null | undefined): SudoRejection | null {\n if (!parsed || parsed.isCompound) return null\n if (basename(parsed.executable) !== 'sudo') return null\n const rest = parsed.argv.join(' ').trim()\n const hint = rest.length > 0\n ? `apes run --as root -- ${rest}`\n : 'apes run --as root -- <cmd>'\n return {\n reason: `sudo is not available in ape-shell. Use \\`${hint}\\` for privileged commands.`,\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa;AACtB,OAAO,aAAa;AACpB,SAAS,aAAa;AAmBtB,SAAS,wBAA4C;AACnD,MAAI,QAAQ,IAAI;AACd,WAAO,QAAQ,IAAI;AAErB,QAAM,SAAS,WAAW;AAC1B,SAAO,OAAO,eAAe;AAC/B;AAQA,SAAS,YAAY,OAAuB;AAI1C,SAAO,MAAM,CAAC,KAAK,CAAC;AACtB;AAMA,SAAS,eAAe,UAAkB,MAAgC;AACxE,SAAO,SACJ,QAAQ,iBAAiB,YAAY,KAAK,OAAO,CAAC,EAClD,QAAQ,oBAAoB,YAAY,KAAK,UAAU,CAAC,EACxD,QAAQ,gBAAgB,YAAY,KAAK,OAAO,CAAC,EACjD,QAAQ,iBAAiB,YAAY,KAAK,QAAQ,CAAC,EACnD,QAAQ,aAAa,YAAY,KAAK,IAAI,CAAC;AAChD;AAiBO,SAAS,mBAAmB,MAA8B;AAC/D,QAAM,WAAW,sBAAsB;AACvC,MAAI,CAAC;AACH;AAEF,QAAM,WAAW,eAAe,UAAU,IAAI;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,QAAQ,GAAG;AAAA,MAC1C,UAAU;AAAA,MACV,OAAO;AAAA,MACP,KAAK,EAAE,GAAG,QAAQ,IAAI;AAAA,IACxB,CAAC;AAGD,UAAM,MAAM;AAGZ,UAAM,UAAU,WAAW,MAAM;AAC/B,UAAI;AACF,cAAM,KAAK,SAAS;AAAA,MACtB,QACM;AAAA,MAAC;AAAA,IACT,GAAG,GAAM;AACT,YAAQ,MAAM;AAEd,UAAM,GAAG,QAAQ,MAAM,aAAa,OAAO,CAAC;AAAA,EAC9C,SACO,KAAK;AAEV,YAAQ,MAAM,gCAAgC,GAAG;AAAA,EACnD;AACF;;;ACtGA,SAAS,gBAAgB;AA+BlB,IAAM,yBAAyB,oBAAI,IAAI,CAAC,OAAO,SAAS,KAAK,CAAC;AAa9D,SAAS,mBAAmB,QAAwD;AACzF,MAAI,CAAC,UAAU,OAAO;AACpB,WAAO;AACT,QAAM,cAAc,SAAS,OAAO,UAAU;AAC9C,MAAI,gBAAgB,UAAU,gBAAgB;AAC5C,WAAO;AACT,QAAM,aAAa,OAAO,KAAK,CAAC;AAChC,MAAI,CAAC;AACH,WAAO;AACT,SAAO,CAAC,uBAAuB,IAAI,UAAU;AAC/C;AA4BO,SAAS,mBAAmB,QAAqE;AACtG,MAAI,CAAC,UAAU,OAAO,WAAY,QAAO;AACzC,MAAI,SAAS,OAAO,UAAU,MAAM,OAAQ,QAAO;AACnD,QAAM,OAAO,OAAO,KAAK,KAAK,GAAG,EAAE,KAAK;AACxC,QAAM,OAAO,KAAK,SAAS,IACvB,yBAAyB,IAAI,KAC7B;AACJ,SAAO;AAAA,IACL,QAAQ,6CAA6C,IAAI;AAAA,EAC3D;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/notifications.ts","../src/shell/apes-self-dispatch.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport consola from 'consola'\nimport { quote } from 'shell-quote'\nimport { loadConfig } from './config'\n\nexport interface PendingGrantInfo {\n grantId: string\n approveUrl: string\n command: string\n audience: string\n host: string\n}\n\n/**\n * Resolve the notification command for pending grants. Checks (in order):\n * 1. `APES_NOTIFY_PENDING_COMMAND` env var (highest priority — lets\n * parent programs like openclaw override per invocation)\n * 2. `[notifications] pending_command` in ~/.config/apes/config.toml\n *\n * Returns undefined if no notification command is configured.\n */\nfunction resolvePendingCommand(): string | undefined {\n if (process.env.APES_NOTIFY_PENDING_COMMAND)\n return process.env.APES_NOTIFY_PENDING_COMMAND\n\n const config = loadConfig()\n return config.notifications?.pending_command\n}\n\n/**\n * Escape a value for safe embedding inside a single-quoted shell string.\n * We use `shell-quote` to produce a safe literal, then strip the outer\n * quoting because the template substitution embeds the value inside the\n * user's command template which is itself passed to `sh -c`.\n */\nfunction shellEscape(value: string): string {\n // quote() wraps in single quotes and escapes internal single quotes\n // e.g. \"it's\" → \"'it'\\\\''s'\"\n // We return the raw escaped form so it's safe inside sh -c.\n return quote([value])\n}\n\n/**\n * Substitute template variables in the notification command.\n * All values are shell-escaped to prevent injection.\n */\nfunction renderTemplate(template: string, info: PendingGrantInfo): string {\n return template\n .replace(/\\{grant_id\\}/g, shellEscape(info.grantId))\n .replace(/\\{approve_url\\}/g, shellEscape(info.approveUrl))\n .replace(/\\{command\\}/g, shellEscape(info.command))\n .replace(/\\{audience\\}/g, shellEscape(info.audience))\n .replace(/\\{host\\}/g, shellEscape(info.host))\n}\n\n/**\n * Send a notification that a grant is awaiting human approval.\n *\n * This is **fire-and-forget**: the notification subprocess runs detached\n * and unref'd so it cannot block the grant flow. A 10-second timeout\n * kills it if it hangs (e.g. network issue reaching Telegram API).\n *\n * Only fires when a notification command is configured. Silently returns\n * if not — the grant flow must never depend on notifications.\n *\n * Only call this when the grant **actually requires waiting** (new grant\n * with pending status). Do NOT call when:\n * - An existing timed/always grant was reused (no human action needed)\n * - The grant was instantly approved (no waiting phase)\n */\nexport function notifyGrantPending(info: PendingGrantInfo): void {\n const template = resolvePendingCommand()\n if (!template)\n return\n\n const rendered = renderTemplate(template, info)\n\n try {\n const child = spawn('sh', ['-c', rendered], {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env },\n })\n\n // Don't let the notification process keep the parent alive\n child.unref()\n\n // Kill after 10 seconds if it hasn't exited\n const timeout = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n }\n catch {}\n }, 10_000)\n timeout.unref()\n\n child.on('exit', () => clearTimeout(timeout))\n }\n catch (err) {\n // Never let notification failure break the grant flow\n consola.debug('Notification command failed:', err)\n }\n}\n","import { basename } from 'node:path'\nimport type { ParsedShellCommand } from '../shapes/shell-parser.js'\n\n/**\n * Subset of `apes` subcommands that remain grant-gated even when invoked\n * as self-dispatches from inside an ape-shell context. These are the\n * three categories where the shell-grant layer adds real security value\n * that isn't duplicated by server-side auth gates or local-file-only\n * semantics:\n *\n * - `run` — spawns arbitrary executables, the core of the grant system\n * - `fetch` — forwards the bearer token to a user-specified URL\n * - `mcp` — binds a network port and serves a persistent API\n *\n * Every other `apes <subcmd>` either reads state, mutates the user's own\n * local config, or talks to the IdP through endpoints that are already\n * scoped by the auth token — gating them in the shell is redundant\n * friction, and under 0.9.0's async-default grant flow it actively\n * breaks `apes grants run <id>` via recursion (the polling call itself\n * creates a new grant, cascading indefinitely).\n *\n * This is the single source of truth shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode` (which\n * receives the bash-c-wrapped command after `rewriteApeShellArgs`\n * rewrites `ape-shell -c \"<cmd>\"` into `apes run --shell -- bash -c <cmd>`)\n *\n * Keep this list in sync with the blocklist snapshot test in\n * `shell-grant-dispatch.test.ts` — the tripwire that forces a review\n * decision whenever a new top-level apes subcommand is added.\n */\nexport const APES_GATED_SUBCOMMANDS = new Set(['run', 'fetch', 'mcp'])\n\n/**\n * Returns true if the parsed shell command is an `apes <subcmd>`\n * invocation that should bypass the grant flow entirely. Non-apes\n * binaries, compound lines (pipes, &&, etc.), and subcommands in\n * `APES_GATED_SUBCOMMANDS` all return false so they stay on the normal\n * grant path.\n *\n * The caller (either `requestGrantForShellLine` for the REPL path or\n * `runShellMode` for the one-shot path) is responsible for parsing the\n * input string and passing the resulting ParsedShellCommand here.\n */\nexport function isApesSelfDispatch(parsed: ParsedShellCommand | null | undefined): boolean {\n if (!parsed || parsed.isCompound)\n return false\n const invokedName = basename(parsed.executable)\n if (invokedName !== 'apes' && invokedName !== 'apes.js')\n return false\n const subCommand = parsed.argv[0]\n if (!subCommand)\n return false\n if (!APES_GATED_SUBCOMMANDS.has(subCommand))\n return true\n // `apes run --as <user>` has its own internal escapes-audience grant\n // flow (runAdapterMode delegates to runAudienceMode('escapes', ...)).\n // Double-gating it through the ape-shell session-grant layer would\n // fall through to a generic session grant that never reaches escapes.\n // Let it self-dispatch so the inner apes process handles elevation.\n if (subCommand === 'run' && parsed.argv.includes('--as'))\n return true\n return false\n}\n\n/**\n * Result of checking a parsed shell command for a leading `sudo` token.\n * The `reason` doubles as a ready-to-print error message so the REPL\n * and one-shot paths emit byte-identical text.\n */\nexport interface SudoRejection {\n reason: string\n}\n\n/**\n * Returns a rejection hint if the parsed line is a simple, non-compound\n * command whose leading executable is `sudo`. `sudo` is not available\n * inside ape-shell (the wrapper user is not in /etc/sudoers by design),\n * so agents and humans should use the explicit\n * `apes run --as root -- <cmd>` flow which routes through the escapes\n * setuid binary.\n *\n * Compound lines (pipes, &&, etc.) return null so the downstream\n * session-grant path can still negotiate a grant and bash surfaces the\n * real sudo error. We only short-circuit the leading-sudo case which is\n * the agent footgun.\n *\n * Shared by both dispatch paths:\n * - Interactive REPL: `shell/grant-dispatch.ts` → `requestGrantForShellLine`\n * - One-shot `ape-shell -c`: `commands/run.ts` → `runShellMode`\n */\nexport function checkSudoRejection(parsed: ParsedShellCommand | null | undefined): SudoRejection | null {\n if (!parsed || parsed.isCompound) return null\n if (basename(parsed.executable) !== 'sudo') return null\n const rest = parsed.argv.join(' ').trim()\n const hint = rest.length > 0\n ? `apes run --as root -- ${rest}`\n : 'apes run --as root -- <cmd>'\n return {\n reason: `sudo is not available in ape-shell. Use \\`${hint}\\` for privileged commands.`,\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa;AACtB,OAAO,aAAa;AACpB,SAAS,aAAa;AAmBtB,SAAS,wBAA4C;AACnD,MAAI,QAAQ,IAAI;AACd,WAAO,QAAQ,IAAI;AAErB,QAAM,SAAS,WAAW;AAC1B,SAAO,OAAO,eAAe;AAC/B;AAQA,SAAS,YAAY,OAAuB;AAI1C,SAAO,MAAM,CAAC,KAAK,CAAC;AACtB;AAMA,SAAS,eAAe,UAAkB,MAAgC;AACxE,SAAO,SACJ,QAAQ,iBAAiB,YAAY,KAAK,OAAO,CAAC,EAClD,QAAQ,oBAAoB,YAAY,KAAK,UAAU,CAAC,EACxD,QAAQ,gBAAgB,YAAY,KAAK,OAAO,CAAC,EACjD,QAAQ,iBAAiB,YAAY,KAAK,QAAQ,CAAC,EACnD,QAAQ,aAAa,YAAY,KAAK,IAAI,CAAC;AAChD;AAiBO,SAAS,mBAAmB,MAA8B;AAC/D,QAAM,WAAW,sBAAsB;AACvC,MAAI,CAAC;AACH;AAEF,QAAM,WAAW,eAAe,UAAU,IAAI;AAE9C,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,QAAQ,GAAG;AAAA,MAC1C,UAAU;AAAA,MACV,OAAO;AAAA,MACP,KAAK,EAAE,GAAG,QAAQ,IAAI;AAAA,IACxB,CAAC;AAGD,UAAM,MAAM;AAGZ,UAAM,UAAU,WAAW,MAAM;AAC/B,UAAI;AACF,cAAM,KAAK,SAAS;AAAA,MACtB,QACM;AAAA,MAAC;AAAA,IACT,GAAG,GAAM;AACT,YAAQ,MAAM;AAEd,UAAM,GAAG,QAAQ,MAAM,aAAa,OAAO,CAAC;AAAA,EAC9C,SACO,KAAK;AAEV,YAAQ,MAAM,gCAAgC,GAAG;AAAA,EACnD;AACF;;;ACtGA,SAAS,gBAAgB;AA+BlB,IAAM,yBAAyB,oBAAI,IAAI,CAAC,OAAO,SAAS,KAAK,CAAC;AAa9D,SAAS,mBAAmB,QAAwD;AACzF,MAAI,CAAC,UAAU,OAAO;AACpB,WAAO;AACT,QAAM,cAAc,SAAS,OAAO,UAAU;AAC9C,MAAI,gBAAgB,UAAU,gBAAgB;AAC5C,WAAO;AACT,QAAM,aAAa,OAAO,KAAK,CAAC;AAChC,MAAI,CAAC;AACH,WAAO;AACT,MAAI,CAAC,uBAAuB,IAAI,UAAU;AACxC,WAAO;AAMT,MAAI,eAAe,SAAS,OAAO,KAAK,SAAS,MAAM;AACrD,WAAO;AACT,SAAO;AACT;AA4BO,SAAS,mBAAmB,QAAqE;AACtG,MAAI,CAAC,UAAU,OAAO,WAAY,QAAO;AACzC,MAAI,SAAS,OAAO,UAAU,MAAM,OAAQ,QAAO;AACnD,QAAM,OAAO,OAAO,KAAK,KAAK,GAAG,EAAE,KAAK;AACxC,QAAM,OAAO,KAAK,SAAS,IACvB,yBAAyB,IAAI,KAC7B;AACJ,SAAO;AAAA,IACL,QAAQ,6CAA6C,IAAI;AAAA,EAC3D;AACF;","names":[]}
package/dist/cli.js CHANGED
@@ -12,10 +12,12 @@ import {
12
12
  checkSudoRejection,
13
13
  isApesSelfDispatch,
14
14
  notifyGrantPending
15
- } from "./chunk-M2NBHR2E.js";
15
+ } from "./chunk-OFIVF6NH.js";
16
16
  import {
17
17
  ApiError,
18
+ GENERIC_OPERATION_ID,
18
19
  apiFetch,
20
+ buildGenericResolved,
19
21
  buildStructuredCliGrantRequest,
20
22
  createShapesGrant,
21
23
  extractOption,
@@ -40,21 +42,23 @@ import {
40
42
  resolveCapabilityRequest,
41
43
  resolveCommand,
42
44
  resolveFromGrant,
45
+ resolveGenericOrReject,
43
46
  searchAdapters,
44
47
  verifyAndExecute,
45
48
  waitForGrantStatus
46
- } from "./chunk-U4CI2RBO.js";
49
+ } from "./chunk-O7GSG3OE.js";
47
50
  import {
48
51
  AUTH_FILE,
49
52
  CONFIG_DIR,
50
53
  clearAuth,
51
54
  getAuthToken,
52
55
  getIdpUrl,
56
+ isGenericFallbackEnabled,
53
57
  loadAuth,
54
58
  loadConfig,
55
59
  saveAuth,
56
60
  saveConfig
57
- } from "./chunk-6JSOSD7R.js";
61
+ } from "./chunk-6GPSKAMU.js";
58
62
 
59
63
  // src/cli.ts
60
64
  import consola27 from "consola";
@@ -1187,15 +1191,24 @@ var runGrantCommand = defineCommand12({
1187
1191
  const hasOpenApeCliDetail = authDetails.some((d) => d?.type === "openape_cli");
1188
1192
  const isShapesGrant = hasOpenApeCliDetail || audience === "shapes";
1189
1193
  if (isShapesGrant) {
1194
+ const isGenericGrant = authDetails.some((d) => d?.operation_id === GENERIC_OPERATION_ID);
1190
1195
  let resolved;
1191
- try {
1192
- resolved = await resolveFromGrant(grant);
1193
- } catch (err) {
1194
- const msg = err instanceof Error ? err.message : String(err);
1195
- throw new CliError(`Cannot re-resolve grant: ${msg}`);
1196
+ if (isGenericGrant) {
1197
+ const argv = grant.request?.command ?? [];
1198
+ if (argv.length === 0)
1199
+ throw new CliError(`Generic grant ${grant.id} is missing command argv`);
1200
+ const cliId = authDetails.find((d) => d?.operation_id === GENERIC_OPERATION_ID)?.cli_id ?? argv[0];
1201
+ resolved = await buildGenericResolved(cliId, argv);
1202
+ } else {
1203
+ try {
1204
+ resolved = await resolveFromGrant(grant);
1205
+ } catch (err) {
1206
+ const msg = err instanceof Error ? err.message : String(err);
1207
+ throw new CliError(`Cannot re-resolve grant: ${msg}`);
1208
+ }
1196
1209
  }
1197
1210
  const token = await fetchGrantToken(idp, grant.id);
1198
- await verifyAndExecute(token, resolved);
1211
+ await verifyAndExecute(token, resolved, grant.id);
1199
1212
  return;
1200
1213
  }
1201
1214
  if (audience === "escapes") {
@@ -2244,7 +2257,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
2244
2257
  if (existingGrantId) {
2245
2258
  consola19.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
2246
2259
  const token = await fetchGrantToken(idp, existingGrantId);
2247
- await verifyAndExecute(token, resolved);
2260
+ await verifyAndExecute(token, resolved, existingGrantId);
2248
2261
  return true;
2249
2262
  }
2250
2263
  } catch {
@@ -2275,7 +2288,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
2275
2288
  if (status !== "approved")
2276
2289
  throw new CliError(`Grant ${status}`);
2277
2290
  const token = await fetchGrantToken(idp, grant.id);
2278
- await verifyAndExecute(token, resolved);
2291
+ await verifyAndExecute(token, resolved, grant.id);
2279
2292
  return true;
2280
2293
  }
2281
2294
  printPendingGrantInfo(grant, idp);
@@ -2311,6 +2324,11 @@ function extractPositionals(rawArgs) {
2311
2324
  }
2312
2325
  return positionals;
2313
2326
  }
2327
+ function printGenericWarning(cliId) {
2328
+ process.stderr.write(`\u26A0 No shape registered for \`${cliId}\`.
2329
+ `);
2330
+ process.stderr.write("Generic mode active \u2014 single-use grant will be required.\n");
2331
+ }
2314
2332
  async function runAdapterMode(command, rawArgs, args) {
2315
2333
  const idp = getIdpUrl(args.idp);
2316
2334
  if (!idp)
@@ -2320,15 +2338,28 @@ async function runAdapterMode(command, rawArgs, args) {
2320
2338
  return;
2321
2339
  }
2322
2340
  const adapterOpt = extractOption(rawArgs, "adapter");
2323
- const loaded = loadAdapter(command[0], adapterOpt);
2324
- const resolved = await resolveCommand(loaded, command);
2341
+ const cliId = command[0];
2342
+ let resolved;
2343
+ try {
2344
+ const loaded = loadAdapter(cliId, adapterOpt);
2345
+ resolved = await resolveCommand(loaded, command);
2346
+ } catch (err) {
2347
+ const message = err instanceof Error ? err.message : String(err);
2348
+ const isNoAdapter = message.startsWith("No adapter found for ");
2349
+ if (!isNoAdapter)
2350
+ throw err;
2351
+ resolved = await resolveGenericOrReject(cliId, command, {
2352
+ genericEnabled: isGenericFallbackEnabled()
2353
+ });
2354
+ printGenericWarning(cliId);
2355
+ }
2325
2356
  const approval = args.approval ?? "once";
2326
2357
  try {
2327
2358
  const existingGrantId = await findExistingGrant(resolved, idp);
2328
2359
  if (existingGrantId) {
2329
2360
  consola19.info(`Reusing existing grant: ${existingGrantId}`);
2330
2361
  const token = await fetchGrantToken(idp, existingGrantId);
2331
- await verifyAndExecute(token, resolved);
2362
+ await verifyAndExecute(token, resolved, existingGrantId);
2332
2363
  return;
2333
2364
  }
2334
2365
  } catch {
@@ -2355,7 +2386,7 @@ async function runAdapterMode(command, rawArgs, args) {
2355
2386
  if (status !== "approved")
2356
2387
  throw new Error(`Grant ${status}`);
2357
2388
  const token = await fetchGrantToken(idp, grant.id);
2358
- await verifyAndExecute(token, resolved);
2389
+ await verifyAndExecute(token, resolved, grant.id);
2359
2390
  return;
2360
2391
  }
2361
2392
  printPendingGrantInfo(grant, idp);
@@ -2692,7 +2723,7 @@ var mcpCommand = defineCommand26({
2692
2723
  if (transport !== "stdio" && transport !== "sse") {
2693
2724
  throw new Error('Transport must be "stdio" or "sse"');
2694
2725
  }
2695
- const { startMcpServer } = await import("./server-TGIP55VI.js");
2726
+ const { startMcpServer } = await import("./server-UXLNYVMG.js");
2696
2727
  await startMcpServer(transport, port);
2697
2728
  }
2698
2729
  });
@@ -3184,7 +3215,7 @@ async function bestEffortGrantCount(idp) {
3184
3215
  }
3185
3216
  }
3186
3217
  async function runHealth(args) {
3187
- const version = true ? "0.11.1" : "0.0.0";
3218
+ const version = true ? "0.12.0" : "0.0.0";
3188
3219
  const auth = loadAuth();
3189
3220
  if (!auth) {
3190
3221
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -3386,10 +3417,10 @@ if (shellRewrite) {
3386
3417
  if (shellRewrite.action === "rewrite") {
3387
3418
  process.argv = shellRewrite.argv;
3388
3419
  } else if (shellRewrite.action === "version") {
3389
- console.log(`ape-shell ${"0.11.1"} (OpenApe DDISA shell wrapper)`);
3420
+ console.log(`ape-shell ${"0.12.0"} (OpenApe DDISA shell wrapper)`);
3390
3421
  process.exit(0);
3391
3422
  } else if (shellRewrite.action === "help") {
3392
- console.log(`ape-shell ${"0.11.1"} \u2014 OpenApe DDISA shell wrapper`);
3423
+ console.log(`ape-shell ${"0.12.0"} \u2014 OpenApe DDISA shell wrapper`);
3393
3424
  console.log("");
3394
3425
  console.log("Usage:");
3395
3426
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -3404,7 +3435,7 @@ if (shellRewrite) {
3404
3435
  console.log(" --help, -h Show this help message");
3405
3436
  process.exit(0);
3406
3437
  } else if (shellRewrite.action === "interactive") {
3407
- const { runInteractiveShell } = await import("./orchestrator-OF2YGFMR.js");
3438
+ const { runInteractiveShell } = await import("./orchestrator-EHFFTEL5.js");
3408
3439
  await runInteractiveShell();
3409
3440
  process.exit(0);
3410
3441
  } else {
@@ -3447,7 +3478,7 @@ var configCommand = defineCommand33({
3447
3478
  var main = defineCommand33({
3448
3479
  meta: {
3449
3480
  name: "apes",
3450
- version: "0.11.1",
3481
+ version: "0.12.0",
3451
3482
  description: "Unified CLI for OpenApe"
3452
3483
  },
3453
3484
  subCommands: {