@letrquan/book 0.2.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hook-approvals.ts"],"sourcesContent":["/**\r\n * Trust decisions for hook entries declared by a repository.\r\n *\r\n * A hook entry is a shell command the repository can get Book to execute at any\r\n * lifecycle event — on every prompt, around every tool call, at session start.\r\n * Once merged into resolved settings an entry carries no provenance, so nothing\r\n * downstream can tell a repository's hook from the user's own.\r\n *\r\n * Project-declared hooks therefore require a one-time per-workspace decision,\r\n * keyed by a fingerprint of the full entry (event, matcher, command, env) and\r\n * recorded in the user-global trust store, outside the workspace, where nothing\r\n * the repository ships can reach it (see `workspace-trust.ts`). Any change to\r\n * an entry produces a new fingerprint and reverts it to untrusted.\r\n *\r\n * User-global and local-layer hooks need no gate: those layers were written by\r\n * or for the user.\r\n */\r\nimport { createHash } from 'crypto';\r\nimport { HOOK_EVENTS, type HookEntry, type HookEvent, type ProjectHookChoice } from './settings.js';\r\nimport { updateWorkspaceTrust } from './workspace-trust.js';\r\n\r\n/** Every project-declared hook entry, paired with the event that triggers it. */\r\nexport interface DeclaredHook {\r\n event: HookEvent;\r\n entry: HookEntry;\r\n}\r\n\r\n/** Stable digest of everything the user is asked to trust about a hook. */\r\nexport function hookFingerprint(event: HookEvent, entry: HookEntry): string {\r\n const sortedEnv = Object.fromEntries(\r\n Object.entries(entry.env ?? {}).sort(([a], [b]) => a.localeCompare(b)),\r\n );\r\n const canonical = JSON.stringify({\r\n event,\r\n matcher: entry.matcher ?? null,\r\n command: entry.command,\r\n env: sortedEnv,\r\n });\r\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\r\n}\r\n\r\n/** Flatten a settings document into its declared hook entries, event paired. */\r\nexport function collectDeclaredHooks(\r\n settings:\r\n | {\r\n hooks?: Partial<Record<HookEvent, HookEntry[]>> & { projectEntries?: unknown };\r\n }\r\n | null\r\n | undefined,\r\n): DeclaredHook[] {\r\n const declared: DeclaredHook[] = [];\r\n for (const event of HOOK_EVENTS) {\r\n for (const entry of settings?.hooks?.[event] ?? []) {\r\n declared.push({ event, entry });\r\n }\r\n }\r\n return declared;\r\n}\r\n\r\nexport type ProjectHookState = ProjectHookChoice | 'unknown';\r\n\r\nexport function evaluateProjectHook(\r\n store: Record<string, ProjectHookChoice> | undefined,\r\n fingerprint: string,\r\n): ProjectHookState {\r\n return store?.[fingerprint] ?? 'unknown';\r\n}\r\n\r\nexport interface ProjectHookPartition {\r\n /** Approved by the user: these join the resolved hooks. */\r\n approved: DeclaredHook[];\r\n /** No decision recorded: withheld until the user makes one. */\r\n pending: DeclaredHook[];\r\n /** Explicitly refused: withheld, and not re-offered. */\r\n rejected: DeclaredHook[];\r\n}\r\n\r\nexport function partitionProjectHooks(\r\n hooks: readonly DeclaredHook[],\r\n store: Record<string, ProjectHookChoice> | undefined,\r\n): ProjectHookPartition {\r\n const partition: ProjectHookPartition = { approved: [], pending: [], rejected: [] };\r\n for (const hook of hooks) {\r\n const state = evaluateProjectHook(store, hookFingerprint(hook.event, hook.entry));\r\n if (state === 'approved') partition.approved.push(hook);\r\n else if (state === 'rejected') partition.rejected.push(hook);\r\n else partition.pending.push(hook);\r\n }\r\n return partition;\r\n}\r\n\r\n/**\r\n * Render repository-authored text for a terminal that is about to ask the user\r\n * to trust it.\r\n *\r\n * A hook command and its environment values are written by the party being\r\n * gated. Printed raw, a value carrying newlines or ANSI escapes can forge\r\n * further lines of the report — including an `[x] …` line for a hook that is\r\n * nothing of the kind. Control characters become visible escapes, and an\r\n * over-long value is truncated so one entry cannot push the rest of the\r\n * disclosure off screen.\r\n */\r\nexport function renderUntrustedText(value: string, maxLength = 200): string {\r\n const escaped = value.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, (character) => {\r\n if (character === '\\n') return '\\\\n';\r\n if (character === '\\r') return '\\\\r';\r\n if (character === '\\t') return '\\\\t';\r\n return `\\\\x${character.codePointAt(0)!.toString(16).padStart(2, '0')}`;\r\n });\r\n return escaped.length > maxLength ? `${escaped.slice(0, maxLength)}…` : escaped;\r\n}\r\n\r\n/**\r\n * Everything the fingerprint covers, rendered for a trust decision.\r\n *\r\n * The fingerprint digests event, matcher, command *and* env, so a disclosure\r\n * showing only the command understates what approval grants: `npm test` with\r\n * `NODE_OPTIONS=--require ./payload.js` reads as harmless and is not. Two\r\n * entries differing only in matcher or env are also indistinguishable without\r\n * these lines.\r\n */\r\nexport function describeDeclaredHook(hook: DeclaredHook): { headline: string; details: string[] } {\r\n const details = [`fingerprint: ${hookFingerprint(hook.event, hook.entry)}`];\r\n if (hook.entry.matcher !== undefined) {\r\n details.push(`matcher: ${renderUntrustedText(hook.entry.matcher)}`);\r\n }\r\n for (const [key, value] of Object.entries(hook.entry.env ?? {}).sort(([a], [b]) =>\r\n a.localeCompare(b),\r\n )) {\r\n details.push(`env: ${renderUntrustedText(key, 64)}=${renderUntrustedText(value)}`);\r\n }\r\n return { headline: `${hook.event}: ${renderUntrustedText(hook.entry.command)}`, details };\r\n}\r\n\r\n/**\r\n * Record an approve/reject decision for one hook, leaving every other recorded\r\n * decision — in this workspace and in every other — untouched.\r\n */\r\nexport function persistProjectHookChoice(\r\n workspace: string,\r\n fingerprint: string,\r\n choice: ProjectHookChoice,\r\n options: { trustStorePath?: string } = {},\r\n): { ok: boolean; error?: string } {\r\n return updateWorkspaceTrust(\r\n workspace,\r\n (trust) => {\r\n trust.hookEntries[fingerprint] = choice;\r\n },\r\n options.trustStorePath,\r\n );\r\n}\r\n"],"mappings":";;;;;;;;AAiBA,SAAS,kBAAkB;AAWpB,SAAS,gBAAgB,OAAkB,OAA0B;AAC1E,QAAM,YAAY,OAAO;AAAA,IACvB,OAAO,QAAQ,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,EACvE;AACA,QAAM,YAAY,KAAK,UAAU;AAAA,IAC/B;AAAA,IACA,SAAS,MAAM,WAAW;AAAA,IAC1B,SAAS,MAAM;AAAA,IACf,KAAK;AAAA,EACP,CAAC;AACD,SAAO,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACzE;AAGO,SAAS,qBACd,UAMgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,SAAS,aAAa;AAC/B,eAAW,SAAS,UAAU,QAAQ,KAAK,KAAK,CAAC,GAAG;AAClD,eAAS,KAAK,EAAE,OAAO,MAAM,CAAC;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,oBACd,OACA,aACkB;AAClB,SAAO,QAAQ,WAAW,KAAK;AACjC;AAWO,SAAS,sBACd,OACA,OACsB;AACtB,QAAM,YAAkC,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC,GAAG,UAAU,CAAC,EAAE;AAClF,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,oBAAoB,OAAO,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC;AAChF,QAAI,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,aAC7C,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,QACtD,WAAU,QAAQ,KAAK,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAaO,SAAS,oBAAoB,OAAe,YAAY,KAAa;AAC1E,QAAM,UAAU,MAAM,QAAQ,iCAAiC,CAAC,cAAc;AAC5E,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,IAAM,QAAO;AAC/B,WAAO,MAAM,UAAU,YAAY,CAAC,EAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EACtE,CAAC;AACD,SAAO,QAAQ,SAAS,YAAY,GAAG,QAAQ,MAAM,GAAG,SAAS,CAAC,WAAM;AAC1E;AAWO,SAAS,qBAAqB,MAA6D;AAChG,QAAM,UAAU,CAAC,gBAAgB,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC,EAAE;AAC1E,MAAI,KAAK,MAAM,YAAY,QAAW;AACpC,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,MAAM,OAAO,CAAC,EAAE;AAAA,EACxE;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,MAAM,OAAO,CAAC,CAAC,EAAE;AAAA,IAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAC3E,EAAE,cAAc,CAAC;AAAA,EACnB,GAAG;AACD,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,EAAE,CAAC,IAAI,oBAAoB,KAAK,CAAC,EAAE;AAAA,EAC3F;AACA,SAAO,EAAE,UAAU,GAAG,KAAK,KAAK,KAAK,oBAAoB,KAAK,MAAM,OAAO,CAAC,IAAI,QAAQ;AAC1F;AAMO,SAAS,yBACd,WACA,aACA,QACA,UAAuC,CAAC,GACP;AACjC,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU;AACT,YAAM,YAAY,WAAW,IAAI;AAAA,IACnC;AAAA,IACA,QAAQ;AAAA,EACV;AACF;","names":[]}
@@ -1,264 +0,0 @@
1
- import {
2
- partitionProjectAllowRules
3
- } from "./chunk-WKBO5R4O.js";
4
- import {
5
- collectDeclaredHooks,
6
- partitionProjectHooks
7
- } from "./chunk-ANSJPYGI.js";
8
- import {
9
- defaultTrustStorePath,
10
- loadWorkspaceTrust
11
- } from "./chunk-DGMQEI6Y.js";
12
- import {
13
- normalizeRemovedSettings
14
- } from "./chunk-5RDRAO4B.js";
15
- import {
16
- DEFAULT_SETTINGS,
17
- HOOK_EVENTS,
18
- SettingsRepository,
19
- bookSettingsSchema,
20
- writeFileAtomic
21
- } from "./chunk-574C6MQR.js";
22
- import {
23
- resolveBookHome
24
- } from "./chunk-24AXE6SP.js";
25
-
26
- // src/settings-loader.ts
27
- import { readFileSync, existsSync } from "fs";
28
- import { join, normalize } from "path";
29
- var LEGACY_PERMISSIONS_MIGRATION_VERSION = 1;
30
- var CONCATENATED_ARRAY_PATHS = /* @__PURE__ */ new Set([
31
- "permissions.allow",
32
- "permissions.ask",
33
- "permissions.deny",
34
- ...[
35
- "SessionStart",
36
- "SessionEnd",
37
- "UserPromptSubmit",
38
- "PreToolUse",
39
- "PostToolUse",
40
- "Stop",
41
- "PreCompact",
42
- "PostCompact",
43
- "SubagentStart",
44
- "SubagentStop"
45
- ].map((event) => `hooks.${event}`)
46
- ]);
47
- function mergeObject(base, override, prefix = "") {
48
- const result = structuredClone(base);
49
- for (const [key, value] of Object.entries(override)) {
50
- if (value === void 0) continue;
51
- const path = prefix ? `${prefix}.${key}` : key;
52
- const existing = result[key];
53
- if (Array.isArray(value)) {
54
- if (path === "additionalDirectories") {
55
- const combined = [...Array.isArray(existing) ? existing : [], ...value].map(
56
- (entry) => normalize(String(entry))
57
- );
58
- result[key] = [...new Set(combined)];
59
- } else if (CONCATENATED_ARRAY_PATHS.has(path)) {
60
- result[key] = [...Array.isArray(existing) ? existing : [], ...value];
61
- } else {
62
- result[key] = structuredClone(value);
63
- }
64
- } else if (typeof value === "object" && value !== null && typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
65
- result[key] = mergeObject(
66
- existing,
67
- value,
68
- path
69
- );
70
- } else {
71
- result[key] = value;
72
- }
73
- }
74
- return result;
75
- }
76
- var WORKSPACE_FORBIDDEN_PATHS = [
77
- ["mcp", "projectServers"],
78
- ["permissions", "projectAllowRules"],
79
- ["hooks", "projectEntries"],
80
- ["commands", "projectCommands"]
81
- ];
82
- function stripPaths(settings, paths) {
83
- for (const [parent, key] of paths) {
84
- const container = settings[parent];
85
- if (container && typeof container === "object" && !Array.isArray(container)) {
86
- delete container[key];
87
- }
88
- }
89
- }
90
- function sanitizeLayer(settings, trust) {
91
- if (trust === "trusted") return settings;
92
- const sanitized = structuredClone(settings);
93
- if (sanitized.defaultMode === "bypassPermissions") delete sanitized.defaultMode;
94
- delete sanitized.shell;
95
- stripPaths(sanitized, WORKSPACE_FORBIDDEN_PATHS);
96
- return sanitized;
97
- }
98
- function mergeLayer(resolved, layer, trust) {
99
- const candidate = sanitizeLayer(layer, trust);
100
- if (resolved.disableBypassPermissionsMode === true && candidate.disableBypassPermissionsMode === false) {
101
- candidate.disableBypassPermissionsMode = true;
102
- }
103
- return mergeSettings(resolved, candidate);
104
- }
105
- function mergeSettings(base, override) {
106
- return mergeObject(
107
- base,
108
- override
109
- );
110
- }
111
- function loadSettingsFile(path) {
112
- if (!existsSync(path)) return null;
113
- const raw = readFileSync(path, "utf-8");
114
- let parsed;
115
- try {
116
- parsed = JSON.parse(raw);
117
- } catch (e) {
118
- throw new Error(
119
- `Invalid JSON in settings file: ${path}
120
- ${e instanceof Error ? e.message : String(e)}`
121
- );
122
- }
123
- parsed = normalizeRemovedSettings(parsed);
124
- const result = bookSettingsSchema.safeParse(parsed);
125
- if (!result.success) throw new Error(`Invalid settings in ${path}:
126
- ${result.error.message}`);
127
- return parsed;
128
- }
129
- function settingsLayerPaths(workspace, overridePath, paths = {}) {
130
- const userPath = paths.userSettingsPath ?? (paths.home ? join(paths.home, ".book", "settings.json") : join(resolveBookHome(), "settings.json"));
131
- const layers = [
132
- userPath,
133
- paths.projectSettingsPath ?? join(workspace, ".book", "settings.json"),
134
- paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json")
135
- ];
136
- if (overridePath) layers.push(overridePath);
137
- return layers;
138
- }
139
- function resolveSettings(workspace, overridePath, paths = {}) {
140
- let resolved = structuredClone(DEFAULT_SETTINGS);
141
- const userPath = paths.userSettingsPath ?? (paths.home ? join(paths.home, ".book", "settings.json") : join(resolveBookHome(), "settings.json"));
142
- const user = loadSettingsFile(userPath);
143
- if (user) resolved = mergeLayer(resolved, user, "trusted");
144
- const projectPath = paths.projectSettingsPath ?? join(workspace, ".book", "settings.json");
145
- const project = loadSettingsFile(projectPath);
146
- const declaredProjectAllow = project?.permissions?.allow ?? [];
147
- const declaredProjectHooks = collectDeclaredHooks(project);
148
- const userHookCounts = Object.fromEntries(
149
- HOOK_EVENTS.map((event) => [event, resolved.hooks[event].length])
150
- );
151
- if (project) {
152
- let withheld = project;
153
- if (declaredProjectAllow.length > 0) {
154
- withheld = { ...withheld, permissions: { ...withheld.permissions, allow: [] } };
155
- }
156
- if (declaredProjectHooks.length > 0) {
157
- withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };
158
- }
159
- resolved = mergeLayer(resolved, withheld, "repository");
160
- }
161
- const localPath = paths.localSettingsPath ?? join(workspace, ".book", "settings.local.json");
162
- const local = loadSettingsFile(localPath);
163
- if (local) resolved = mergeLayer(resolved, local, "local");
164
- if (overridePath) {
165
- const override = loadSettingsFile(overridePath);
166
- if (override) resolved = mergeLayer(resolved, override, "trusted");
167
- }
168
- const trust = loadWorkspaceTrust(
169
- workspace,
170
- paths.trustStorePath ?? defaultTrustStorePath(paths.home)
171
- );
172
- resolved.permissions.projectAllowRules = {
173
- ...resolved.permissions.projectAllowRules,
174
- ...trust.permissionAllowRules
175
- };
176
- resolved.mcp.projectServers = { ...resolved.mcp.projectServers, ...trust.mcpServers };
177
- resolved.hooks.projectEntries = { ...resolved.hooks.projectEntries, ...trust.hookEntries };
178
- resolved.commands.projectCommands = {
179
- ...resolved.commands.projectCommands,
180
- ...trust.projectCommands
181
- };
182
- if (declaredProjectAllow.length > 0) {
183
- const { approved } = partitionProjectAllowRules(
184
- declaredProjectAllow,
185
- resolved.permissions?.projectAllowRules
186
- );
187
- if (approved.length > 0) {
188
- resolved.permissions.allow = [...resolved.permissions.allow ?? [], ...approved];
189
- }
190
- }
191
- if (declaredProjectHooks.length > 0) {
192
- const { approved } = partitionProjectHooks(
193
- declaredProjectHooks,
194
- resolved.hooks?.projectEntries
195
- );
196
- const approvedByEvent = /* @__PURE__ */ new Map();
197
- for (const { event, entry } of approved) {
198
- const entries = approvedByEvent.get(event) ?? [];
199
- entries.push(entry);
200
- approvedByEvent.set(event, entries);
201
- }
202
- for (const [event, entries] of approvedByEvent) {
203
- resolved.hooks[event].splice(userHookCounts[event], 0, ...entries);
204
- }
205
- }
206
- const settings = bookSettingsSchema.parse(resolved);
207
- return settings;
208
- }
209
- function migrateLegacyPermissions(workspace, home, validatedSettings) {
210
- if (!validatedSettings) {
211
- resolveSettings(workspace, void 0, home ? { home } : void 0);
212
- }
213
- const legacyPath = join(home ? join(home, ".book") : resolveBookHome(), "permissions.json");
214
- if (!existsSync(legacyPath)) return false;
215
- const markerPath = join(workspace, ".book", "migrations.json");
216
- try {
217
- const marker = JSON.parse(readFileSync(markerPath, "utf-8"));
218
- if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;
219
- } catch {
220
- }
221
- let legacyRules = [];
222
- try {
223
- const raw = readFileSync(legacyPath, "utf-8");
224
- const parsed = JSON.parse(raw);
225
- legacyRules = parsed.rules ?? [];
226
- } catch {
227
- return false;
228
- }
229
- if (legacyRules.length === 0) return false;
230
- const localDir = join(workspace, ".book");
231
- const localPath = join(localDir, "settings.local.json");
232
- const result = new SettingsRepository(localPath).update((existing) => {
233
- const permissions = existing.permissions ?? {};
234
- for (const key of ["allow", "ask", "deny"]) {
235
- if (!Array.isArray(permissions[key])) permissions[key] = [];
236
- }
237
- for (const rule of legacyRules) {
238
- const specifier = rule.pattern ? `${rule.toolName}(${rule.pattern})` : rule.toolName;
239
- const effect = rule.effect;
240
- if (!permissions[effect].includes(specifier)) permissions[effect].push(specifier);
241
- }
242
- existing.permissions = permissions;
243
- });
244
- if (!result.ok) return false;
245
- writeFileAtomic(
246
- markerPath,
247
- `${JSON.stringify({ legacyPermissions: LEGACY_PERMISSIONS_MIGRATION_VERSION }, null, 2)}
248
- `
249
- );
250
- if (!result.changed) return false;
251
- console.warn(
252
- `\u26A0 Migrated ${legacyRules.length} permission rule(s) from ~/.book/permissions.json to ${localPath}. Delete the legacy file after verifying the migration.`
253
- );
254
- return true;
255
- }
256
-
257
- export {
258
- mergeSettings,
259
- loadSettingsFile,
260
- settingsLayerPaths,
261
- resolveSettings,
262
- migrateLegacyPermissions
263
- };
264
- //# sourceMappingURL=chunk-BTZZUCLI.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/settings-loader.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\r\nimport { join, normalize } from 'path';\r\nimport {\r\n bookSettingsSchema,\r\n DEFAULT_SETTINGS,\r\n HOOK_EVENTS,\r\n type BookSettings,\r\n type HookEntry,\r\n type HookEvent,\r\n type ResolvedSettings,\r\n} from './settings.js';\r\nimport { SettingsRepository, writeFileAtomic } from './settings-repository.js';\r\nimport { resolveBookHome } from './book-home.js';\r\nimport { partitionProjectAllowRules } from './permission-approvals.js';\r\nimport { collectDeclaredHooks, partitionProjectHooks } from './hook-approvals.js';\r\nimport { defaultTrustStorePath, loadWorkspaceTrust } from './workspace-trust.js';\r\nimport { normalizeRemovedSettings } from './settings-removed.js';\r\n\r\nconst LEGACY_PERMISSIONS_MIGRATION_VERSION = 1;\r\n\r\n/**\r\n * Deep-merge two settings objects. For arrays, concatenate (used for\r\n * permission rules and additionalDirectories). For objects, merge recursively.\r\n * For scalars, the override wins.\r\n */\r\nconst CONCATENATED_ARRAY_PATHS = new Set([\r\n 'permissions.allow',\r\n 'permissions.ask',\r\n 'permissions.deny',\r\n ...[\r\n 'SessionStart',\r\n 'SessionEnd',\r\n 'UserPromptSubmit',\r\n 'PreToolUse',\r\n 'PostToolUse',\r\n 'Stop',\r\n 'PreCompact',\r\n 'PostCompact',\r\n 'SubagentStart',\r\n 'SubagentStop',\r\n ].map((event) => `hooks.${event}`),\r\n]);\r\n\r\nfunction mergeObject(\r\n base: Record<string, unknown>,\r\n override: Record<string, unknown>,\r\n prefix = '',\r\n): Record<string, unknown> {\r\n const result = structuredClone(base);\r\n\r\n for (const [key, value] of Object.entries(override)) {\r\n if (value === undefined) continue;\r\n const path = prefix ? `${prefix}.${key}` : key;\r\n const existing = result[key];\r\n\r\n if (Array.isArray(value)) {\r\n if (path === 'additionalDirectories') {\r\n const combined = [...(Array.isArray(existing) ? existing : []), ...value].map((entry) =>\r\n normalize(String(entry)),\r\n );\r\n result[key] = [...new Set(combined)];\r\n } else if (CONCATENATED_ARRAY_PATHS.has(path)) {\r\n result[key] = [...(Array.isArray(existing) ? existing : []), ...value];\r\n } else {\r\n result[key] = structuredClone(value);\r\n }\r\n } else if (\r\n typeof value === 'object' &&\r\n value !== null &&\r\n typeof existing === 'object' &&\r\n existing !== null &&\r\n !Array.isArray(existing)\r\n ) {\r\n result[key] = mergeObject(\r\n existing as Record<string, unknown>,\r\n value as Record<string, unknown>,\r\n path,\r\n );\r\n } else {\r\n result[key] = value;\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * How much authority a settings layer carries.\r\n *\r\n * - `trusted` — the user's own global file, or a path they passed on the CLI.\r\n * - `local` — `<workspace>/.book/settings.local.json`: gitignored and normally\r\n * written by the user, or by Book on their behalf.\r\n * - `repository` — `<workspace>/.book/settings.json`: checked in and controlled\r\n * by whoever wrote the repository. It may never grant itself trust.\r\n */\r\nexport type SettingsLayerTrust = 'trusted' | 'local' | 'repository';\r\n\r\n/**\r\n * Settings paths that a workspace file may not supply for itself. Most record\r\n * a decision the user made *about* repository-controlled input; honouring one\r\n * from the workspace would let a clone approve itself. Experimental capability\r\n * flags are included because merely opening a clone must not opt the user into\r\n * unstable runtime behavior.\r\n *\r\n * The local layer is no safer than the checked-in one here. `.gitignore` does\r\n * not stop a *tracked* file from reaching a clone, so a repository that\r\n * force-adds `.book/settings.local.json` ships its own approvals with it. Both\r\n * workspace layers are therefore stripped. Trust decisions come from the\r\n * user-global store; experimental flags come from a trusted user-global or\r\n * explicit settings document (or from a process environment opt-in).\r\n */\r\nconst WORKSPACE_FORBIDDEN_PATHS: ReadonlyArray<readonly [string, string]> = [\r\n ['mcp', 'projectServers'],\r\n ['permissions', 'projectAllowRules'],\r\n ['hooks', 'projectEntries'],\r\n ['commands', 'projectCommands'],\r\n];\r\n\r\nfunction stripPaths(\r\n settings: Partial<BookSettings>,\r\n paths: ReadonlyArray<readonly [string, string]>,\r\n): void {\r\n for (const [parent, key] of paths) {\r\n const container = (settings as Record<string, unknown>)[parent];\r\n if (container && typeof container === 'object' && !Array.isArray(container)) {\r\n delete (container as Record<string, unknown>)[key];\r\n }\r\n }\r\n}\r\n\r\nfunction sanitizeLayer(\r\n settings: Partial<BookSettings>,\r\n trust: SettingsLayerTrust,\r\n): Partial<BookSettings> {\r\n if (trust === 'trusted') return settings;\r\n const sanitized = structuredClone(settings);\r\n // Project/local settings cannot opt a session into the most permissive mode.\r\n if (sanitized.defaultMode === 'bypassPermissions') delete sanitized.defaultMode;\r\n // `shell` names the program every Bash command is handed to. A clone that\r\n // could point it at a binary it ships would run that binary on the first\r\n // command, so the key is honoured from trusted layers only.\r\n delete sanitized.shell;\r\n stripPaths(sanitized, WORKSPACE_FORBIDDEN_PATHS);\r\n return sanitized;\r\n}\r\n\r\nfunction mergeLayer(\r\n resolved: ResolvedSettings,\r\n layer: Partial<BookSettings>,\r\n trust: SettingsLayerTrust,\r\n): ResolvedSettings {\r\n const candidate = sanitizeLayer(layer, trust);\r\n // A trusted global safety ceiling cannot be disabled by a lower-trust layer.\r\n if (\r\n resolved.disableBypassPermissionsMode === true &&\r\n candidate.disableBypassPermissionsMode === false\r\n ) {\r\n candidate.disableBypassPermissionsMode = true;\r\n }\r\n return mergeSettings(resolved, candidate);\r\n}\r\n\r\nexport function mergeSettings(\r\n base: ResolvedSettings,\r\n override: Partial<BookSettings>,\r\n): ResolvedSettings {\r\n return mergeObject(\r\n base as unknown as Record<string, unknown>,\r\n override as Record<string, unknown>,\r\n ) as unknown as ResolvedSettings;\r\n}\r\n\r\n/**\r\n * Load and validate a single settings.json file. Returns null if the file\r\n * doesn't exist. Throws on parse/validation errors.\r\n */\r\nfunction loadSettingsFile(path: string): BookSettings | null {\r\n if (!existsSync(path)) return null;\r\n const raw = readFileSync(path, 'utf-8');\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch (e) {\r\n throw new Error(\r\n `Invalid JSON in settings file: ${path}\\n${e instanceof Error ? e.message : String(e)}`,\r\n );\r\n }\r\n // A setting that was removed must not brick a working install: an obsolete\r\n // value of a surviving key would otherwise fail the whole document and stop\r\n // Book from starting, with a schema dump that names no remedy. Removed keys\r\n // are reported by `book doctor`, which reads the file itself.\r\n parsed = normalizeRemovedSettings(parsed);\r\n const result = bookSettingsSchema.safeParse(parsed);\r\n if (!result.success) throw new Error(`Invalid settings in ${path}:\\n${result.error.message}`);\r\n // Resolution owns defaults. Returning the source document preserves the distinction\r\n // between an omitted field and a field explicitly set to an empty collection.\r\n return parsed as BookSettings;\r\n}\r\n\r\n/**\r\n * Resolve settings from all scopes: user → project → local.\r\n * Priority: Local > Project > User.\r\n *\r\n * @param workspace - Project root directory\r\n * @param overridePath - Optional path to an ad-hoc settings file (--settings flag)\r\n * @returns Fully resolved settings with all defaults filled\r\n */\r\nexport interface SettingsResolutionPaths {\r\n home?: string;\r\n userSettingsPath?: string;\r\n projectSettingsPath?: string;\r\n localSettingsPath?: string;\r\n trustStorePath?: string;\r\n}\r\n\r\n/**\r\n * Every settings file this resolution would read, in layer order.\r\n *\r\n * Exported so callers that need to inspect the files themselves — `book doctor`\r\n * reporting removed keys, the credential error checking for a stale `auth`\r\n * block — resolve the same paths the loader does rather than rebuilding them.\r\n */\r\nexport function settingsLayerPaths(\r\n workspace: string,\r\n overridePath?: string,\r\n paths: SettingsResolutionPaths = {},\r\n): string[] {\r\n const userPath =\r\n paths.userSettingsPath ??\r\n (paths.home\r\n ? join(paths.home, '.book', 'settings.json')\r\n : join(resolveBookHome(), 'settings.json'));\r\n const layers = [\r\n userPath,\r\n paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json'),\r\n paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json'),\r\n ];\r\n if (overridePath) layers.push(overridePath);\r\n return layers;\r\n}\r\n\r\nexport function resolveSettings(\r\n workspace: string,\r\n overridePath?: string,\r\n paths: SettingsResolutionPaths = {},\r\n): ResolvedSettings {\r\n let resolved = structuredClone(DEFAULT_SETTINGS);\r\n\r\n // Layer 1: User settings (~/.book/settings.json)\r\n const userPath =\r\n paths.userSettingsPath ??\r\n (paths.home\r\n ? join(paths.home, '.book', 'settings.json')\r\n : join(resolveBookHome(), 'settings.json'));\r\n const user = loadSettingsFile(userPath);\r\n if (user) resolved = mergeLayer(resolved, user, 'trusted');\r\n\r\n // Layer 2: Project settings (<workspace>/.book/settings.json)\r\n const projectPath = paths.projectSettingsPath ?? join(workspace, '.book', 'settings.json');\r\n const project = loadSettingsFile(projectPath);\r\n // `allow` rules from the repository layer are held back rather than merged:\r\n // they only widen authority, and the decisions that release them live in the\r\n // local layer, which has not been merged yet.\r\n const declaredProjectAllow = project?.permissions?.allow ?? [];\r\n // Project-declared hook entries are held back the same way: each one is a\r\n // shell command the repository would otherwise get Book to run.\r\n const declaredProjectHooks = collectDeclaredHooks(project);\r\n // Released hooks belong between the user and local layers, matching the\r\n // relative merge order ungated layers still produce.\r\n const userHookCounts = Object.fromEntries(\r\n HOOK_EVENTS.map((event) => [event, resolved.hooks[event].length]),\r\n ) as Record<HookEvent, number>;\r\n if (project) {\r\n let withheld: BookSettings = project;\r\n if (declaredProjectAllow.length > 0) {\r\n withheld = { ...withheld, permissions: { ...withheld.permissions, allow: [] } };\r\n }\r\n if (declaredProjectHooks.length > 0) {\r\n withheld = { ...withheld, hooks: structuredClone(DEFAULT_SETTINGS.hooks) };\r\n }\r\n resolved = mergeLayer(resolved, withheld, 'repository');\r\n }\r\n\r\n // Layer 3: Local settings (<workspace>/.book/settings.local.json)\r\n const localPath = paths.localSettingsPath ?? join(workspace, '.book', 'settings.local.json');\r\n const local = loadSettingsFile(localPath);\r\n if (local) resolved = mergeLayer(resolved, local, 'local');\r\n\r\n // Layer 4 (optional): Ad-hoc override (--settings flag)\r\n if (overridePath) {\r\n const override = loadSettingsFile(overridePath);\r\n if (override) resolved = mergeLayer(resolved, override, 'trusted');\r\n }\r\n\r\n // Trust decisions come from outside the workspace, so a repository cannot\r\n // ship its own. Layers the user controls may still carry them — the global\r\n // file and an explicit `--settings` path are trusted for everything else —\r\n // but the store has the final say on any key it records.\r\n const trust = loadWorkspaceTrust(\r\n workspace,\r\n paths.trustStorePath ?? defaultTrustStorePath(paths.home),\r\n );\r\n resolved.permissions.projectAllowRules = {\r\n ...resolved.permissions.projectAllowRules,\r\n ...trust.permissionAllowRules,\r\n };\r\n resolved.mcp.projectServers = { ...resolved.mcp.projectServers, ...trust.mcpServers };\r\n resolved.hooks.projectEntries = { ...resolved.hooks.projectEntries, ...trust.hookEntries };\r\n resolved.commands.projectCommands = {\r\n ...resolved.commands.projectCommands,\r\n ...trust.projectCommands,\r\n };\r\n\r\n // Every decision source is in place now, so the withheld repository rules can\r\n // be released — approved ones only.\r\n if (declaredProjectAllow.length > 0) {\r\n const { approved } = partitionProjectAllowRules(\r\n declaredProjectAllow,\r\n resolved.permissions?.projectAllowRules,\r\n );\r\n if (approved.length > 0) {\r\n resolved.permissions.allow = [...(resolved.permissions.allow ?? []), ...approved];\r\n }\r\n }\r\n\r\n if (declaredProjectHooks.length > 0) {\r\n const { approved } = partitionProjectHooks(\r\n declaredProjectHooks,\r\n resolved.hooks?.projectEntries,\r\n );\r\n const approvedByEvent = new Map<HookEvent, HookEntry[]>();\r\n for (const { event, entry } of approved) {\r\n const entries = approvedByEvent.get(event) ?? [];\r\n entries.push(entry);\r\n approvedByEvent.set(event, entries);\r\n }\r\n for (const [event, entries] of approvedByEvent) {\r\n resolved.hooks[event].splice(userHookCounts[event], 0, ...entries);\r\n }\r\n }\r\n\r\n const settings = bookSettingsSchema.parse(resolved) as ResolvedSettings;\r\n return settings;\r\n}\r\n\r\nexport { loadSettingsFile };\r\n\r\n/**\r\n * Migrate rules from the legacy ~/.book/permissions.json into the local\r\n * settings file (<workspace>/.book/settings.local.json). Runs once on first\r\n * load when the legacy file exists and the local settings don't have rules yet.\r\n *\r\n * @param home - User home directory; injectable for isolated migration tests\r\n * @param validatedSettings - Already-resolved settings from the startup preflight\r\n * @returns true if migration occurred, false otherwise\r\n */\r\nexport function migrateLegacyPermissions(\r\n workspace: string,\r\n home?: string,\r\n validatedSettings?: ResolvedSettings,\r\n): boolean {\r\n // Direct callers must observe the same fail-before-storage boundary as the\r\n // normal startup path. Internal callers pass already-resolved settings to\r\n // avoid reading the layers twice on the common off path.\r\n if (!validatedSettings) {\r\n resolveSettings(workspace, undefined, home ? { home } : undefined);\r\n }\r\n\r\n const legacyPath = join(home ? join(home, '.book') : resolveBookHome(), 'permissions.json');\r\n if (!existsSync(legacyPath)) return false;\r\n const markerPath = join(workspace, '.book', 'migrations.json');\r\n try {\r\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as {\r\n legacyPermissions?: number;\r\n };\r\n if ((marker.legacyPermissions ?? 0) >= LEGACY_PERMISSIONS_MIGRATION_VERSION) return false;\r\n } catch {\r\n // Missing or malformed markers are safely rebuilt after a successful migration.\r\n }\r\n\r\n let legacyRules: Array<{ toolName: string; pattern?: string; effect: string }> = [];\r\n try {\r\n const raw = readFileSync(legacyPath, 'utf-8');\r\n const parsed = JSON.parse(raw) as {\r\n rules: Array<{ toolName: string; pattern?: string; effect: string }>;\r\n };\r\n legacyRules = parsed.rules ?? [];\r\n } catch {\r\n return false; // corrupt — leave the legacy file alone\r\n }\r\n\r\n if (legacyRules.length === 0) return false;\r\n\r\n const localDir = join(workspace, '.book');\r\n const localPath = join(localDir, 'settings.local.json');\r\n const result = new SettingsRepository(localPath).update((existing) => {\r\n const permissions = (existing.permissions ?? {}) as {\r\n allow?: string[];\r\n ask?: string[];\r\n deny?: string[];\r\n };\r\n for (const key of ['allow', 'ask', 'deny'] as const) {\r\n if (!Array.isArray(permissions[key])) permissions[key] = [];\r\n }\r\n\r\n for (const rule of legacyRules) {\r\n const specifier = rule.pattern ? `${rule.toolName}(${rule.pattern})` : rule.toolName;\r\n const effect = rule.effect as 'allow' | 'ask' | 'deny';\r\n if (!permissions[effect]!.includes(specifier)) permissions[effect]!.push(specifier);\r\n }\r\n existing.permissions = permissions;\r\n });\r\n if (!result.ok) return false;\r\n\r\n writeFileAtomic(\r\n markerPath,\r\n `${JSON.stringify({ legacyPermissions: LEGACY_PERMISSIONS_MIGRATION_VERSION }, null, 2)}\\n`,\r\n );\r\n if (!result.changed) return false;\r\n\r\n console.warn(\r\n `⚠ Migrated ${legacyRules.length} permission rule(s) from ~/.book/permissions.json to ${localPath}. ` +\r\n 'Delete the legacy file after verifying the migration.',\r\n );\r\n return true;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1,217 +0,0 @@
1
- // src/cli/ink-scroll-renderer.ts
2
- import { createRequire } from "module";
3
- import { dirname, join } from "path";
4
- import { pathToFileURL } from "url";
5
- var pendingHint = null;
6
- var rendererInstalled = false;
7
- function isInkScrollRendererEnabled(value) {
8
- return value === "on";
9
- }
10
- function setTranscriptScrollHint(hint) {
11
- if (!Number.isFinite(hint.delta) || hint.delta === 0) return;
12
- if (pendingHint && pendingHint.top === hint.top && pendingHint.bottom === hint.bottom && Math.sign(pendingHint.delta) === Math.sign(hint.delta)) {
13
- pendingHint = { ...pendingHint, delta: pendingHint.delta + hint.delta };
14
- return;
15
- }
16
- pendingHint = { ...hint };
17
- }
18
- function splitFrame(frame) {
19
- return frame.endsWith("\n") ? frame.slice(0, -1).split("\n") : frame.split("\n");
20
- }
21
- function getFrameEndRow(frame, visibleLineCount) {
22
- return visibleLineCount + (frame.endsWith("\n") ? 1 : 0);
23
- }
24
- function cursorTo(row) {
25
- return `\x1B[${Math.max(1, Math.floor(row))};1H`;
26
- }
27
- function setScrollRegion(top, bottom) {
28
- return `\x1B[${Math.max(1, Math.floor(top))};${Math.max(1, Math.floor(bottom))}r`;
29
- }
30
- function isPureVerticalShift(previousLines, nextLines, hint) {
31
- if (previousLines.length !== nextLines.length) return false;
32
- const top = Math.floor(hint.top) - 1;
33
- const bottom = Math.floor(hint.bottom);
34
- const delta = Math.trunc(hint.delta);
35
- if (top < 0 || bottom > nextLines.length || bottom - top < 2) return false;
36
- if (Math.abs(delta) >= bottom - top) return false;
37
- for (let row = 0; row < top; row++) {
38
- if (previousLines[row] !== nextLines[row]) return false;
39
- }
40
- for (let row = bottom; row < nextLines.length; row++) {
41
- if (previousLines[row] !== nextLines[row]) return false;
42
- }
43
- if (delta > 0) {
44
- for (let row = top; row < bottom - delta; row++) {
45
- if (nextLines[row] !== previousLines[row + delta]) return false;
46
- }
47
- } else {
48
- const distance = Math.abs(delta);
49
- for (let row = top + distance; row < bottom; row++) {
50
- if (nextLines[row] !== previousLines[row - distance]) return false;
51
- }
52
- }
53
- return true;
54
- }
55
- function writeExposedRows(stream, lines, top, bottom, delta) {
56
- const distance = Math.abs(delta);
57
- const first = delta > 0 ? bottom - distance : top;
58
- for (let offset = 0; offset < distance; offset++) {
59
- const row = first + offset;
60
- stream.write(cursorTo(row + 1) + (lines[row] ?? "") + "\x1B[K");
61
- }
62
- }
63
- function createScrollAwareRenderer(stream, options, createBase) {
64
- const base = createBase(stream, options);
65
- let previousFrame = "";
66
- const render = ((output) => {
67
- const hint = pendingHint;
68
- pendingHint = null;
69
- const previousLines = splitFrame(previousFrame);
70
- const nextLines = splitFrame(output);
71
- const terminalRows = stream.rows ?? 0;
72
- const frameEndRow = getFrameEndRow(output, nextLines.length);
73
- const canScroll = Boolean(stream.isTTY) && terminalRows > 0 && frameEndRow <= terminalRows && previousFrame.length > 0 && hint !== null && hint.bottom <= nextLines.length && isPureVerticalShift(previousLines, nextLines, hint);
74
- if (canScroll && hint) {
75
- const top = Math.floor(hint.top);
76
- const bottom = Math.floor(hint.bottom);
77
- const delta = Math.trunc(hint.delta);
78
- stream.write("\x1B[?25l" + setScrollRegion(top, bottom));
79
- stream.write(`\x1B[${Math.abs(delta)}${delta > 0 ? "S" : "T"}`);
80
- writeExposedRows(stream, nextLines, top - 1, bottom, delta);
81
- stream.write(setScrollRegion(1, terminalRows) + cursorTo(frameEndRow));
82
- previousFrame = output;
83
- base.sync(output);
84
- return true;
85
- }
86
- previousFrame = output;
87
- return base(output);
88
- });
89
- render.clear = () => {
90
- pendingHint = null;
91
- previousFrame = "";
92
- base.clear();
93
- };
94
- render.done = () => {
95
- pendingHint = null;
96
- previousFrame = "";
97
- base.done();
98
- };
99
- render.sync = (output) => {
100
- pendingHint = null;
101
- previousFrame = output;
102
- base.sync(output);
103
- };
104
- render.setCursorPosition = (position) => base.setCursorPosition(position);
105
- render.isCursorDirty = () => base.isCursorDirty();
106
- render.willRender = (output) => {
107
- const shouldRender = base.willRender(output);
108
- if (!shouldRender) pendingHint = null;
109
- return shouldRender;
110
- };
111
- return render;
112
- }
113
- async function installInkScrollRenderer(enabled) {
114
- if (rendererInstalled) return;
115
- if (!(enabled ?? isInkScrollRendererEnabled(process.env.BOOK_SCROLL_RENDERER))) return;
116
- rendererInstalled = true;
117
- try {
118
- const require2 = createRequire(import.meta.url);
119
- const inkEntry = require2.resolve("ink");
120
- const logUpdateUrl = pathToFileURL(join(dirname(inkEntry), "log-update.js")).href;
121
- const module = await import(logUpdateUrl);
122
- const logUpdate = module.default;
123
- const createBase = logUpdate.create;
124
- logUpdate.create = (stream, options) => createScrollAwareRenderer(stream, options, createBase);
125
- } catch {
126
- }
127
- }
128
-
129
- // src/cli/frame-buffer.ts
130
- import { createRequire as createRequire2 } from "module";
131
- import { dirname as dirname2, join as join2 } from "path";
132
- import { pathToFileURL as pathToFileURL2 } from "url";
133
- var latestFrame = "";
134
- var lastCursorPosition = null;
135
- var captureInstalled = false;
136
- var frameUpdateListeners = /* @__PURE__ */ new Set();
137
- function notifyFrameUpdate() {
138
- for (const listener of frameUpdateListeners) {
139
- try {
140
- listener();
141
- } catch {
142
- }
143
- }
144
- }
145
- function splitFrame2(frame) {
146
- return frame.endsWith("\n") ? frame.slice(0, -1).split("\n") : frame.split("\n");
147
- }
148
- function toCursorPosition(position) {
149
- if (!position || typeof position !== "object") return null;
150
- const candidate = position;
151
- if (typeof candidate.x !== "number" || typeof candidate.y !== "number") return null;
152
- return { x: candidate.x, y: candidate.y };
153
- }
154
- function getFrameLines() {
155
- return latestFrame.length === 0 ? [] : splitFrame2(latestFrame);
156
- }
157
- function getLastCursorPosition() {
158
- return lastCursorPosition;
159
- }
160
- function subscribeToFrameUpdates(listener) {
161
- frameUpdateListeners.add(listener);
162
- return () => frameUpdateListeners.delete(listener);
163
- }
164
- function createFrameCapturingRenderer(stream, options, createBase) {
165
- const base = createBase(stream, options);
166
- const render = ((output) => {
167
- latestFrame = output;
168
- const rendered = base(output);
169
- if (rendered !== false) notifyFrameUpdate();
170
- return rendered;
171
- });
172
- render.clear = () => {
173
- latestFrame = "";
174
- base.clear();
175
- };
176
- render.done = () => {
177
- latestFrame = "";
178
- lastCursorPosition = null;
179
- base.done();
180
- };
181
- render.sync = (output) => {
182
- latestFrame = output;
183
- base.sync(output);
184
- notifyFrameUpdate();
185
- };
186
- render.setCursorPosition = (position) => {
187
- lastCursorPosition = toCursorPosition(position);
188
- base.setCursorPosition(position);
189
- };
190
- render.isCursorDirty = () => base.isCursorDirty();
191
- render.willRender = (output) => base.willRender(output);
192
- return render;
193
- }
194
- async function installFrameCapture() {
195
- if (captureInstalled) return;
196
- captureInstalled = true;
197
- try {
198
- const require2 = createRequire2(import.meta.url);
199
- const inkEntry = require2.resolve("ink");
200
- const logUpdateUrl = pathToFileURL2(join2(dirname2(inkEntry), "log-update.js")).href;
201
- const module = await import(logUpdateUrl);
202
- const logUpdate = module.default;
203
- const createBase = logUpdate.create;
204
- logUpdate.create = (stream, options) => createFrameCapturingRenderer(stream, options, createBase);
205
- } catch {
206
- }
207
- }
208
-
209
- export {
210
- setTranscriptScrollHint,
211
- installInkScrollRenderer,
212
- getFrameLines,
213
- getLastCursorPosition,
214
- subscribeToFrameUpdates,
215
- installFrameCapture
216
- };
217
- //# sourceMappingURL=chunk-C7D4NJ2D.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/ink-scroll-renderer.ts","../src/cli/frame-buffer.ts"],"sourcesContent":["import { createRequire } from 'node:module';\r\nimport { dirname, join } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\nexport interface TranscriptScrollHint {\r\n top: number;\r\n bottom: number;\r\n delta: number;\r\n}\r\n\r\ninterface InkStream {\r\n isTTY?: boolean;\r\n rows?: number;\r\n write: (data: string) => unknown;\r\n}\r\n\r\ninterface LogUpdateRenderer {\r\n (output: string): boolean | void;\r\n clear: () => void;\r\n done: () => void;\r\n sync: (output: string) => void;\r\n setCursorPosition: (position: unknown) => void;\r\n isCursorDirty: () => boolean;\r\n willRender: (output: string) => boolean;\r\n}\r\n\r\ninterface LogUpdateModule {\r\n create: (stream: InkStream, options?: { incremental?: boolean }) => LogUpdateRenderer;\r\n}\r\n\r\nlet pendingHint: TranscriptScrollHint | null = null;\r\nlet rendererInstalled = false;\r\n\r\nexport function isInkScrollRendererEnabled(value?: string): boolean {\r\n return value === 'on';\r\n}\r\n\r\nexport function setTranscriptScrollHint(hint: TranscriptScrollHint): void {\r\n if (!Number.isFinite(hint.delta) || hint.delta === 0) return;\r\n if (\r\n pendingHint &&\r\n pendingHint.top === hint.top &&\r\n pendingHint.bottom === hint.bottom &&\r\n Math.sign(pendingHint.delta) === Math.sign(hint.delta)\r\n ) {\r\n pendingHint = { ...pendingHint, delta: pendingHint.delta + hint.delta };\r\n return;\r\n }\r\n pendingHint = { ...hint };\r\n}\r\n\r\nfunction splitFrame(frame: string): string[] {\r\n return frame.endsWith('\\n') ? frame.slice(0, -1).split('\\n') : frame.split('\\n');\r\n}\r\n\r\nfunction getFrameEndRow(frame: string, visibleLineCount: number): number {\r\n return visibleLineCount + (frame.endsWith('\\n') ? 1 : 0);\r\n}\r\n\r\nfunction cursorTo(row: number): string {\r\n return `\\x1b[${Math.max(1, Math.floor(row))};1H`;\r\n}\r\n\r\nfunction setScrollRegion(top: number, bottom: number): string {\r\n return `\\x1b[${Math.max(1, Math.floor(top))};${Math.max(1, Math.floor(bottom))}r`;\r\n}\r\n\r\nexport function isPureVerticalShift(\r\n previousLines: readonly string[],\r\n nextLines: readonly string[],\r\n hint: TranscriptScrollHint,\r\n): boolean {\r\n if (previousLines.length !== nextLines.length) return false;\r\n const top = Math.floor(hint.top) - 1;\r\n const bottom = Math.floor(hint.bottom);\r\n const delta = Math.trunc(hint.delta);\r\n if (top < 0 || bottom > nextLines.length || bottom - top < 2) return false;\r\n if (Math.abs(delta) >= bottom - top) return false;\r\n\r\n for (let row = 0; row < top; row++) {\r\n if (previousLines[row] !== nextLines[row]) return false;\r\n }\r\n for (let row = bottom; row < nextLines.length; row++) {\r\n if (previousLines[row] !== nextLines[row]) return false;\r\n }\r\n\r\n if (delta > 0) {\r\n for (let row = top; row < bottom - delta; row++) {\r\n if (nextLines[row] !== previousLines[row + delta]) return false;\r\n }\r\n } else {\r\n const distance = Math.abs(delta);\r\n for (let row = top + distance; row < bottom; row++) {\r\n if (nextLines[row] !== previousLines[row - distance]) return false;\r\n }\r\n }\r\n return true;\r\n}\r\n\r\nfunction writeExposedRows(\r\n stream: InkStream,\r\n lines: readonly string[],\r\n top: number,\r\n bottom: number,\r\n delta: number,\r\n): void {\r\n const distance = Math.abs(delta);\r\n const first = delta > 0 ? bottom - distance : top;\r\n for (let offset = 0; offset < distance; offset++) {\r\n const row = first + offset;\r\n stream.write(cursorTo(row + 1) + (lines[row] ?? '') + '\\x1b[K');\r\n }\r\n}\r\n\r\nfunction createScrollAwareRenderer(\r\n stream: InkStream,\r\n options: { incremental?: boolean } | undefined,\r\n createBase: LogUpdateModule['create'],\r\n): LogUpdateRenderer {\r\n const base = createBase(stream, options);\r\n let previousFrame = '';\r\n\r\n const render = ((output: string) => {\r\n const hint = pendingHint;\r\n pendingHint = null;\r\n const previousLines = splitFrame(previousFrame);\r\n const nextLines = splitFrame(output);\r\n const terminalRows = stream.rows ?? 0;\r\n const frameEndRow = getFrameEndRow(output, nextLines.length);\r\n const canScroll =\r\n Boolean(stream.isTTY) &&\r\n terminalRows > 0 &&\r\n frameEndRow <= terminalRows &&\r\n previousFrame.length > 0 &&\r\n hint !== null &&\r\n hint.bottom <= nextLines.length &&\r\n isPureVerticalShift(previousLines, nextLines, hint);\r\n\r\n if (canScroll && hint) {\r\n const top = Math.floor(hint.top);\r\n const bottom = Math.floor(hint.bottom);\r\n const delta = Math.trunc(hint.delta);\r\n stream.write('\\x1b[?25l' + setScrollRegion(top, bottom));\r\n stream.write(`\\x1b[${Math.abs(delta)}${delta > 0 ? 'S' : 'T'}`);\r\n writeExposedRows(stream, nextLines, top - 1, bottom, delta);\r\n // Ink's sync() positions the input cursor relative to the frame bottom.\r\n // Re-anchor there first so its cursor suffix cannot repaint the prompt on\r\n // a transcript row after a terminal-native scroll.\r\n stream.write(setScrollRegion(1, terminalRows) + cursorTo(frameEndRow));\r\n previousFrame = output;\r\n base.sync(output);\r\n return true;\r\n }\r\n\r\n previousFrame = output;\r\n return base(output);\r\n }) as LogUpdateRenderer;\r\n\r\n render.clear = () => {\r\n pendingHint = null;\r\n previousFrame = '';\r\n base.clear();\r\n };\r\n render.done = () => {\r\n pendingHint = null;\r\n previousFrame = '';\r\n base.done();\r\n };\r\n render.sync = (output: string) => {\r\n pendingHint = null;\r\n previousFrame = output;\r\n base.sync(output);\r\n };\r\n render.setCursorPosition = (position: unknown) => base.setCursorPosition(position);\r\n render.isCursorDirty = () => base.isCursorDirty();\r\n render.willRender = (output: string) => {\r\n const shouldRender = base.willRender(output);\r\n if (!shouldRender) pendingHint = null;\r\n return shouldRender;\r\n };\r\n return render;\r\n}\r\n\r\nexport async function installInkScrollRenderer(enabled?: boolean): Promise<void> {\r\n if (rendererInstalled) return;\r\n // DECSTBM/CSI S/T rendering remains available for profiling, but Windows\r\n // ConPTY can corrupt fixed footer rows after a later incremental repaint.\r\n if (!(enabled ?? isInkScrollRendererEnabled(process.env.BOOK_SCROLL_RENDERER))) return;\r\n rendererInstalled = true;\r\n\r\n try {\r\n const require = createRequire(import.meta.url);\r\n const inkEntry = require.resolve('ink');\r\n const logUpdateUrl = pathToFileURL(join(dirname(inkEntry), 'log-update.js')).href;\r\n const module = (await import(logUpdateUrl)) as { default: LogUpdateModule };\r\n const logUpdate = module.default;\r\n const createBase = logUpdate.create;\r\n logUpdate.create = (stream, options) => createScrollAwareRenderer(stream, options, createBase);\r\n } catch {\r\n // Keep Ink's built-in renderer when its internal module layout changes.\r\n }\r\n}\r\n","import { createRequire } from 'node:module';\r\nimport { dirname, join } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\nexport interface FrameCursorPosition {\r\n x: number;\r\n y: number;\r\n}\r\n\r\ninterface InkStream {\r\n isTTY?: boolean;\r\n rows?: number;\r\n write: (data: string) => unknown;\r\n}\r\n\r\ninterface LogUpdateRenderer {\r\n (output: string): boolean | void;\r\n clear: () => void;\r\n done: () => void;\r\n sync: (output: string) => void;\r\n setCursorPosition: (position: unknown) => void;\r\n isCursorDirty: () => boolean;\r\n willRender: (output: string) => boolean;\r\n}\r\n\r\ninterface LogUpdateModule {\r\n create: (stream: InkStream, options?: { incremental?: boolean }) => LogUpdateRenderer;\r\n}\r\n\r\nlet latestFrame = '';\r\nlet lastCursorPosition: FrameCursorPosition | null = null;\r\nlet captureInstalled = false;\r\nconst frameUpdateListeners = new Set<() => void>();\r\n\r\nfunction notifyFrameUpdate(): void {\r\n for (const listener of frameUpdateListeners) {\r\n try {\r\n listener();\r\n } catch {\r\n // A terminal overlay must never break Ink's renderer.\r\n }\r\n }\r\n}\r\n\r\nfunction splitFrame(frame: string): string[] {\r\n return frame.endsWith('\\n') ? frame.slice(0, -1).split('\\n') : frame.split('\\n');\r\n}\r\n\r\nfunction toCursorPosition(position: unknown): FrameCursorPosition | null {\r\n if (!position || typeof position !== 'object') return null;\r\n const candidate = position as { x?: unknown; y?: unknown };\r\n if (typeof candidate.x !== 'number' || typeof candidate.y !== 'number') return null;\r\n return { x: candidate.x, y: candidate.y };\r\n}\r\n\r\n/** Lines of the most recently rendered full-screen frame. */\r\nexport function getFrameLines(): string[] {\r\n return latestFrame.length === 0 ? [] : splitFrame(latestFrame);\r\n}\r\n\r\n/** Last cursor position Ink asked the renderer to restore, if any. */\r\nexport function getLastCursorPosition(): FrameCursorPosition | null {\r\n return lastCursorPosition;\r\n}\r\n\r\n/** Subscribe to completed Ink frame writes, used to preserve terminal overlays. */\r\nexport function subscribeToFrameUpdates(listener: () => void): () => void {\r\n frameUpdateListeners.add(listener);\r\n return () => frameUpdateListeners.delete(listener);\r\n}\r\n\r\n/** Override the captured snapshot. Used by focused selection tests. */\r\nexport function setFrameSnapshotForTesting(\r\n frame: string | null,\r\n cursor: FrameCursorPosition | null = null,\r\n): void {\r\n latestFrame = frame ?? '';\r\n lastCursorPosition = cursor;\r\n}\r\n\r\nexport function createFrameCapturingRenderer(\r\n stream: InkStream,\r\n options: { incremental?: boolean } | undefined,\r\n createBase: LogUpdateModule['create'],\r\n): LogUpdateRenderer {\r\n const base = createBase(stream, options);\r\n\r\n const render = ((output: string) => {\r\n latestFrame = output;\r\n const rendered = base(output);\r\n if (rendered !== false) notifyFrameUpdate();\r\n return rendered;\r\n }) as LogUpdateRenderer;\r\n\r\n render.clear = () => {\r\n latestFrame = '';\r\n base.clear();\r\n };\r\n render.done = () => {\r\n latestFrame = '';\r\n lastCursorPosition = null;\r\n base.done();\r\n };\r\n render.sync = (output: string) => {\r\n latestFrame = output;\r\n base.sync(output);\r\n notifyFrameUpdate();\r\n };\r\n render.setCursorPosition = (position: unknown) => {\r\n lastCursorPosition = toCursorPosition(position);\r\n base.setCursorPosition(position);\r\n };\r\n render.isCursorDirty = () => base.isCursorDirty();\r\n render.willRender = (output: string) => base.willRender(output);\r\n return render;\r\n}\r\n\r\n/** Capture Ink's latest full frame so mouse selection can extract visible text. */\r\nexport async function installFrameCapture(): Promise<void> {\r\n if (captureInstalled) return;\r\n captureInstalled = true;\r\n\r\n try {\r\n const require = createRequire(import.meta.url);\r\n const inkEntry = require.resolve('ink');\r\n const logUpdateUrl = pathToFileURL(join(dirname(inkEntry), 'log-update.js')).href;\r\n const module = (await import(logUpdateUrl)) as { default: LogUpdateModule };\r\n const logUpdate = module.default;\r\n const createBase = logUpdate.create;\r\n logUpdate.create = (stream, options) =>\r\n createFrameCapturingRenderer(stream, options, createBase);\r\n } catch {\r\n // Keep the TUI usable if Ink changes its private renderer layout.\r\n }\r\n}\r\n"],"mappings":"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","names":["require","createRequire","dirname","join","pathToFileURL","splitFrame","require","createRequire","pathToFileURL","join","dirname"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/model-window-store.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\r\nimport { join } from 'node:path';\r\nimport { z } from 'zod';\r\nimport { resolveBookHome } from './book-home.js';\r\nimport { writeFileAtomic } from './settings-repository.js';\r\n\r\nexport const MODEL_WINDOW_STORE_VERSION = 1;\r\n\r\n/**\r\n * Fraction of the refused prompt history recorded as the learned window ceiling.\r\n *\r\n * When a provider rejects a request for exceeding the context limit, the history\r\n * tokens at that moment represent a size that was REFUSED, not a size the provider\r\n * accepts. Recording the refused size directly would set a ceiling larger than the\r\n * true context window (e.g. true window 100k, refused history 150k -> ceiling 150k),\r\n * causing repeated user-visible failures. Applying a safety margin ensures the\r\n * recorded ceiling is strictly below the refused size and converges rapidly to a\r\n * viable operating limit.\r\n */\r\nexport const LEARNED_WINDOW_SAFETY_MARGIN = 0.8;\r\n\r\n/**\r\n * Minimum context window ceiling recorded from an overflow refusal.\r\n *\r\n * Sane coding sessions require at least 16k tokens for tools, system prompt,\r\n * and working context. Any refusal below this floor is far more likely to be an\r\n * upstream proxy error (e.g. 413 Request Entity Too Large) or bad telemetry\r\n * than a real context limit. Refusals below this floor are ignored rather than\r\n * recorded, preventing a transient error from permanently bricking the model.\r\n */\r\nexport const MIN_LEARNED_CONTEXT_WINDOW = 16_384;\r\n\r\nexport interface LearnedModelWindowEntry {\r\n contextWindow: number;\r\n learnedAt: number;\r\n}\r\n\r\nexport interface ModelWindowStoreData {\r\n version: number;\r\n models: Record<string, LearnedModelWindowEntry>;\r\n}\r\n\r\nexport interface ModelWindowStoreOptions {\r\n /** Explicit BOOK_HOME directory root (used by tests to isolate storage). */\r\n home?: string;\r\n /** Explicit file path override (used by tests). */\r\n path?: string;\r\n /** Optional clock override for timestamps (used by tests). */\r\n now?: () => number;\r\n}\r\n\r\nexport interface ModelWindowStore {\r\n get(model: string): number | undefined;\r\n getEntry(model: string): LearnedModelWindowEntry | undefined;\r\n ratchet(model: string, ceiling: number): boolean;\r\n all(): Record<string, LearnedModelWindowEntry>;\r\n}\r\n\r\nconst learnedModelWindowEntrySchema = z.object({\r\n contextWindow: z.number().int().positive(),\r\n learnedAt: z.number().int().nonnegative(),\r\n});\r\n\r\nconst looseModelWindowStoreSchema = z.object({\r\n version: z.number().int().positive().default(MODEL_WINDOW_STORE_VERSION),\r\n models: z.record(z.unknown()).default({}),\r\n});\r\n\r\nfunction emptyStore(): ModelWindowStoreData {\r\n return { version: MODEL_WINDOW_STORE_VERSION, models: {} };\r\n}\r\n\r\n/**\r\n * Resolved location for the user-global learned-window store.\r\n *\r\n * This file lives under BOOK_HOME (e.g. ~/.book/model-windows.json), NEVER inside\r\n * the workspace. A repository must never be able to declare or tamper with\r\n * learned window ceilings for itself.\r\n */\r\nexport function defaultModelWindowStorePath(home?: string): string {\r\n return home ? join(home, 'model-windows.json') : join(resolveBookHome(), 'model-windows.json');\r\n}\r\n\r\n/**\r\n * Read the learned-window store.\r\n * Tolerates missing, empty, or corrupt files by returning an empty store.\r\n * Never throws into a run.\r\n */\r\nexport function readModelWindowStore(options?: ModelWindowStoreOptions): ModelWindowStoreData {\r\n const filePath = options?.path ?? defaultModelWindowStorePath(options?.home);\r\n if (!existsSync(filePath)) {\r\n return emptyStore();\r\n }\r\n try {\r\n const raw = readFileSync(filePath, 'utf-8').trim();\r\n if (!raw) return emptyStore();\r\n const parsed = JSON.parse(raw);\r\n const validated = looseModelWindowStoreSchema.safeParse(parsed);\r\n if (!validated.success) {\r\n return emptyStore();\r\n }\r\n const models: Record<string, LearnedModelWindowEntry> = {};\r\n for (const [key, value] of Object.entries(validated.data.models)) {\r\n const entryResult = learnedModelWindowEntrySchema.safeParse(value);\r\n if (entryResult.success) {\r\n models[key] = entryResult.data;\r\n }\r\n }\r\n return {\r\n version: validated.data.version,\r\n models,\r\n };\r\n } catch {\r\n return emptyStore();\r\n }\r\n}\r\n\r\n/**\r\n * Atomically persist the learned-window store.\r\n * Never throws into a run.\r\n */\r\nexport function writeModelWindowStore(\r\n data: ModelWindowStoreData,\r\n options?: ModelWindowStoreOptions,\r\n): boolean {\r\n if (data.version > MODEL_WINDOW_STORE_VERSION) {\r\n return false;\r\n }\r\n const filePath = options?.path ?? defaultModelWindowStorePath(options?.home);\r\n try {\r\n if (existsSync(filePath)) {\r\n const existing = readModelWindowStore(options);\r\n if (existing.version > MODEL_WINDOW_STORE_VERSION) {\r\n return false;\r\n }\r\n }\r\n const serialized = JSON.stringify(data, null, 2) + '\\n';\r\n writeFileAtomic(filePath, serialized);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Records a learned context window ceiling for a model.\r\n *\r\n * Ratchet, downward only:\r\n * - If no entry exists for the model, records the new ceiling.\r\n * - If an entry exists with a larger ceiling, lowers it.\r\n * - If an entry exists with an equal or smaller ceiling, changes nothing.\r\n *\r\n * Monotonicity is strict: a later overflow at a smaller size lowers it further;\r\n * an overflow at a larger size does not raise it.\r\n *\r\n * Returns true if the store was updated with a new ceiling, false otherwise.\r\n */\r\nexport function ratchetModelWindow(\r\n model: string,\r\n ceiling: number,\r\n options?: ModelWindowStoreOptions,\r\n): boolean {\r\n return createModelWindowStore(options).ratchet(model, ceiling);\r\n}\r\n\r\n/**\r\n * File-backed implementation of ModelWindowStore.\r\n * Delegates to createModelWindowStore for closure-based, freeze-safe caching.\r\n */\r\nexport class FileModelWindowStore implements ModelWindowStore {\r\n private readonly inner: ModelWindowStore;\r\n\r\n constructor(options?: ModelWindowStoreOptions) {\r\n this.inner = createModelWindowStore(options);\r\n }\r\n\r\n get(model: string): number | undefined {\r\n return this.inner.get(model);\r\n }\r\n\r\n getEntry(model: string): LearnedModelWindowEntry | undefined {\r\n return this.inner.getEntry(model);\r\n }\r\n\r\n ratchet(model: string, ceiling: number): boolean {\r\n return this.inner.ratchet(model, ceiling);\r\n }\r\n\r\n all(): Record<string, LearnedModelWindowEntry> {\r\n return this.inner.all();\r\n }\r\n}\r\n\r\n/**\r\n * In-memory implementation of ModelWindowStore, useful for unit tests.\r\n */\r\nexport class MemoryModelWindowStore implements ModelWindowStore {\r\n private readonly models = new Map<string, LearnedModelWindowEntry>();\r\n private readonly now: () => number;\r\n\r\n constructor(initial?: Record<string, LearnedModelWindowEntry>, now = Date.now) {\r\n this.now = now;\r\n if (initial) {\r\n for (const [k, v] of Object.entries(initial)) {\r\n this.models.set(k, { ...v });\r\n }\r\n }\r\n }\r\n\r\n get(model: string): number | undefined {\r\n return this.models.get(model)?.contextWindow;\r\n }\r\n\r\n getEntry(model: string): LearnedModelWindowEntry | undefined {\r\n const entry = this.models.get(model);\r\n return entry ? { ...entry } : undefined;\r\n }\r\n\r\n ratchet(model: string, ceiling: number): boolean {\r\n if (\r\n !model ||\r\n typeof ceiling !== 'number' ||\r\n !Number.isFinite(ceiling) ||\r\n ceiling < MIN_LEARNED_CONTEXT_WINDOW\r\n ) {\r\n return false;\r\n }\r\n const normalized = Math.floor(ceiling);\r\n const existing = this.models.get(model);\r\n if (existing && existing.contextWindow <= normalized) {\r\n return false;\r\n }\r\n this.models.set(model, { contextWindow: normalized, learnedAt: this.now() });\r\n return true;\r\n }\r\n\r\n all(): Record<string, LearnedModelWindowEntry> {\r\n const result: Record<string, LearnedModelWindowEntry> = {};\r\n for (const [k, v] of this.models.entries()) {\r\n result[k] = { ...v };\r\n }\r\n return result;\r\n }\r\n}\r\n\r\n/**\r\n * Create a freeze-safe ModelWindowStore instance.\r\n *\r\n * Cache state is held in a closure rather than as object properties, ensuring\r\n * that Object.freeze / deepFreeze(config) does not break subsequent memoization.\r\n */\r\nexport function createModelWindowStore(options?: ModelWindowStoreOptions): ModelWindowStore {\r\n let cachedData: ModelWindowStoreData | undefined;\r\n\r\n function load(): ModelWindowStoreData {\r\n if (cachedData === undefined) {\r\n cachedData = readModelWindowStore(options);\r\n }\r\n return cachedData;\r\n }\r\n\r\n return {\r\n get(model: string): number | undefined {\r\n if (!model) return undefined;\r\n return load().models[model]?.contextWindow;\r\n },\r\n\r\n getEntry(model: string): LearnedModelWindowEntry | undefined {\r\n if (!model) return undefined;\r\n const entry = load().models[model];\r\n return entry ? { ...entry } : undefined;\r\n },\r\n\r\n ratchet(model: string, ceiling: number): boolean {\r\n if (\r\n !model ||\r\n typeof ceiling !== 'number' ||\r\n !Number.isFinite(ceiling) ||\r\n ceiling < MIN_LEARNED_CONTEXT_WINDOW\r\n ) {\r\n return false;\r\n }\r\n const fresh = readModelWindowStore(options);\r\n if (fresh.version > MODEL_WINDOW_STORE_VERSION) {\r\n return false;\r\n }\r\n const normalizedCeiling = Math.floor(ceiling);\r\n const existingEntry = fresh.models[model];\r\n const targetCeiling = existingEntry\r\n ? Math.min(existingEntry.contextWindow, normalizedCeiling)\r\n : normalizedCeiling;\r\n\r\n if (existingEntry && existingEntry.contextWindow === targetCeiling) {\r\n // If the merged value equals what is already on disk, write nothing and return false.\r\n return false;\r\n }\r\n\r\n const now = options?.now ? options.now() : Date.now();\r\n const updated: ModelWindowStoreData = {\r\n version: fresh.version,\r\n models: {\r\n ...fresh.models,\r\n [model]: {\r\n contextWindow: targetCeiling,\r\n learnedAt: now,\r\n },\r\n },\r\n };\r\n const written = writeModelWindowStore(updated, options);\r\n if (written) {\r\n cachedData = updated;\r\n return true;\r\n }\r\n return false;\r\n },\r\n\r\n all(): Record<string, LearnedModelWindowEntry> {\r\n const models = load().models;\r\n const result: Record<string, LearnedModelWindowEntry> = {};\r\n for (const [k, v] of Object.entries(models)) {\r\n result[k] = { ...v };\r\n }\r\n return result;\r\n },\r\n };\r\n}\r\n"],"mappings":"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","names":[]}