@letrquan/book 0.2.0 → 0.4.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -2,30 +2,51 @@ import {
2
2
  mcpProjectServerChoiceSchema,
3
3
  projectAllowRuleChoiceSchema,
4
4
  projectCommandChoiceSchema,
5
+ projectDirectoryChoiceSchema,
5
6
  projectHookChoiceSchema,
7
+ workspaceProviderChoiceSchema,
6
8
  writeFileAtomic
7
- } from "./chunk-574C6MQR.js";
9
+ } from "./chunk-REASC2QG.js";
8
10
  import {
9
11
  resolveBookHome
10
- } from "./chunk-24AXE6SP.js";
12
+ } from "./chunk-ZPSZZUWF.js";
11
13
 
12
14
  // src/workspace-trust.ts
13
15
  import { existsSync, readFileSync } from "fs";
14
16
  import { join, resolve } from "path";
15
17
  import { z } from "zod";
16
- var TRUST_STORE_VERSION = 2;
18
+ var TRUST_STORE_VERSION = 4;
17
19
  var workspaceTrustSchema = z.object({
18
20
  /** Keyed by the exact rule text the project layer declared. */
19
- permissionAllowRules: z.record(projectAllowRuleChoiceSchema).default({}),
21
+ permissionAllowRules: z.record(z.string(), projectAllowRuleChoiceSchema).default({}),
20
22
  /** Keyed by server name, carrying the fingerprint the decision was made against. */
21
- mcpServers: z.record(mcpProjectServerChoiceSchema).default({}),
23
+ mcpServers: z.record(z.string(), mcpProjectServerChoiceSchema).default({}),
22
24
  /** Keyed by a fingerprint of `{ event, matcher, command, env }`. */
23
- hookEntries: z.record(projectHookChoiceSchema).default({}),
25
+ hookEntries: z.record(z.string(), projectHookChoiceSchema).default({}),
24
26
  /** Keyed by command name, carrying the fingerprint of the shell its body runs. */
25
- projectCommands: z.record(projectCommandChoiceSchema).default({})
27
+ projectCommands: z.record(z.string(), projectCommandChoiceSchema).default({}),
28
+ /**
29
+ * Keyed by the *real path* of a project-declared `additionalDirectories` entry. The declared
30
+ * text is what the repository controls; the real path is what the file tools would serve, and
31
+ * so what the user is approving.
32
+ */
33
+ projectDirectories: z.record(z.string(), projectDirectoryChoiceSchema).default({}),
34
+ /**
35
+ * Keyed by provider id, carrying the fingerprint of the declared type, origin and `apiKey`
36
+ * reference the decision was made against. The id `.bookrc.json` holds the decision about a
37
+ * legacy `baseUrl`, which is not a provider but redirects the same credential.
38
+ */
39
+ workspaceProviders: z.record(z.string(), workspaceProviderChoiceSchema).default({})
26
40
  });
27
41
  function emptyWorkspaceTrust() {
28
- return { permissionAllowRules: {}, mcpServers: {}, hookEntries: {}, projectCommands: {} };
42
+ return {
43
+ permissionAllowRules: {},
44
+ mcpServers: {},
45
+ hookEntries: {},
46
+ projectCommands: {},
47
+ projectDirectories: {},
48
+ workspaceProviders: {}
49
+ };
29
50
  }
30
51
  function defaultTrustStorePath(home) {
31
52
  return home ? join(home, ".book", "trust.json") : join(resolveBookHome(), "trust.json");
@@ -95,4 +116,4 @@ export {
95
116
  loadWorkspaceTrust,
96
117
  updateWorkspaceTrust
97
118
  };
98
- //# sourceMappingURL=chunk-DGMQEI6Y.js.map
119
+ //# sourceMappingURL=chunk-HFB74CLM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workspace-trust.ts"],"sourcesContent":["/**\n * Trust decisions the user makes about repository-controlled input.\n *\n * Book gates four kinds of repository-declared configuration behind a one-time\n * per-workspace decision: `permissions.allow` rules, MCP servers, hook entries,\n * and shell substitution in a project `.book/commands/*.md` body. Each gate is\n * only as strong as the store its decisions live in.\n *\n * That store used to be `<workspace>/.book/settings.local.json`, on the\n * reasoning that the file is gitignored and only Book writes it. `.gitignore`\n * does not stop a *tracked* file from reaching a clone: `git add -f\n * .book/settings.local.json` ships it with the repository, and every\n * fingerprint the store is keyed by is a digest of configuration the repository\n * already controls. A hostile project could therefore precompute its own\n * approvals and arrive pre-trusted, releasing its hooks on first run.\n *\n * Approval data lives outside the workspace instead, in\n * `<BOOK_HOME>/trust.json`, keyed by absolute workspace path. Nothing a\n * repository can write reaches it, so the gated party cannot self-approve\n * whatever it does to the files it ships.\n *\n * Reads are fail-closed: an unparseable or off-schema store resolves to \"no\n * decisions recorded\", which withholds the gated input rather than releasing\n * it. Writes refuse to touch a store they could not parse, so a corrupt or\n * newer-versioned file is reported rather than silently overwritten.\n */\nimport { existsSync, readFileSync } from 'fs';\nimport { join, resolve } from 'path';\nimport { z } from 'zod';\nimport { resolveBookHome } from './book-home.js';\nimport { writeFileAtomic } from './settings-repository.js';\nimport {\n mcpProjectServerChoiceSchema,\n projectAllowRuleChoiceSchema,\n projectCommandChoiceSchema,\n projectDirectoryChoiceSchema,\n projectHookChoiceSchema,\n workspaceProviderChoiceSchema,\n} from './settings.js';\n\n/**\n * Bumped only for a format change that older Book versions cannot read.\n *\n * v2 added `projectCommands`. The addition is *readable* by a v1 build, which\n * is exactly the problem: `updateWorkspaceTrust` writes the schema-parsed\n * value, and `z.object` strips unknown keys, so a v1 build recording any hook,\n * rule, or MCP decision for a workspace would silently drop that workspace's\n * command approvals on the way out. Refusing the whole store is the louder and\n * safer failure — a v1 build reports it as unreadable, withholds every gated\n * declaration, and declines to write, rather than quietly erasing decisions.\n *\n * v3 added `projectDirectories` for the same reason and with the same\n * consequence: a v2 build that records a hook or command decision for a\n * workspace would write the store without that workspace's `additionalDirectories`\n * approvals, and a project directory the user had approved would silently go\n * back to being withheld. Bumping the version is what makes that failure loud\n * instead of silent: an older build reads a v3 store as unreadable, withholds\n * every gated declaration, and declines to write until it is upgraded.\n *\n * v4 added `workspaceProviders`, for the same reason and with the same\n * consequence: a v3 build that records any other decision for a workspace would\n * write the store without that workspace's provider approvals, and a provider\n * the user had approved would silently go back to being withheld. An older\n * build reads a v4 store as unreadable, withholds every gated declaration, and\n * declines to write until it is upgraded.\n */\nexport const TRUST_STORE_VERSION = 4;\n\nexport const workspaceTrustSchema = z.object({\n /** Keyed by the exact rule text the project layer declared. */\n permissionAllowRules: z.record(z.string(), projectAllowRuleChoiceSchema).default({}),\n /** Keyed by server name, carrying the fingerprint the decision was made against. */\n mcpServers: z.record(z.string(), mcpProjectServerChoiceSchema).default({}),\n /** Keyed by a fingerprint of `{ event, matcher, command, env }`. */\n hookEntries: z.record(z.string(), projectHookChoiceSchema).default({}),\n /** Keyed by command name, carrying the fingerprint of the shell its body runs. */\n projectCommands: z.record(z.string(), projectCommandChoiceSchema).default({}),\n /**\n * Keyed by the *real path* of a project-declared `additionalDirectories` entry. The declared\n * text is what the repository controls; the real path is what the file tools would serve, and\n * so what the user is approving.\n */\n projectDirectories: z.record(z.string(), projectDirectoryChoiceSchema).default({}),\n /**\n * Keyed by provider id, carrying the fingerprint of the declared type, origin and `apiKey`\n * reference the decision was made against. The id `.bookrc.json` holds the decision about a\n * legacy `baseUrl`, which is not a provider but redirects the same credential.\n */\n workspaceProviders: z.record(z.string(), workspaceProviderChoiceSchema).default({}),\n});\n\nexport type WorkspaceTrust = z.infer<typeof workspaceTrustSchema>;\n\nexport function emptyWorkspaceTrust(): WorkspaceTrust {\n return {\n permissionAllowRules: {},\n mcpServers: {},\n hookEntries: {},\n projectCommands: {},\n projectDirectories: {},\n workspaceProviders: {},\n };\n}\n\n/** Location of the user-global trust store. */\nexport function defaultTrustStorePath(home?: string): string {\n return home ? join(home, '.book', 'trust.json') : join(resolveBookHome(), 'trust.json');\n}\n\nfunction workspaceKey(workspace: string): string {\n const absolute = resolve(workspace);\n // Windows paths are case-insensitive: without folding, `D:\\repo` and `d:\\repo`\n // would accumulate two independent sets of decisions for one directory, and a\n // decision recorded under one spelling would read back as \"not decided\".\n return process.platform === 'win32' ? absolute.toLowerCase() : absolute;\n}\n\ninterface RawTrustStore {\n /** Other workspaces' entries, preserved verbatim so a write never drops them. */\n workspaces: Record<string, unknown>;\n}\n\ninterface RawTrustStoreRead {\n store: RawTrustStore;\n /** Set when the file exists but could not be understood; blocks writes. */\n unreadable?: string;\n}\n\nfunction readRawStore(path: string): RawTrustStoreRead {\n if (!existsSync(path)) return { store: { workspaces: {} } };\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf-8'));\n } catch (error) {\n return {\n store: { workspaces: {} },\n unreadable: `${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n };\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { store: { workspaces: {} }, unreadable: `${path} must contain a JSON object` };\n }\n\n const record = parsed as Record<string, unknown>;\n const version = typeof record.version === 'number' ? record.version : TRUST_STORE_VERSION;\n if (version > TRUST_STORE_VERSION) {\n return {\n store: { workspaces: {} },\n unreadable: `${path} was written by a newer version of Book (trust store v${version}); upgrade before recording new decisions`,\n };\n }\n\n const workspaces = record.workspaces;\n if (workspaces === undefined) return { store: { workspaces: {} } };\n if (workspaces === null || typeof workspaces !== 'object' || Array.isArray(workspaces)) {\n return { store: { workspaces: {} }, unreadable: `${path} \"workspaces\" must be an object` };\n }\n return { store: { workspaces: { ...(workspaces as Record<string, unknown>) } } };\n}\n\n/**\n * Decisions recorded for one workspace. Anything unreadable resolves to no\n * decisions, which withholds the gated input.\n */\nexport function loadWorkspaceTrust(\n workspace: string,\n storePath: string = defaultTrustStorePath(),\n): WorkspaceTrust {\n const { store } = readRawStore(storePath);\n const parsed = workspaceTrustSchema.safeParse(store.workspaces[workspaceKey(workspace)] ?? {});\n return parsed.success ? parsed.data : emptyWorkspaceTrust();\n}\n\n/**\n * Apply a decision to one workspace's record, leaving every other workspace's\n * entry byte-identical.\n */\nexport function updateWorkspaceTrust(\n workspace: string,\n mutate: (trust: WorkspaceTrust) => void,\n storePath: string = defaultTrustStorePath(),\n): { ok: boolean; error?: string } {\n const { store, unreadable } = readRawStore(storePath);\n if (unreadable) return { ok: false, error: unreadable };\n\n const key = workspaceKey(workspace);\n const existing = workspaceTrustSchema.safeParse(store.workspaces[key] ?? {});\n const candidate = existing.success ? existing.data : emptyWorkspaceTrust();\n mutate(candidate);\n\n const validated = workspaceTrustSchema.safeParse(candidate);\n if (!validated.success) return { ok: false, error: validated.error.message };\n\n const next = {\n version: TRUST_STORE_VERSION,\n workspaces: { ...store.workspaces, [key]: validated.data },\n };\n try {\n writeFileAtomic(storePath, `${JSON.stringify(next, null, 2)}\\n`);\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n return { ok: true };\n}\n"],"mappings":"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","names":[]}
@@ -14,6 +14,24 @@ var REMOVED_BLOCKS = [
14
14
  'The experimental Zero-Mem capability was removed and with it the "experimental" block. Delete it; compaction is always the production summary strategy now.'
15
15
  ]
16
16
  ];
17
+ var REMOVED_NESTED = [
18
+ [
19
+ ["memory", "requireApproval"],
20
+ "The memory inbox was removed: the model writes memory files directly. Delete memory.requireApproval; set memory.autoSave to false to make memory read-only."
21
+ ],
22
+ [
23
+ ["memory", "quarantineExternal"],
24
+ "The external-content quarantine was removed: a session that read the web or an MCP server writes memory like any other. Delete memory.quarantineExternal."
25
+ ],
26
+ [
27
+ ["memory", "extraction"],
28
+ "Post-session memory extraction was removed. Delete the memory.extraction block; it is no longer read."
29
+ ]
30
+ ];
31
+ var PERMISSION_RULE_LISTS = ["allow", "ask", "deny"];
32
+ function namesMemorySave(rule) {
33
+ return typeof rule === "string" && /^\s*MemorySave\s*(\(|$)/.test(rule);
34
+ }
17
35
  var REMOVED_AUTH_STORE_FILENAME = "auth.json";
18
36
  function removedAuthStoreNotice(path) {
19
37
  return `${path} holds an OAuth refresh token from the removed subscription login. Nothing reads it any more \u2014 delete the file, and revoke the token with the provider if it was ever used.`;
@@ -54,6 +72,24 @@ function collectRemovedSettingNotices(document) {
54
72
  for (const [key, message] of REMOVED_BLOCKS) {
55
73
  if (document[key] !== void 0) notices.push({ key, message });
56
74
  }
75
+ for (const [path, message] of REMOVED_NESTED) {
76
+ let node = document;
77
+ for (const segment of path) node = isRecord(node) ? node[segment] : void 0;
78
+ if (node !== void 0) notices.push({ key: path.join("."), message });
79
+ }
80
+ const permissions = document.permissions;
81
+ if (isRecord(permissions)) {
82
+ for (const list of PERMISSION_RULE_LISTS) {
83
+ const rules = permissions[list];
84
+ if (!Array.isArray(rules)) continue;
85
+ for (const rule of rules.filter(namesMemorySave)) {
86
+ notices.push({
87
+ key: `permissions.${list}`,
88
+ message: `The rule ${JSON.stringify(rule)} names MemorySave, which was removed: the model writes memory with Write, Edit and ApplyPatch now, so it matches nothing. To restrict memory writes, write a rule on those tools for the memory directory, or set memory.autoSave to false.`
89
+ });
90
+ }
91
+ }
92
+ }
57
93
  if (document.compactStrategy !== void 0 && document.compactStrategy !== "summary") {
58
94
  notices.push({
59
95
  key: "compactStrategy",
@@ -89,4 +125,4 @@ export {
89
125
  collectRemovedEnvNotices,
90
126
  normalizeRemovedSettings
91
127
  };
92
- //# sourceMappingURL=chunk-5RDRAO4B.js.map
128
+ //# sourceMappingURL=chunk-HOZXKOE3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/settings-removed.ts"],"sourcesContent":["/**\n * Settings that used to exist and no longer do.\n *\n * Removing a feature leaves its configuration behind on real machines, and zod\n * strips unknown keys silently — so a user who configured a removed capability\n * gets no signal at all that their setting stopped being read. Worse, a removed\n * *value* of a surviving key (`compactStrategy: \"zero-mem\"`) fails the schema\n * and takes the whole document down with a raw validator dump naming no remedy.\n *\n * This module is the one place that knows what was removed, so both failure\n * modes become a sentence that names the feature, the file, and the edit to\n * make. Notices are advisory: a stale key must not brick a working install, so\n * nothing here throws. `book doctor` prints them, and the credential error\n * consults them when the removed block was the user's only credential.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\n\n/** A removed setting found in a document, and what to tell the user about it. */\nexport interface RemovedSettingNotice {\n /** Dotted path of the offending key, e.g. `auth` or `compactStrategy`. */\n key: string;\n message: string;\n}\n\n/** Removed top-level settings blocks, with the guidance for each. */\nconst REMOVED_BLOCKS: ReadonlyArray<readonly [string, string]> = [\n [\n 'auth',\n 'Subscription authentication was removed; Book authenticates with API keys only. ' +\n 'Delete the \"auth\" block, and set BOOK_API_KEY or provider.<id>.apiKey instead.',\n ],\n [\n 'harness',\n 'The adaptive harness was removed. Delete the \"harness\" block; it is no longer read.',\n ],\n [\n 'experimental',\n 'The experimental Zero-Mem capability was removed and with it the \"experimental\" block. ' +\n 'Delete it; compaction is always the production summary strategy now.',\n ],\n];\n\n/**\n * Removed keys inside a surviving block, by their path below the document root. The auto-memory\n * rework removed the approval inbox, the external-content quarantine and post-session extraction,\n * so the three settings that tuned them are no longer read.\n */\nconst REMOVED_NESTED: ReadonlyArray<readonly [readonly string[], string]> = [\n [\n ['memory', 'requireApproval'],\n 'The memory inbox was removed: the model writes memory files directly. Delete memory.requireApproval; ' +\n 'set memory.autoSave to false to make memory read-only.',\n ],\n [\n ['memory', 'quarantineExternal'],\n 'The external-content quarantine was removed: a session that read the web or an MCP server writes memory ' +\n 'like any other. Delete memory.quarantineExternal.',\n ],\n [\n ['memory', 'extraction'],\n 'Post-session memory extraction was removed. Delete the memory.extraction block; it is no longer read.',\n ],\n];\n\n/** The permission rule lists whose entries can name a tool that no longer exists. */\nconst PERMISSION_RULE_LISTS = ['allow', 'ask', 'deny'] as const;\n\n/** A permission rule that names the removed `MemorySave` tool: bare, or with a pattern. */\nfunction namesMemorySave(rule: unknown): rule is string {\n return typeof rule === 'string' && /^\\s*MemorySave\\s*(\\(|$)/.test(rule);\n}\n\n/** The credential file `book auth login` used to write, which nothing reads any more. */\nexport const REMOVED_AUTH_STORE_FILENAME = 'auth.json';\n\n/**\n * Guidance for the orphaned credential store. The file is deliberately not\n * deleted for the user: it is theirs, it is mode 0600, and a tool that removes\n * credentials without being asked is worse than one that reports them.\n */\nexport function removedAuthStoreNotice(path: string): string {\n return (\n `${path} holds an OAuth refresh token from the removed subscription login. ` +\n 'Nothing reads it any more — delete the file, and revoke the token with the provider ' +\n 'if it was ever used.'\n );\n}\n\n/** Environment variables that no longer do anything, with their guidance. */\nconst REMOVED_ENV_VARS: ReadonlyArray<readonly [string, string]> = [\n ['BOOK_AUTH_PROFILE', 'Subscription authentication was removed; use BOOK_API_KEY.'],\n ['BOOK_AUTH_CLIENT_ID', 'Subscription authentication was removed; use BOOK_API_KEY.'],\n ['BOOK_AUTH_CLIENT_SECRET', 'Subscription authentication was removed; use BOOK_API_KEY.'],\n ['BOOK_EXPERIMENTAL_ZERO_MEM', 'Zero-Mem was removed; summary compaction is the only strategy.'],\n ['BOOK_ZERO_MEM_MODEL_CACHE', 'Zero-Mem was removed.'],\n ['BOOK_ZERO_MEM_LOCAL_FILES_ONLY', 'Zero-Mem was removed.'],\n [\n 'BOOK_COMPACT_STRATEGY',\n 'Compaction strategy is no longer selectable; summary is the only one.',\n ],\n];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Parse a settings file for inspection, or undefined if it is missing or unreadable. */\nexport function readSettingsDocumentForNotices(path: string): unknown {\n try {\n if (!existsSync(path)) return undefined;\n return JSON.parse(readFileSync(path, 'utf-8'));\n } catch {\n // A malformed file is a different problem, reported by the loader.\n return undefined;\n }\n}\n\n/**\n * Did this machine carry subscription-auth configuration that is no longer read?\n *\n * Consulted only on the \"no credential\" error path, so the file reads cost\n * nothing in the normal case.\n */\nexport function hadRemovedAuthConfiguration(\n settingsFilePaths: readonly string[],\n env: NodeJS.ProcessEnv,\n): boolean {\n if (Object.keys(env).some((key) => key.startsWith('BOOK_AUTH_'))) return true;\n return settingsFilePaths.some((path) => {\n const document = readSettingsDocumentForNotices(path);\n return isRecord(document) && document.auth !== undefined;\n });\n}\n\n/**\n * Removed settings present in one parsed settings document.\n *\n * Takes the *raw* parsed JSON rather than a validated document, because the\n * whole point is to see the keys validation is about to discard.\n */\nexport function collectRemovedSettingNotices(document: unknown): RemovedSettingNotice[] {\n if (!isRecord(document)) return [];\n const notices: RemovedSettingNotice[] = [];\n for (const [key, message] of REMOVED_BLOCKS) {\n if (document[key] !== undefined) notices.push({ key, message });\n }\n for (const [path, message] of REMOVED_NESTED) {\n let node: unknown = document;\n for (const segment of path) node = isRecord(node) ? node[segment] : undefined;\n if (node !== undefined) notices.push({ key: path.join('.'), message });\n }\n const permissions = document.permissions;\n if (isRecord(permissions)) {\n for (const list of PERMISSION_RULE_LISTS) {\n const rules = permissions[list];\n if (!Array.isArray(rules)) continue;\n for (const rule of rules.filter(namesMemorySave)) {\n notices.push({\n key: `permissions.${list}`,\n message:\n `The rule ${JSON.stringify(rule)} names MemorySave, which was removed: the model writes memory with ` +\n 'Write, Edit and ApplyPatch now, so it matches nothing. To restrict memory writes, ' +\n 'write a rule on those tools for the memory directory, or set memory.autoSave to false.',\n });\n }\n }\n }\n // A removed *value* of a surviving key. `compactStrategy` still exists and is\n // now `\"summary\"` only, so the old Zero-Mem selector has to be named\n // explicitly or the user is left reading a schema error about a literal type.\n if (document.compactStrategy !== undefined && document.compactStrategy !== 'summary') {\n notices.push({\n key: 'compactStrategy',\n message:\n `compactStrategy: ${JSON.stringify(document.compactStrategy)} is no longer a choice. ` +\n 'Zero-Mem was removed and summary compaction is the only strategy; delete the key.',\n });\n }\n return notices;\n}\n\n/** Removed environment variables that are set in `env`, with their guidance. */\nexport function collectRemovedEnvNotices(env: NodeJS.ProcessEnv): RemovedSettingNotice[] {\n const notices: RemovedSettingNotice[] = [];\n for (const [name, message] of REMOVED_ENV_VARS) {\n // The client-id variables are per-profile suffixed, so match by prefix.\n const matches =\n name.endsWith('CLIENT_ID') || name.endsWith('CLIENT_SECRET')\n ? Object.keys(env).filter((key) => key.startsWith(`${name}_`) || key === name)\n : env[name] !== undefined\n ? [name]\n : [];\n for (const key of matches) notices.push({ key, message });\n }\n return notices;\n}\n\n/**\n * Drop removed keys and coerce removed values so a stale document still loads.\n *\n * Unknown blocks are stripped by validation anyway; the value coercion is the\n * part that matters, because without it one obsolete string fails the whole\n * file and Book will not start. Returns a copy — the caller's document is left\n * alone so it can still be reported on.\n */\nexport function normalizeRemovedSettings(document: unknown): unknown {\n if (!isRecord(document)) return document;\n if (document.compactStrategy === undefined || document.compactStrategy === 'summary') {\n return document;\n }\n const normalized = { ...document };\n delete normalized.compactStrategy;\n return normalized;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,36 @@
1
+ import {
2
+ resolveBookHome
3
+ } from "./chunk-ZPSZZUWF.js";
4
+
5
+ // src/workspace-files.ts
6
+ import { lstatSync } from "fs";
7
+ import { basename, dirname, join, resolve } from "path";
8
+ function safeMessage(error) {
9
+ return error instanceof Error ? error.message : String(error);
10
+ }
11
+ function projectFileWriteRefusal(path) {
12
+ const target = resolve(path);
13
+ const home = resolve(resolveBookHome());
14
+ if (target === join(home, "settings.json") || target === join(home, "mcp.json")) {
15
+ return void 0;
16
+ }
17
+ try {
18
+ const folder = dirname(target);
19
+ if (basename(folder) === ".book" && folder !== home) {
20
+ const folderStats = lstatSync(folder, { throwIfNoEntry: false });
21
+ if (folderStats && !folderStats.isDirectory()) {
22
+ return "its .book folder is not a regular directory, so Book left it unchanged";
23
+ }
24
+ }
25
+ const stats = lstatSync(target, { throwIfNoEntry: false });
26
+ if (!stats || stats.isFile()) return void 0;
27
+ } catch (error) {
28
+ return `could not be inspected (${safeMessage(error)}), so Book left it unchanged`;
29
+ }
30
+ return "not a regular file, so Book left it unchanged";
31
+ }
32
+
33
+ export {
34
+ projectFileWriteRefusal
35
+ };
36
+ //# sourceMappingURL=chunk-IDYWN37T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workspace-files.ts"],"sourcesContent":["/**\n * The rule for the files Book itself writes into a workspace. Its own module, with nothing but\n * `fs`, `path` and `BOOK_HOME` behind it, so the debug logger can use it too.\n */\nimport { lstatSync } from 'fs';\nimport { basename, dirname, join, resolve } from 'path';\nimport { resolveBookHome } from './book-home.js';\n\nfunction safeMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Why Book will not write the workspace file at `path`, or undefined when it may: the one check\n * every one of Book's own workspace writes goes through (`.book/settings.json`,\n * `.book/settings.local.json`, `.book/migrations.json`, a project `.mcp.json`, `/export`, the\n * default `.book/debug.log`), including the read a write starts from.\n *\n * Such a file is written only while it is a regular file or does not exist yet, in a `.book`\n * folder that is a real directory or does not exist yet. Anything else (a link, a FIFO, a device,\n * a directory in the file's place) is left exactly as it is: not read for the write, not written\n * through, and not replaced. The answer is the reason, without the path or a full stop, for the\n * caller's one-line notice: `<path>: <reason>`.\n *\n * The user-global files (`<BOOK_HOME>/settings.json` and `<BOOK_HOME>/mcp.json`) keep their\n * behaviour: they are the user's own, and one kept elsewhere and linked into place is a setup this\n * must not break. Only those two paths are exempt, not the rest of `<BOOK_HOME>`, which also holds\n * checkouts of other repositories.\n */\nexport function projectFileWriteRefusal(path: string): string | undefined {\n const target = resolve(path);\n const home = resolve(resolveBookHome());\n if (target === join(home, 'settings.json') || target === join(home, 'mcp.json')) {\n return undefined;\n }\n try {\n const folder = dirname(target);\n // `<BOOK_HOME>` itself (a workspace at `$HOME`, where `.book` is it) is the user's own folder,\n // and a dotfiles setup often links it into place.\n if (basename(folder) === '.book' && folder !== home) {\n const folderStats = lstatSync(folder, { throwIfNoEntry: false });\n if (folderStats && !folderStats.isDirectory()) {\n return 'its .book folder is not a regular directory, so Book left it unchanged';\n }\n }\n const stats = lstatSync(target, { throwIfNoEntry: false });\n if (!stats || stats.isFile()) return undefined;\n } catch (error) {\n return `could not be inspected (${safeMessage(error)}), so Book left it unchanged`;\n }\n return 'not a regular file, so Book left it unchanged';\n}\n"],"mappings":";;;;;AAIA,SAAS,iBAAiB;AAC1B,SAAS,UAAU,SAAS,MAAM,eAAe;AAGjD,SAAS,YAAY,OAAwB;AAC3C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAmBO,SAAS,wBAAwB,MAAkC;AACxE,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,OAAO,QAAQ,gBAAgB,CAAC;AACtC,MAAI,WAAW,KAAK,MAAM,eAAe,KAAK,WAAW,KAAK,MAAM,UAAU,GAAG;AAC/E,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,SAAS,QAAQ,MAAM;AAG7B,QAAI,SAAS,MAAM,MAAM,WAAW,WAAW,MAAM;AACnD,YAAM,cAAc,UAAU,QAAQ,EAAE,gBAAgB,MAAM,CAAC;AAC/D,UAAI,eAAe,CAAC,YAAY,YAAY,GAAG;AAC7C,eAAO;AAAA,MACT;AAAA,IACF;AACA,UAAM,QAAQ,UAAU,QAAQ,EAAE,gBAAgB,MAAM,CAAC;AACzD,QAAI,CAAC,SAAS,MAAM,OAAO,EAAG,QAAO;AAAA,EACvC,SAAS,OAAO;AACd,WAAO,2BAA2B,YAAY,KAAK,CAAC;AAAA,EACtD;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,273 @@
1
+ // src/types/theme.ts
2
+ var DEFAULT_THEME = {
3
+ brand: "#5E5CE6",
4
+ brandShimmer: "#8E8CFF",
5
+ text: "#F5F5F7",
6
+ inverseText: "#111113",
7
+ inactive: "#6E6E73",
8
+ subtle: "#98989D",
9
+ suggestion: "#8E8E93",
10
+ permission: "#FF9F0A",
11
+ remember: "#BF5AF2",
12
+ surface: "#1C1C1E",
13
+ surfaceActive: "#2C2C2E",
14
+ border: "#3A3A3C",
15
+ selectionText: "#FFFFFF",
16
+ userAccent: "#0A84FF",
17
+ assistantAccent: "#64D2FF",
18
+ toolRail: "#636366",
19
+ success: "#30D158",
20
+ error: "#FF453A",
21
+ warning: "#FF9F0A",
22
+ merged: "#66D4CF",
23
+ promptBorder: "#0A84FF",
24
+ planMode: "#BF5AF2",
25
+ autoAccept: "#30D158",
26
+ bashBorder: "#FF9F0A",
27
+ // `default` is the quiet mode: neutral grey, so an ordinary session carries
28
+ // no permission-mode signal at all.
29
+ modeDefault: "#98989D",
30
+ modePlan: "#BF5AF2",
31
+ modeAcceptEdits: "#30D158",
32
+ modeAuto: "#66D4CF",
33
+ modeDontAsk: "#FF453A",
34
+ modeBypass: "#FF9F0A",
35
+ diffAdded: "#173A28",
36
+ diffRemoved: "#3A1E22",
37
+ diffAddedWord: "#24633D",
38
+ diffRemovedWord: "#6B2932",
39
+ diffAddedDimmed: "#122B1E",
40
+ diffRemovedDimmed: "#2B181C",
41
+ usageMeter: "#66D4CF",
42
+ usageMeterHigh: "#FF9F0A",
43
+ usageMeterCritical: "#FF453A",
44
+ // The spinner is the agent speaking, so it keeps the cyan identity.
45
+ shimmerPair: ["#64D2FF", "#A5E3FF"],
46
+ subagentColors: [
47
+ "#FF453A",
48
+ "#FF9F0A",
49
+ "#FFD60A",
50
+ "#30D158",
51
+ "#64D2FF",
52
+ "#0A84FF",
53
+ "#BF5AF2",
54
+ "#5E5CE6"
55
+ ],
56
+ mdCodeBackground: "#161618",
57
+ mdCodeBorder: "#3A3A3C",
58
+ mdCodeText: "#F5F5F7",
59
+ mdCodeKeyword: "#BF5AF2",
60
+ mdCodeString: "#30D158",
61
+ mdCodeComment: "#6E6E73",
62
+ mdCodeNumber: "#FF9F0A",
63
+ mdCodeFunction: "#64D2FF",
64
+ mdCodeLineNumber: "#636366",
65
+ mdInlineCodeBg: "#2C2C2E",
66
+ mdInlineCodeText: "#66D4CF",
67
+ // A three-step brightness ramp, all bold. Depth is legible only if these
68
+ // differ from each other *and* from `text` — otherwise a heading is
69
+ // indistinguishable from a bold run in body copy.
70
+ mdHeadingH1: "#FFFFFF",
71
+ mdHeadingH2: "#E5E5EA",
72
+ mdHeading: "#AEAEB2",
73
+ mdBlockquoteBorder: "#636366",
74
+ mdBlockquoteText: "#98989D",
75
+ mdLink: "#66D4CF",
76
+ mdListMarker: "#0A84FF",
77
+ mdHr: "#3A3C36",
78
+ mdTableBorder: "#3A3C36",
79
+ mdThinkBg: "#161618",
80
+ mdThinkBorder: "#3A3C36",
81
+ mdThinkText: "#8E8E93",
82
+ mdTurnSeparator: "#3A3A3C",
83
+ mdCheckboxChecked: "#30D158",
84
+ mdCheckboxUnchecked: "#6E6E73",
85
+ userBg: "#202022",
86
+ userText: "#FFFFFF"
87
+ };
88
+ var FOLIO_THEME = {
89
+ brand: "#D9B36C",
90
+ brandShimmer: "#F2D9A0",
91
+ text: "#E8E3D9",
92
+ inverseText: "#15140F",
93
+ inactive: "#6F6A61",
94
+ subtle: "#A09A8F",
95
+ suggestion: "#8F897E",
96
+ permission: "#E5894F",
97
+ remember: "#B8A1D9",
98
+ surface: "#1D1C1A",
99
+ surfaceActive: "#2B2926",
100
+ border: "#3B3834",
101
+ selectionText: "#F6F1E7",
102
+ userAccent: "#D9B36C",
103
+ assistantAccent: "#D9B36C",
104
+ toolRail: "#4D4943",
105
+ success: "#93B97F",
106
+ error: "#E0675C",
107
+ warning: "#E5894F",
108
+ merged: "#8EBBB5",
109
+ // The composer border is a hairline, not a signal. Its accent lives on the
110
+ // prompt glyph (`userAccent`).
111
+ promptBorder: "#4D4943",
112
+ planMode: "#B8A1D9",
113
+ autoAccept: "#93B97F",
114
+ bashBorder: "#E5894F",
115
+ modeDefault: "#A09A8F",
116
+ modePlan: "#B8A1D9",
117
+ modeAcceptEdits: "#93B97F",
118
+ modeAuto: "#8EBBB5",
119
+ modeDontAsk: "#E0675C",
120
+ modeBypass: "#E5894F",
121
+ diffAdded: "#1F2E20",
122
+ diffRemoved: "#3A2220",
123
+ diffAddedWord: "#2F4D2F",
124
+ diffRemovedWord: "#5E302B",
125
+ diffAddedDimmed: "#18231A",
126
+ diffRemovedDimmed: "#2A1A18",
127
+ usageMeter: "#8EBBB5",
128
+ usageMeterHigh: "#E5894F",
129
+ usageMeterCritical: "#E0675C",
130
+ shimmerPair: ["#D9B36C", "#F2D9A0"],
131
+ subagentColors: [
132
+ "#E0675C",
133
+ "#E5894F",
134
+ "#D9B36C",
135
+ "#93B97F",
136
+ "#8EBBB5",
137
+ "#8FAFD6",
138
+ "#B8A1D9",
139
+ "#D59AB4"
140
+ ],
141
+ mdCodeBackground: "#191816",
142
+ mdCodeBorder: "#3B3834",
143
+ mdCodeText: "#E8E3D9",
144
+ mdCodeKeyword: "#C4A3D6",
145
+ mdCodeString: "#A7C48B",
146
+ mdCodeComment: "#6F6A61",
147
+ mdCodeNumber: "#E3A774",
148
+ mdCodeFunction: "#8FB4D9",
149
+ mdCodeLineNumber: "#55514A",
150
+ mdInlineCodeBg: "#2B2926",
151
+ mdInlineCodeText: "#9FC3BE",
152
+ // Gilt for the two headings an answer actually uses, ivory-grey below them.
153
+ mdHeadingH1: "#F2D9A0",
154
+ mdHeadingH2: "#D9B36C",
155
+ mdHeading: "#BDB6AA",
156
+ mdBlockquoteBorder: "#5A554D",
157
+ mdBlockquoteText: "#A09A8F",
158
+ mdLink: "#9FC3BE",
159
+ mdListMarker: "#D9B36C",
160
+ mdHr: "#3B3834",
161
+ mdTableBorder: "#5A554D",
162
+ mdThinkBg: "#191816",
163
+ mdThinkBorder: "#3B3834",
164
+ mdThinkText: "#8F897E",
165
+ mdTurnSeparator: "#3B3834",
166
+ mdCheckboxChecked: "#93B97F",
167
+ mdCheckboxUnchecked: "#6F6A61",
168
+ userBg: "#24221E",
169
+ userText: "#F6F1E7"
170
+ };
171
+ var RUBRIC_THEME = {
172
+ ...FOLIO_THEME,
173
+ brand: "#E4573D",
174
+ brandShimmer: "#F28A6E",
175
+ userAccent: "#E4573D",
176
+ // The agent's own voice is ink, not rubric.
177
+ assistantAccent: "#F0EBE1",
178
+ shimmerPair: ["#F0EBE1", "#A09A8F"],
179
+ error: "#EF6F95",
180
+ warning: "#E3A84E",
181
+ permission: "#E3A84E",
182
+ bashBorder: "#E3A84E",
183
+ modeBypass: "#E3A84E",
184
+ modeDontAsk: "#EF6F95",
185
+ usageMeterHigh: "#E3A84E",
186
+ usageMeterCritical: "#EF6F95",
187
+ // The ink ramp, re-tuned for a long read. Folio's body is a warm ivory at
188
+ // roughly 15:1 on a near-black terminal, which glares; Rubric's body comes
189
+ // down one step to about 12:1, so the headings above it — and your own
190
+ // prompt — have somewhere to stand. Inline code and links take a warm tone
191
+ // rather than Folio's cool one, which sat beside the cinnabar as a third hue
192
+ // in a two-colour palette, and `inactive` clears 4.5:1 so a dimmed row is
193
+ // still readable rather than merely present.
194
+ text: "#CFC9BE",
195
+ mdCodeText: "#CFC9BE",
196
+ mdHeadingH1: "#F4EFE6",
197
+ mdHeadingH2: "#EFE9DE",
198
+ mdHeading: "#E2DCD0",
199
+ mdInlineCodeText: "#CDB791",
200
+ mdLink: "#CDB791",
201
+ inactive: "#857F74",
202
+ userText: "#F4EFE6",
203
+ mdListMarker: "#E4573D",
204
+ subagentColors: [
205
+ "#E4573D",
206
+ "#E3A84E",
207
+ "#D9B36C",
208
+ "#93B97F",
209
+ "#8EBBB5",
210
+ "#8FAFD6",
211
+ "#B8A1D9",
212
+ "#EF6F95"
213
+ ]
214
+ };
215
+
216
+ // src/tui/theme.ts
217
+ import { createContext, useContext } from "react";
218
+ import { readFileSync, existsSync, readdirSync } from "fs";
219
+ import { join } from "path";
220
+ var APPLE_THEME = { ...DEFAULT_THEME };
221
+ var DEFAULT_THEME_NAME = "rubric";
222
+ var CUSTOM_THEME_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
223
+ var ThemeContext = createContext(DEFAULT_THEME);
224
+ function useTheme() {
225
+ return useContext(ThemeContext);
226
+ }
227
+ function listCustomThemes(workspace) {
228
+ const themesDir = join(workspace, ".book", "themes");
229
+ try {
230
+ return readdirSync(themesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length)).filter((name) => CUSTOM_THEME_NAME.test(name)).sort((a, b) => a.localeCompare(b));
231
+ } catch {
232
+ return [];
233
+ }
234
+ }
235
+ function loadCustomTheme(workspace, name) {
236
+ if (!CUSTOM_THEME_NAME.test(name)) return null;
237
+ const themePath = join(workspace, ".book", "themes", `${name}.json`);
238
+ if (!existsSync(themePath)) return null;
239
+ try {
240
+ const raw = readFileSync(themePath, "utf-8");
241
+ const parsed = JSON.parse(raw);
242
+ const userText = parsed.userText ?? parsed.text ?? RUBRIC_THEME.userText;
243
+ return { ...RUBRIC_THEME, ...parsed, userText };
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ function resolveTheme(workspace, preference) {
249
+ const requested = preference.trim();
250
+ const builtin = requested.toLowerCase();
251
+ if (builtin === "apple" || builtin === "apple-dark") {
252
+ return { preference: "apple", resolvedName: "apple", tokens: APPLE_THEME };
253
+ }
254
+ if (builtin === "folio") {
255
+ return { preference: "folio", resolvedName: "folio", tokens: FOLIO_THEME };
256
+ }
257
+ if (builtin === "rubric") {
258
+ return { preference: "rubric", resolvedName: "rubric", tokens: RUBRIC_THEME };
259
+ }
260
+ if (!requested) return null;
261
+ const custom = loadCustomTheme(workspace, requested);
262
+ return custom ? { preference: requested, resolvedName: requested, tokens: custom } : null;
263
+ }
264
+
265
+ export {
266
+ RUBRIC_THEME,
267
+ DEFAULT_THEME_NAME,
268
+ ThemeContext,
269
+ useTheme,
270
+ listCustomThemes,
271
+ resolveTheme
272
+ };
273
+ //# sourceMappingURL=chunk-J4F2QLM2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/theme.ts","../src/tui/theme.ts"],"sourcesContent":["/**\n * Theme token system matching Claude Code's color token architecture.\n * All values are Ink-compatible color strings (named colors, hex, rgb, ansi256, ansi:<name>).\n */\nexport interface ThemeTokens {\n /** Identity */\n brand: string;\n brandShimmer: string;\n\n /** Text */\n text: string;\n inverseText: string;\n inactive: string;\n subtle: string;\n suggestion: string;\n permission: string;\n remember: string;\n\n /** Shared TUI chrome */\n /**\n * Panel fill.\n *\n * Built-in components fill only *verbatim* content — a code block keeps\n * `mdCodeBackground`. Secondary content (expanded tool output, reasoning)\n * gets a left rail and nothing else, so no built-in currently renders this\n * token or `mdThinkBg`. Both stay in the contract for custom themes in\n * `.book/themes/*.json`.\n */\n surface: string;\n surfaceActive: string;\n border: string;\n selectionText: string;\n userAccent: string;\n assistantAccent: string;\n toolRail: string;\n\n /** Status */\n success: string;\n error: string;\n warning: string;\n merged: string;\n\n /** Mode borders */\n promptBorder: string;\n planMode: string;\n autoAccept: string;\n bashBorder: string;\n\n /** Permission mode colors (one per mode) */\n modeDefault: string;\n modePlan: string;\n modeAcceptEdits: string;\n modeAuto: string;\n modeDontAsk: string;\n modeBypass: string;\n\n /** Diff rendering */\n diffAdded: string;\n diffRemoved: string;\n diffAddedWord: string;\n diffRemovedWord: string;\n diffAddedDimmed: string;\n diffRemovedDimmed: string;\n\n /** Usage meter */\n usageMeter: string;\n usageMeterHigh: string;\n usageMeterCritical: string;\n\n /** Shimmer pairs for animated gradients */\n shimmerPair: [string, string];\n\n /** Subagent colors (8 named colors) */\n subagentColors: string[];\n\n /** Markdown rendering */\n mdCodeBackground: string;\n mdCodeBorder: string;\n mdCodeText: string;\n mdCodeKeyword: string;\n mdCodeString: string;\n mdCodeComment: string;\n mdCodeNumber: string;\n mdCodeFunction: string;\n mdCodeLineNumber: string;\n /**\n * Retained for custom theme files that still set it; inline code renders\n * with `mdInlineCodeText` alone and never paints a background.\n */\n mdInlineCodeBg: string;\n mdInlineCodeText: string;\n mdHeading: string;\n mdHeadingH1: string;\n mdHeadingH2: string;\n mdBlockquoteBorder: string;\n mdBlockquoteText: string;\n mdLink: string;\n mdListMarker: string;\n mdHr: string;\n mdTableBorder: string;\n mdThinkBg: string;\n mdThinkBorder: string;\n mdThinkText: string;\n mdTurnSeparator: string;\n mdCheckboxChecked: string;\n mdCheckboxUnchecked: string;\n\n /** User message background */\n userBg: string;\n /** Your own prompt in the transcript: the brightest ink, so each turn is a landmark. */\n userText: string;\n}\n\n/**\n * Apple-inspired default palette: near-black neutral surfaces, bright grey\n * text, and one blue action accent. Every other hue is a system colour that\n * appears only when a state needs attention, so ordinary chrome never competes\n * with the work.\n *\n * Roles stay distinct: blue is the user and the composer (the things you act\n * on), cyan is the agent speaking, indigo is product chrome, teal carries\n * references, and the orange/red/green trio is status.\n */\nexport const DEFAULT_THEME: ThemeTokens = {\n brand: '#5E5CE6',\n brandShimmer: '#8E8CFF',\n\n text: '#F5F5F7',\n inverseText: '#111113',\n inactive: '#6E6E73',\n subtle: '#98989D',\n suggestion: '#8E8E93',\n permission: '#FF9F0A',\n remember: '#BF5AF2',\n\n surface: '#1C1C1E',\n surfaceActive: '#2C2C2E',\n border: '#3A3A3C',\n selectionText: '#FFFFFF',\n userAccent: '#0A84FF',\n assistantAccent: '#64D2FF',\n toolRail: '#636366',\n\n success: '#30D158',\n error: '#FF453A',\n warning: '#FF9F0A',\n merged: '#66D4CF',\n\n promptBorder: '#0A84FF',\n planMode: '#BF5AF2',\n autoAccept: '#30D158',\n bashBorder: '#FF9F0A',\n\n // `default` is the quiet mode: neutral grey, so an ordinary session carries\n // no permission-mode signal at all.\n modeDefault: '#98989D',\n modePlan: '#BF5AF2',\n modeAcceptEdits: '#30D158',\n modeAuto: '#66D4CF',\n modeDontAsk: '#FF453A',\n modeBypass: '#FF9F0A',\n\n diffAdded: '#173A28',\n diffRemoved: '#3A1E22',\n diffAddedWord: '#24633D',\n diffRemovedWord: '#6B2932',\n diffAddedDimmed: '#122B1E',\n diffRemovedDimmed: '#2B181C',\n\n usageMeter: '#66D4CF',\n usageMeterHigh: '#FF9F0A',\n usageMeterCritical: '#FF453A',\n\n // The spinner is the agent speaking, so it keeps the cyan identity.\n shimmerPair: ['#64D2FF', '#A5E3FF'],\n\n subagentColors: [\n '#FF453A',\n '#FF9F0A',\n '#FFD60A',\n '#30D158',\n '#64D2FF',\n '#0A84FF',\n '#BF5AF2',\n '#5E5CE6',\n ],\n\n mdCodeBackground: '#161618',\n mdCodeBorder: '#3A3A3C',\n mdCodeText: '#F5F5F7',\n mdCodeKeyword: '#BF5AF2',\n mdCodeString: '#30D158',\n mdCodeComment: '#6E6E73',\n mdCodeNumber: '#FF9F0A',\n mdCodeFunction: '#64D2FF',\n mdCodeLineNumber: '#636366',\n mdInlineCodeBg: '#2C2C2E',\n mdInlineCodeText: '#66D4CF',\n // A three-step brightness ramp, all bold. Depth is legible only if these\n // differ from each other *and* from `text` — otherwise a heading is\n // indistinguishable from a bold run in body copy.\n mdHeadingH1: '#FFFFFF',\n mdHeadingH2: '#E5E5EA',\n mdHeading: '#AEAEB2',\n mdBlockquoteBorder: '#636366',\n mdBlockquoteText: '#98989D',\n mdLink: '#66D4CF',\n mdListMarker: '#0A84FF',\n mdHr: '#3A3C36',\n mdTableBorder: '#3A3C36',\n mdThinkBg: '#161618',\n mdThinkBorder: '#3A3C36',\n mdThinkText: '#8E8E93',\n mdTurnSeparator: '#3A3A3C',\n mdCheckboxChecked: '#30D158',\n mdCheckboxUnchecked: '#6E6E73',\n\n userBg: '#202022',\n userText: '#FFFFFF',\n};\n\n/**\n * Folio: ink on dark paper, with one gilt accent.\n *\n * Where the Apple palette gives every role its own system hue, Folio leans on\n * value and weight. Text is a warm ivory, secondary text steps down through\n * warm greys, and a single gold, like the gilt title on a spine, marks the few\n * places you act or look first: the prompt glyph, your own turns, headings,\n * list markers and the spinner. Status colours are muted versions of the usual\n * trio (sage, brick, burnt orange). They still read at a glance, but they no\n * longer shout over the prose around them.\n */\nexport const FOLIO_THEME: ThemeTokens = {\n brand: '#D9B36C',\n brandShimmer: '#F2D9A0',\n\n text: '#E8E3D9',\n inverseText: '#15140F',\n inactive: '#6F6A61',\n subtle: '#A09A8F',\n suggestion: '#8F897E',\n permission: '#E5894F',\n remember: '#B8A1D9',\n\n surface: '#1D1C1A',\n surfaceActive: '#2B2926',\n border: '#3B3834',\n selectionText: '#F6F1E7',\n userAccent: '#D9B36C',\n assistantAccent: '#D9B36C',\n toolRail: '#4D4943',\n\n success: '#93B97F',\n error: '#E0675C',\n warning: '#E5894F',\n merged: '#8EBBB5',\n\n // The composer border is a hairline, not a signal. Its accent lives on the\n // prompt glyph (`userAccent`).\n promptBorder: '#4D4943',\n planMode: '#B8A1D9',\n autoAccept: '#93B97F',\n bashBorder: '#E5894F',\n\n modeDefault: '#A09A8F',\n modePlan: '#B8A1D9',\n modeAcceptEdits: '#93B97F',\n modeAuto: '#8EBBB5',\n modeDontAsk: '#E0675C',\n modeBypass: '#E5894F',\n\n diffAdded: '#1F2E20',\n diffRemoved: '#3A2220',\n diffAddedWord: '#2F4D2F',\n diffRemovedWord: '#5E302B',\n diffAddedDimmed: '#18231A',\n diffRemovedDimmed: '#2A1A18',\n\n usageMeter: '#8EBBB5',\n usageMeterHigh: '#E5894F',\n usageMeterCritical: '#E0675C',\n\n shimmerPair: ['#D9B36C', '#F2D9A0'],\n\n subagentColors: [\n '#E0675C',\n '#E5894F',\n '#D9B36C',\n '#93B97F',\n '#8EBBB5',\n '#8FAFD6',\n '#B8A1D9',\n '#D59AB4',\n ],\n\n mdCodeBackground: '#191816',\n mdCodeBorder: '#3B3834',\n mdCodeText: '#E8E3D9',\n mdCodeKeyword: '#C4A3D6',\n mdCodeString: '#A7C48B',\n mdCodeComment: '#6F6A61',\n mdCodeNumber: '#E3A774',\n mdCodeFunction: '#8FB4D9',\n mdCodeLineNumber: '#55514A',\n mdInlineCodeBg: '#2B2926',\n mdInlineCodeText: '#9FC3BE',\n // Gilt for the two headings an answer actually uses, ivory-grey below them.\n mdHeadingH1: '#F2D9A0',\n mdHeadingH2: '#D9B36C',\n mdHeading: '#BDB6AA',\n mdBlockquoteBorder: '#5A554D',\n mdBlockquoteText: '#A09A8F',\n mdLink: '#9FC3BE',\n mdListMarker: '#D9B36C',\n mdHr: '#3B3834',\n mdTableBorder: '#5A554D',\n mdThinkBg: '#191816',\n mdThinkBorder: '#3B3834',\n mdThinkText: '#8F897E',\n mdTurnSeparator: '#3B3834',\n mdCheckboxChecked: '#93B97F',\n mdCheckboxUnchecked: '#6F6A61',\n\n userBg: '#24221E',\n userText: '#F6F1E7',\n};\n\n/**\n * Rubric: two-colour printing, the way manuscripts were rubricated.\n *\n * The body is set in ink (Folio's warm ivory and greys). One cinnabar red is\n * reserved for the marks a reader navigates by: the pilcrow that opens each of\n * your turns and the composer, the section sign before a heading, list\n * markers, and the drop cap on an empty page. The agent speaks in ink, spinner\n * included, so red never reads as an alarm. Because red is the accent, errors\n * move to a rose that stays distinct from it, and warnings to amber.\n */\nexport const RUBRIC_THEME: ThemeTokens = {\n ...FOLIO_THEME,\n brand: '#E4573D',\n brandShimmer: '#F28A6E',\n userAccent: '#E4573D',\n // The agent's own voice is ink, not rubric.\n assistantAccent: '#F0EBE1',\n shimmerPair: ['#F0EBE1', '#A09A8F'],\n\n error: '#EF6F95',\n warning: '#E3A84E',\n permission: '#E3A84E',\n bashBorder: '#E3A84E',\n modeBypass: '#E3A84E',\n modeDontAsk: '#EF6F95',\n usageMeterHigh: '#E3A84E',\n usageMeterCritical: '#EF6F95',\n\n // The ink ramp, re-tuned for a long read. Folio's body is a warm ivory at\n // roughly 15:1 on a near-black terminal, which glares; Rubric's body comes\n // down one step to about 12:1, so the headings above it — and your own\n // prompt — have somewhere to stand. Inline code and links take a warm tone\n // rather than Folio's cool one, which sat beside the cinnabar as a third hue\n // in a two-colour palette, and `inactive` clears 4.5:1 so a dimmed row is\n // still readable rather than merely present.\n text: '#CFC9BE',\n mdCodeText: '#CFC9BE',\n mdHeadingH1: '#F4EFE6',\n mdHeadingH2: '#EFE9DE',\n mdHeading: '#E2DCD0',\n mdInlineCodeText: '#CDB791',\n mdLink: '#CDB791',\n inactive: '#857F74',\n userText: '#F4EFE6',\n\n mdListMarker: '#E4573D',\n\n subagentColors: [\n '#E4573D',\n '#E3A84E',\n '#D9B36C',\n '#93B97F',\n '#8EBBB5',\n '#8FAFD6',\n '#B8A1D9',\n '#EF6F95',\n ],\n};\n","import { createContext, useContext } from 'react';\nimport type { ThemeTokens } from '../types/theme.js';\nimport { DEFAULT_THEME, FOLIO_THEME, RUBRIC_THEME } from '../types/theme.js';\nimport { readFileSync, existsSync, readdirSync } from 'fs';\nimport { join } from 'path';\n\n/**\n * Apple-inspired default palette: near-black neutral surfaces, bright grey\n * text, and one blue action accent. Every other hue is a system colour that\n * appears only when a state needs attention, so ordinary chrome never competes\n * with the work.\n */\nexport const APPLE_THEME: ThemeTokens = { ...DEFAULT_THEME };\n\n/** Warm ink on dark paper with one gilt accent. See {@link FOLIO_THEME}. */\nexport { FOLIO_THEME };\n\n/** Ink with cinnabar marks, like a rubricated manuscript. See {@link RUBRIC_THEME}. */\nexport { RUBRIC_THEME };\n\n/** The theme a session gets when settings name none. */\nexport const DEFAULT_THEME_NAME = 'rubric';\n\nexport interface ResolvedTheme {\n preference: string;\n resolvedName: string;\n tokens: ThemeTokens;\n}\n\nconst CUSTOM_THEME_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\n/**\n * React context for theme tokens.\n * Components that render colors read from this instead of hardcoded strings,\n * so custom themes can override the palette globally.\n */\nexport const ThemeContext = createContext<ThemeTokens>(DEFAULT_THEME);\n\n/**\n * Hook to access the current theme tokens.\n */\nexport function useTheme(): ThemeTokens {\n return useContext(ThemeContext);\n}\n\n/** List safe custom theme names from .book/themes. */\nexport function listCustomThemes(workspace: string): string[] {\n const themesDir = join(workspace, '.book', 'themes');\n try {\n return readdirSync(themesDir, { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))\n .map((entry) => entry.name.slice(0, -'.json'.length))\n .filter((name) => CUSTOM_THEME_NAME.test(name))\n .sort((a, b) => a.localeCompare(b));\n } catch {\n return [];\n }\n}\n\n/**\n * Try to load a custom theme from .book/themes/<name>.json.\n * Returns the loaded theme tokens, or null if not found.\n */\nexport function loadCustomTheme(workspace: string, name: string): ThemeTokens | null {\n if (!CUSTOM_THEME_NAME.test(name)) return null;\n const themePath = join(workspace, '.book', 'themes', `${name}.json`);\n if (!existsSync(themePath)) return null;\n try {\n const raw = readFileSync(themePath, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<ThemeTokens>;\n // A custom theme overrides the default look, so it starts from Rubric.\n // A theme written before `userText` existed set its ink through `text`; the\n // prompt keeps following that ink rather than Rubric's near-white.\n const userText = parsed.userText ?? parsed.text ?? RUBRIC_THEME.userText;\n return { ...RUBRIC_THEME, ...parsed, userText };\n } catch {\n return null;\n }\n}\n\n/** Resolve a theme preference into tokens. */\nexport function resolveTheme(workspace: string, preference: string): ResolvedTheme | null {\n const requested = preference.trim();\n const builtin = requested.toLowerCase();\n if (builtin === 'apple' || builtin === 'apple-dark') {\n return { preference: 'apple', resolvedName: 'apple', tokens: APPLE_THEME };\n }\n if (builtin === 'folio') {\n return { preference: 'folio', resolvedName: 'folio', tokens: FOLIO_THEME };\n }\n if (builtin === 'rubric') {\n return { preference: 'rubric', resolvedName: 'rubric', tokens: RUBRIC_THEME };\n }\n if (!requested) return null;\n const custom = loadCustomTheme(workspace, requested);\n return custom ? { preference: requested, resolvedName: requested, tokens: custom } : null;\n}\n\nexport { DEFAULT_THEME };\nexport type { ThemeTokens };\n"],"mappings":"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","names":[]}