@letrquan/book 0.3.0 → 0.4.0-beta.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.
- package/CHANGELOG.md +1024 -0
- package/README.md +6 -3
- package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
- package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
- package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
- package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
- package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
- package/dist/index.js +657 -754
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
- package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
- package/dist/job-runner.js +10 -3
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.js +13 -4
- package/dist/job-supervisor.js.map +1 -1
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
- package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
- package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
- package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
- package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
- package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +876 -64
- package/dist/sdk.js +57 -37
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +5 -5
- package/dist/app-GE4OHJOC.js.map +0 -1
- package/dist/chunk-224FJZLH.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-5YHTKWJN.js.map +0 -1
- package/dist/chunk-62QL5QUP.js.map +0 -1
- package/dist/chunk-67UZEWWF.js.map +0 -1
- package/dist/chunk-6OQQOMZR.js +0 -785
- package/dist/chunk-6OQQOMZR.js.map +0 -1
- package/dist/chunk-AQEQX4FF.js.map +0 -1
- package/dist/chunk-C7ADUNPR.js +0 -273
- package/dist/chunk-C7ADUNPR.js.map +0 -1
- package/dist/chunk-EFJ2NH6E.js.map +0 -1
- package/dist/chunk-IME7R2UI.js.map +0 -1
- package/dist/chunk-J62VZYOB.js +0 -1339
- package/dist/chunk-J62VZYOB.js.map +0 -1
- package/dist/chunk-JTPOI3KU.js.map +0 -1
- package/dist/chunk-QYBD22H5.js.map +0 -1
- package/dist/chunk-RFJ6Q6LE.js.map +0 -1
- package/dist/chunk-S4JMID4G.js +0 -259
- package/dist/chunk-S4JMID4G.js.map +0 -1
- package/dist/chunk-TLX3H2CL.js.map +0 -1
- package/dist/chunk-VPNLCMVR.js.map +0 -1
- package/dist/chunk-W3XFHEF4.js.map +0 -1
- package/dist/chunk-X4NJT436.js.map +0 -1
- package/dist/chunk-X66I2NQO.js +0 -137
- package/dist/chunk-X66I2NQO.js.map +0 -1
- package/dist/chunk-Y2VHN7TC.js.map +0 -1
- package/dist/chunk-YC6W4EJH.js.map +0 -1
- package/dist/context-CBOBDY5Q.js +0 -38
- package/dist/loader-S77OVTHH.js +0 -18
- package/dist/memory-store-DZUT5HUH.js +0 -75
- package/dist/scrollback-35ME2GFX.js.map +0 -1
- package/dist/settings-loader-JUNPGAAM.js +0 -35
- package/dist/shell-selection-CDHGJ4GC.js +0 -17
- /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
- /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
- /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
- /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
- /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
- /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
- /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
- /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/jobs/process-tree.ts","../src/system32.ts","../src/tools/git.ts"],"sourcesContent":["/**\n * Process-liveness helpers shared by the shell manager and the detached job runner.\n *\n * A POSIX process group outlives its leader. Background commands run through `sh -c`, and the\n * shell forks the real worker instead of exec'ing it whenever it cannot hand its own process\n * over, so the direct child can exit — for instance by taking the SIGTERM a worker ignores —\n * while that worker keeps running in the same group. Termination therefore has to be judged on\n * group membership; the direct child's own exit proves nothing about the tree behind it.\n */\n\nimport { execFile, type ChildProcess } from 'node:child_process';\nimport { systemClock, type Clock } from '../clock.js';\nimport { buildChildEnv } from '../child-env.js';\nimport { system32Executable } from '../system32.js';\n\nconst GROUP_POLL_INTERVAL_MS = 25;\n/** How long a process group is given between SIGTERM and SIGKILL, and how long taskkill may take. */\nexport const TERMINATE_GRACE_MS = 1_500;\n\n/** A signal target exists when it accepts signal 0, or rejects it as another user's. */\nfunction signalTargetExists(target: number): boolean {\n try {\n process.kill(target, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nexport function isProcessAlive(pid: number | undefined): boolean {\n return pid === undefined ? false : signalTargetExists(pid);\n}\n\n/** True while any process remains in the group, whether or not the leader is one of them. */\nexport function isProcessGroupAlive(pgid: number | undefined): boolean {\n if (pgid === undefined || process.platform === 'win32') return false;\n return signalTargetExists(-pgid);\n}\n\n/** Signal every process in the group, falling back to the direct child when it has no group. */\nexport function signalProcessGroup(\n proc: { kill(signal: NodeJS.Signals): boolean } | undefined,\n pgid: number,\n signal: NodeJS.Signals,\n): void {\n try {\n process.kill(-pgid, signal);\n } catch {\n try {\n proc?.kill(signal);\n } catch {\n // The tree may have exited between the group signal and the direct-child fallback.\n }\n }\n}\n\n/**\n * Resolve `true` once the group holds no processes, or `false` when the bound elapses first.\n *\n * `kill(-pgid, 0)` also succeeds for a member that has exited but has not been reaped yet, so a\n * group reads as alive until its reaper runs. That bound is deliberate: a zombie holds no ports,\n * file handles, or CPU, and reporting \"not stopped\" for the moments before it is reaped is\n * honest, where trusting the direct child's exit is not.\n */\nexport async function waitForProcessGroupExit(\n pgid: number,\n timeoutMs: number,\n // A kill bound is a duration: on the wall clock, a backwards step here waits\n // past the bound and a forwards one abandons a group that was about to exit.\n clock: Clock = systemClock,\n): Promise<boolean> {\n const deadline = clock.monotonicNowMs() + timeoutMs;\n while (isProcessGroupAlive(pgid)) {\n if (clock.monotonicNowMs() >= deadline) return false;\n await new Promise((resolve) => setTimeout(resolve, GROUP_POLL_INTERVAL_MS));\n }\n return true;\n}\n\nexport function waitForProcessClose(proc: ChildProcess, timeoutMs: number): Promise<boolean> {\n if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve(true);\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n resolve(proc.exitCode !== null || proc.signalCode !== null);\n }, timeoutMs);\n const onClose = () => {\n cleanup();\n resolve(true);\n };\n const cleanup = () => {\n clearTimeout(timer);\n proc.off('close', onClose);\n };\n proc.once('close', onClose);\n });\n}\n\ntype WindowsTreeKill = (pid: number) => Promise<boolean>;\n\n/**\n * `taskkill /T /F` on the tree rooted at `pid`, resolved through `system32Executable` so a\n * workspace on the PATH cannot stand in for it. Reports whether the kill actually ran. The bound is\n * a parameter because a caller that exits straight afterwards has nobody to escalate for it and can\n * afford to wait far longer than one that does.\n */\nexport async function runTaskkill(\n pid: number,\n timeoutMs: number = TERMINATE_GRACE_MS,\n): Promise<boolean> {\n return new Promise((resolve) => {\n execFile(\n system32Executable('taskkill'),\n ['/PID', String(pid), '/T', '/F'],\n // Book's own helper, but it is a command like any other and is handed no more of Book's\n // environment than the next one would be.\n { windowsHide: true, timeout: timeoutMs, env: buildChildEnv() },\n (error) => resolve(!error),\n );\n });\n}\n\nexport async function terminateWindowsProcessTree(\n proc: ChildProcess,\n pid: number,\n signal: NodeJS.Signals,\n treeKill: WindowsTreeKill = runTaskkill,\n): Promise<boolean> {\n const alreadyExited = proc.exitCode !== null || proc.signalCode !== null;\n if (alreadyExited) return true;\n if (await treeKill(pid)) return true;\n try {\n proc.kill(signal);\n } catch {\n // The process may have exited between taskkill and the direct-child fallback.\n }\n return false;\n}\n\n/**\n * Escalate SIGTERM → SIGKILL across the whole tree and report whether it is really gone.\n *\n * On POSIX the direct child is the `sh -c` wrapper, which dies from SIGTERM even when the worker\n * it forked ignores it, so its exit says nothing about the tree — success is judged on whether\n * the process group still holds anything. On Windows `taskkill /T /F` covers the tree, so the\n * direct child's close speaks for the tree only when taskkill actually ran.\n */\nexport async function terminateProcessTree(\n proc: ChildProcess | undefined,\n pid: number | undefined,\n hasClosed: (timeoutMs: number) => Promise<boolean>,\n): Promise<boolean> {\n if (!proc || pid === undefined) return true;\n if (process.platform !== 'win32') {\n signalProcessGroup(proc, pid, 'SIGTERM');\n if (await waitForProcessGroupExit(pid, TERMINATE_GRACE_MS)) return true;\n signalProcessGroup(proc, pid, 'SIGKILL');\n return waitForProcessGroupExit(pid, TERMINATE_GRACE_MS);\n }\n const confirmed = await terminateWindowsProcessTree(proc, pid, 'SIGTERM');\n if (await hasClosed(TERMINATE_GRACE_MS)) return confirmed;\n try {\n proc.kill('SIGKILL');\n } catch {\n return false;\n }\n return (await hasClosed(TERMINATE_GRACE_MS)) && confirmed;\n}\n\nexport async function terminateForegroundProcess(proc: ChildProcess): Promise<void> {\n await terminateProcessTree(proc, proc.pid, (timeoutMs) => waitForProcessClose(proc, timeoutMs));\n}\n","import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Resolve an executable from %SystemRoot%\\System32 on Windows.\n *\n * When System32 is missing from PATH (e.g. Git Bash, MSYS, or sanitized subprocess environments),\n * bare executable names fail with ENOENT under execFile / spawn with shell: false.\n * Off Windows, or if the executable is not found in System32, falls back to the bare name.\n */\nexport function system32Executable(\n name: string,\n platform: NodeJS.Platform = process.platform,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n if (platform !== 'win32') return name;\n const fileName = name.toLowerCase().endsWith('.exe') ? name : `${name}.exe`;\n const systemRoot = env.SystemRoot || env.windir || 'C:\\\\Windows';\n const targetPath = join(systemRoot, 'System32', fileName);\n return existsSync(targetPath) ? targetPath : name;\n}\n","import { execFile } from 'child_process';\nimport type { ToolDefinition, ToolContext, ToolResult } from '../types/tools.js';\nimport { buildChildEnv } from '../child-env.js';\nimport { toolFailure, toolSuccess } from './result.js';\n\ntype ExecFile = typeof execFile;\n\n/**\n * `-c` overrides the Git tools and the managed-agent subsystem carry, so a checkout's own\n * configuration cannot turn a git call into something that runs a program.\n *\n * The configuration belongs to the checkout, and a checkout may have come from an archive, a\n * shared directory, or someone else's machine, so any of these can be set without the operator\n * having made that choice themselves:\n *\n * - `core.fsmonitor` — a command `git status` executes to check whether the tree changed. This\n * is the sharpest one: a status call is supposed to be inert and it will happily run whatever\n * the repository named.\n * - `log.showSignature` — `git log` then verifies every signature it prints, and verification\n * runs `gpg.program`, another program the repository names. `GitDiff` and `GitStatus` do not\n * reach it, but `GitLog` does on every commit that carries a `gpgsig` header.\n * - `core.pager` — a command Git runs to page its output. `execFile` never allocates a TTY, so\n * git normally skips it, but the setting is read before that decision.\n * - `core.hooksPath` — where hooks live, and where most of the effect of this list lands: a\n * `post-checkout` on `worktree add`, a `pre-commit` on an agent's commit, a\n * `reference-transaction` on every ref Book moves, a `prepare-commit-msg` on the\n * `cherry-pick` that applies a candidate. The four read-only tools here run no hook at all, so\n * for them this is belt and braces against a future subcommand; for\n * `src/agents/git-isolation.ts`, whose calls are writes nothing asks about (#348), switching\n * them off is the decision. It is **not** applied to `GitCommit` or to `runGit` generally: on\n * the operator's own commit it would silently disable a hook they installed, and their code\n * silently not running is a worse failure than the one this list prevents.\n * - `core.untrackedCache`, `gc.auto`, `maintenance.auto` — background writers. `gc.auto=0` is not\n * the whole of it: `maintenance.auto` is separately on by default, so every commit spawns\n * `git maintenance run --auto`, which repacks and can run whatever strategies the configuration\n * registers while Book is moving refs. `--no-optional-locks` below covers the index lock these\n * need.\n *\n * `--no-optional-locks` is a top-level flag, not a config key, so it is added separately: it stops\n * Git taking `.git/index.lock` for a call that only reads, which a read-only worktree and a\n * concurrent `book` session would otherwise fight over.\n *\n * Each of these was checked against a real `git`, not read off this list — see\n * `git.test.ts`. Anything not reached through a non-TTY `execFile` (`column.ui`, `pager.log`,\n * `diff.external`) is not here either, because the flags that close those are per-command and\n * belong in {@link HARDENED_DIFF_ARGS}, which every hardened caller that runs `git diff` uses.\n */\nconst GIT_HARDENING_ARGS: readonly string[] = [\n '-c',\n 'core.fsmonitor=false',\n '-c',\n 'core.pager=cat',\n '-c',\n 'core.hooksPath=',\n '-c',\n 'core.untrackedCache=false',\n '-c',\n 'gc.auto=0',\n '-c',\n 'maintenance.auto=false',\n '-c',\n 'log.showSignature=false',\n '--no-optional-locks',\n];\n\n/**\n * The per-command flags that close the two remaining routes a checkout owns for producing a diff,\n * for every hardened caller that runs `git diff`: `diff.external` names a program to produce it,\n * and a `.gitattributes` `textconv` line names one per file. They are command flags rather than\n * configuration, which is why they are not in {@link GIT_HARDENING_ARGS}, and they are on the\n * command as well as in the config so the guarantee does not rest on the `-c` overrides being\n * consulted for the subcommand.\n */\nexport const HARDENED_DIFF_ARGS = ['--no-ext-diff', '--no-textconv'] as const;\n\n/**\n * The environment every hardened call carries: a pager, so a checkout cannot name a program git\n * pages its output with, and a credential prompt it cannot sit waiting on. Callers that have an\n * environment of their own to merge (a tool's `ToolContext.env`, a temporary `GIT_INDEX_FILE`,\n * git isolation's commit identity) spread this into it; the pager and the prompt are the floor,\n * not a replacement.\n *\n * The return type is `Record<string, string>` rather than `NodeJS.ProcessEnv`: a caller's\n * environment is `Record<string, string>` too, and spreading a wider one into it would make the\n * whole object `string | undefined` where nothing may be undefined.\n */\nexport function hardenedGitEnv(): Record<string, string> {\n return { GIT_PAGER: 'cat', GIT_TERMINAL_PROMPT: '0' };\n}\n\n/**\n * The hardening flags in front of a caller's own arguments; see {@link GIT_HARDENING_ARGS}.\n *\n * Four callers, and they read this differently. The read-only Git tools ({@link readOnlyGit}) take\n * it because nothing asks before they run, so a checkout must not be able to start a program\n * through one. `/review`'s target (`src/review/target.ts`) and the TUI status poll\n * (`src/tui/hooks/useGitStatus.ts`) are the same: reports and background polls that fire with no\n * prompt. Git isolation (`src/agents/git-isolation.ts`) is the one that takes it on writes as\n * well — the agent's commit, the cherry-pick, `worktree add`, `update-ref` — where hooks being\n * off is a decision (#348) rather than a consequence: that work is Book's, in Book's worktree,\n * and a hook the operator installed is theirs to decide when it runs. The operator's own commits\n * keep their hooks, through {@link runGit} below.\n */\nexport function hardenedGitArgs(args: readonly string[]): string[] {\n return [...GIT_HARDENING_ARGS, ...args];\n}\n\n/**\n * `runGit` runs a caller's arguments as given, and the caller chooses.\n *\n * The hardening is a property of the calls that are *not* the operator's own work, not of every\n * git call this module makes. `GitCommit` is the operator's own work, and it is the only\n * mutating tool here: it comes through this function with its own argv and the environment the\n * user configured, exactly as it did before the hardening existed, so a `pre-commit` or\n * `commit-msg` hook still runs. Silently not running the user's hook would be a worse failure\n * than the one the hardening exists to prevent. The read-only tools go through\n * {@link readOnlyGit}, and a caller whose work is Book's own builds its argv with\n * {@link hardenedGitArgs} directly.\n */\nexport async function runGit(\n args: string[],\n ctx: ToolContext,\n execute: ExecFile = execFile,\n): Promise<{ success: boolean; output: string; error?: string }> {\n return new Promise((resolve) => {\n const options = {\n cwd: ctx.workspaceRoot,\n encoding: 'utf-8' as const,\n timeout: 30_000,\n signal: ctx.signal,\n // A hook git runs is a project command like any other, so it must not inherit a NODE_ENV\n // Book defaulted for its own renderer.\n env: buildChildEnv(process.env, ctx.env),\n };\n\n execute('git', args, options, (error, stdout, stderr) => {\n if (!error) {\n resolve({ success: true, output: stdout || '(no output)' });\n return;\n }\n\n if (ctx.signal?.aborted) {\n resolve({ success: false, output: '', error: 'CANCELLED: Git command was cancelled' });\n return;\n }\n\n resolve({\n success: false,\n output: '',\n error: stderr || error.message || 'Git command failed',\n });\n });\n });\n}\n\n/** A read-only report: hardened, and with the pager and credential prompt it cannot wait on. */\nfunction readOnlyGit(args: readonly string[], ctx: ToolContext) {\n return runGit(hardenedGitArgs(args), {\n ...ctx,\n env: { ...ctx.env, ...hardenedGitEnv() },\n });\n}\n\nfunction gitResult(result: Awaited<ReturnType<typeof runGit>>): ToolResult {\n return result.success\n ? toolSuccess(result.output)\n : toolFailure(result.error ?? 'Git command failed', { content: result.output });\n}\n\nasync function gitStatus(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitStatus, ctx);\n return gitResult(result);\n}\n\nasync function gitDiff(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitDiff, ctx);\n return gitResult(result);\n}\n\nasync function gitLog(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitLog, ctx);\n return gitResult(result);\n}\n\nasync function gitCommit(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const message = args.message as string;\n const result = await runGit(['commit', '-m', message], ctx);\n return gitResult(result);\n}\n\nasync function gitBranch(_args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {\n const result = await readOnlyGit(READ_ONLY_GIT_ARGS.GitBranch, ctx);\n return gitResult(result);\n}\n\n/**\n * The arguments the four read-only tools run.\n *\n * These are the tools `permissions.ts` lets through without a prompt, so their whole security\n * argument rests on two things: they read the workspace and change nothing, and the repository's\n * own configuration cannot make them execute a program (see {@link GIT_HARDENING_ARGS}). Exported\n * so that argument can be asserted per tool rather than taken on trust.\n *\n * `GitDiff` carries {@link HARDENED_DIFF_ARGS}, which close the two remaining routes a checkout\n * owns: `diff.external` names a program to produce the diff, and a `.gitattributes` `textconv`\n * line names one per file. `GitLog` carries `--no-show-signature` for the same reason\n * `log.showSignature=false` is in the hardening: verifying a signature runs `gpg.program`. The flag\n * is on the command as well as in the config so the guarantee does not rest on the `-c` override\n * being consulted for this subcommand. `GitCommit` is deliberately not here — it is a mutation,\n * still asks, and runs with the user's own hooks (see {@link runGit}).\n */\nexport const READ_ONLY_GIT_ARGS = {\n GitStatus: ['status', '--short'],\n GitDiff: ['diff', ...HARDENED_DIFF_ARGS],\n GitLog: ['log', '--oneline', '-20', '--no-show-signature'],\n GitBranch: ['branch', '-a'],\n} as const satisfies Readonly<Record<string, string[]>>;\n\nexport const gitTools: ToolDefinition[] = [\n {\n name: 'GitStatus',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show the working tree status (git status --short)',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitStatus,\n },\n {\n name: 'GitDiff',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show changes between commits, commit and working tree, etc.',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitDiff,\n },\n {\n name: 'GitLog',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'Show recent commit logs (last 20, oneline)',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitLog,\n },\n {\n name: 'GitCommit',\n description: 'Create a new commit with a message',\n parameters: {\n type: 'object',\n properties: { message: { type: 'string', description: 'Commit message' } },\n required: ['message'],\n },\n execute: gitCommit,\n },\n {\n name: 'GitBranch',\n idempotent: true,\n policy: { concurrency: 'parallel' },\n description: 'List branches',\n parameters: { type: 'object', properties: {}, required: [] },\n execute: gitBranch,\n },\n];\n"],"mappings":"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","names":["execFile","execFile"]}
|
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|