@agimon-ai/doompi-config 0.0.1-alpha.13 → 0.0.1-alpha.15

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 (253) hide show
  1. package/README.md +51 -14
  2. package/dist/adapters/atomicJson.cjs +2 -0
  3. package/dist/{services → adapters}/atomicJson.cjs.map +1 -1
  4. package/dist/{services → adapters}/atomicJson.d.cts +1 -1
  5. package/dist/adapters/atomicJson.d.cts.map +1 -0
  6. package/dist/{services → adapters}/atomicJson.d.mts +1 -1
  7. package/dist/adapters/atomicJson.d.mts.map +1 -0
  8. package/dist/adapters/atomicJson.mjs +2 -0
  9. package/dist/{services → adapters}/atomicJson.mjs.map +1 -1
  10. package/dist/adapters/config.cjs +1 -1
  11. package/dist/adapters/config.cjs.map +1 -1
  12. package/dist/adapters/config.d.cts +2 -16
  13. package/dist/adapters/config.d.cts.map +1 -1
  14. package/dist/adapters/config.d.mts +2 -16
  15. package/dist/adapters/config.d.mts.map +1 -1
  16. package/dist/adapters/config.mjs +1 -1
  17. package/dist/adapters/config.mjs.map +1 -1
  18. package/dist/adapters/configWriter.cjs +2 -0
  19. package/dist/adapters/configWriter.cjs.map +1 -0
  20. package/dist/{services → adapters}/configWriter.d.cts +1 -1
  21. package/dist/{services → adapters}/configWriter.d.cts.map +1 -1
  22. package/dist/{services → adapters}/configWriter.d.mts +1 -1
  23. package/dist/{services → adapters}/configWriter.d.mts.map +1 -1
  24. package/dist/adapters/configWriter.mjs +2 -0
  25. package/dist/adapters/configWriter.mjs.map +1 -0
  26. package/dist/adapters/domains.cjs +2 -0
  27. package/dist/adapters/domains.cjs.map +1 -0
  28. package/dist/adapters/domains.d.cts +25 -0
  29. package/dist/adapters/domains.d.cts.map +1 -0
  30. package/dist/adapters/domains.d.mts +25 -0
  31. package/dist/adapters/domains.d.mts.map +1 -0
  32. package/dist/adapters/domains.mjs +2 -0
  33. package/dist/adapters/domains.mjs.map +1 -0
  34. package/dist/adapters/harnessState.cjs.map +1 -0
  35. package/dist/{services → adapters}/harnessState.d.cts +2 -2
  36. package/dist/{services → adapters}/harnessState.d.cts.map +1 -1
  37. package/dist/{services → adapters}/harnessState.d.mts +2 -2
  38. package/dist/{services → adapters}/harnessState.d.mts.map +1 -1
  39. package/dist/adapters/harnessState.mjs.map +1 -0
  40. package/dist/{services → adapters}/harnessStore.cjs +1 -1
  41. package/dist/adapters/harnessStore.cjs.map +1 -0
  42. package/dist/{services → adapters}/harnessStore.d.cts +2 -2
  43. package/dist/{services → adapters}/harnessStore.d.cts.map +1 -1
  44. package/dist/{services → adapters}/harnessStore.d.mts +2 -2
  45. package/dist/{services → adapters}/harnessStore.d.mts.map +1 -1
  46. package/dist/{services → adapters}/harnessStore.mjs +1 -1
  47. package/dist/adapters/harnessStore.mjs.map +1 -0
  48. package/dist/adapters/init.cjs +70 -41
  49. package/dist/adapters/init.cjs.map +1 -1
  50. package/dist/adapters/init.d.cts +1 -1
  51. package/dist/adapters/init.d.cts.map +1 -1
  52. package/dist/adapters/init.d.mts +1 -1
  53. package/dist/adapters/init.d.mts.map +1 -1
  54. package/dist/adapters/init.mjs +70 -41
  55. package/dist/adapters/init.mjs.map +1 -1
  56. package/dist/adapters/layeredConfig.cjs +2 -0
  57. package/dist/{services → adapters}/layeredConfig.cjs.map +1 -1
  58. package/dist/{services → adapters}/layeredConfig.d.cts +1 -1
  59. package/dist/{services → adapters}/layeredConfig.d.cts.map +1 -1
  60. package/dist/{services → adapters}/layeredConfig.d.mts +1 -1
  61. package/dist/{services → adapters}/layeredConfig.d.mts.map +1 -1
  62. package/dist/adapters/layeredConfig.mjs +2 -0
  63. package/dist/{services → adapters}/layeredConfig.mjs.map +1 -1
  64. package/dist/adapters/majorModes.cjs +2 -0
  65. package/dist/adapters/majorModes.cjs.map +1 -0
  66. package/dist/{services → adapters}/majorModes.d.cts +18 -16
  67. package/dist/adapters/majorModes.d.cts.map +1 -0
  68. package/dist/{services → adapters}/majorModes.d.mts +18 -16
  69. package/dist/adapters/majorModes.d.mts.map +1 -0
  70. package/dist/adapters/majorModes.mjs +2 -0
  71. package/dist/adapters/majorModes.mjs.map +1 -0
  72. package/dist/adapters/pi/configExtension.cjs +2 -0
  73. package/dist/adapters/pi/configExtension.cjs.map +1 -0
  74. package/dist/adapters/pi/configExtension.d.cts +8 -0
  75. package/dist/adapters/pi/configExtension.d.cts.map +1 -0
  76. package/dist/adapters/pi/configExtension.d.mts +8 -0
  77. package/dist/adapters/pi/configExtension.d.mts.map +1 -0
  78. package/dist/adapters/pi/configExtension.mjs +2 -0
  79. package/dist/adapters/pi/configExtension.mjs.map +1 -0
  80. package/dist/adapters/pi/doomConfigExtension.cjs +2 -0
  81. package/dist/adapters/pi/doomConfigExtension.cjs.map +1 -0
  82. package/dist/adapters/pi/doomConfigExtension.d.cts +8 -0
  83. package/dist/adapters/pi/doomConfigExtension.d.cts.map +1 -0
  84. package/dist/adapters/pi/doomConfigExtension.d.mts +8 -0
  85. package/dist/adapters/pi/doomConfigExtension.d.mts.map +1 -0
  86. package/dist/adapters/pi/doomConfigExtension.mjs +2 -0
  87. package/dist/adapters/pi/doomConfigExtension.mjs.map +1 -0
  88. package/dist/adapters/pi/piContext.cjs +1 -1
  89. package/dist/adapters/pi/piContext.cjs.map +1 -1
  90. package/dist/adapters/pi/piContext.d.cts +1 -1
  91. package/dist/adapters/pi/piContext.d.mts +1 -1
  92. package/dist/adapters/pi/piContext.mjs +1 -1
  93. package/dist/adapters/pi/piContext.mjs.map +1 -1
  94. package/dist/adapters/piConfig.cjs +1 -1
  95. package/dist/adapters/piConfig.mjs +1 -1
  96. package/dist/{services → adapters}/profiles.cjs +1 -1
  97. package/dist/{services → adapters}/profiles.cjs.map +1 -1
  98. package/dist/{services → adapters}/profiles.d.cts +1 -1
  99. package/dist/{services → adapters}/profiles.d.cts.map +1 -1
  100. package/dist/{services → adapters}/profiles.d.mts +1 -1
  101. package/dist/{services → adapters}/profiles.d.mts.map +1 -1
  102. package/dist/adapters/profiles.mjs +4 -0
  103. package/dist/{services → adapters}/profiles.mjs.map +1 -1
  104. package/dist/atomicJson.cjs +1 -1
  105. package/dist/atomicJson.d.cts +1 -1
  106. package/dist/atomicJson.d.mts +1 -1
  107. package/dist/atomicJson.mjs +1 -1
  108. package/dist/config/piConfig.cjs +1 -1
  109. package/dist/config/piConfig.mjs +1 -1
  110. package/dist/config/schema.d.cts +1 -1
  111. package/dist/config/schema.d.mts +1 -1
  112. package/dist/config.cjs +1 -1
  113. package/dist/config.d.cts +2 -1
  114. package/dist/config.d.mts +2 -1
  115. package/dist/config.mjs +1 -1
  116. package/dist/configWriter.cjs +1 -1
  117. package/dist/configWriter.d.cts +2 -2
  118. package/dist/configWriter.d.mts +2 -2
  119. package/dist/configWriter.mjs +1 -1
  120. package/dist/container/doomConfigContainer.cjs +2 -0
  121. package/dist/container/doomConfigContainer.cjs.map +1 -0
  122. package/dist/container/{index.d.mts → doomConfigContainer.d.cts} +3 -3
  123. package/dist/container/doomConfigContainer.d.cts.map +1 -0
  124. package/dist/container/{index.d.cts → doomConfigContainer.d.mts} +3 -3
  125. package/dist/container/doomConfigContainer.d.mts.map +1 -0
  126. package/dist/container/doomConfigContainer.mjs +2 -0
  127. package/dist/container/doomConfigContainer.mjs.map +1 -0
  128. package/dist/container.cjs +1 -1
  129. package/dist/container.d.cts +1 -1
  130. package/dist/container.d.mts +1 -1
  131. package/dist/container.mjs +1 -1
  132. package/dist/domains.cjs +1 -1
  133. package/dist/domains.d.cts +5 -3
  134. package/dist/domains.d.mts +5 -3
  135. package/dist/domains.mjs +1 -1
  136. package/dist/extensions/doom.cjs +1 -2
  137. package/dist/extensions/doom.d.cts +2 -8
  138. package/dist/extensions/doom.d.mts +2 -8
  139. package/dist/extensions/doom.mjs +1 -2
  140. package/dist/extensions/pi.cjs +1 -2
  141. package/dist/extensions/pi.d.cts +2 -8
  142. package/dist/extensions/pi.d.mts +2 -8
  143. package/dist/extensions/pi.mjs +1 -2
  144. package/dist/harnessState.cjs +1 -1
  145. package/dist/harnessState.d.cts +2 -2
  146. package/dist/harnessState.d.mts +2 -2
  147. package/dist/harnessState.mjs +1 -1
  148. package/dist/harnessStore.cjs +1 -1
  149. package/dist/harnessStore.d.cts +2 -2
  150. package/dist/harnessStore.d.mts +2 -2
  151. package/dist/harnessStore.mjs +1 -1
  152. package/dist/index.cjs +1 -1
  153. package/dist/index.d.cts +16 -13
  154. package/dist/index.d.mts +16 -13
  155. package/dist/index.mjs +1 -1
  156. package/dist/init.cjs +1 -1
  157. package/dist/init.d.cts +1 -1
  158. package/dist/init.d.mts +1 -1
  159. package/dist/init.mjs +1 -1
  160. package/dist/layeredConfig.cjs +1 -1
  161. package/dist/layeredConfig.d.cts +2 -2
  162. package/dist/layeredConfig.d.mts +2 -2
  163. package/dist/layeredConfig.mjs +1 -1
  164. package/dist/majorModes.cjs +1 -1
  165. package/dist/majorModes.d.cts +2 -2
  166. package/dist/majorModes.d.mts +2 -2
  167. package/dist/majorModes.mjs +1 -1
  168. package/dist/piContext.d.cts +1 -1
  169. package/dist/piContext.d.mts +1 -1
  170. package/dist/profiles.cjs +1 -1
  171. package/dist/profiles.d.cts +2 -2
  172. package/dist/profiles.d.mts +2 -2
  173. package/dist/profiles.mjs +1 -1
  174. package/dist/services/configPolicy.cjs +2 -0
  175. package/dist/services/configPolicy.cjs.map +1 -0
  176. package/dist/services/configPolicy.d.cts +19 -0
  177. package/dist/services/configPolicy.d.cts.map +1 -0
  178. package/dist/services/configPolicy.d.mts +19 -0
  179. package/dist/services/configPolicy.d.mts.map +1 -0
  180. package/dist/services/configPolicy.mjs +2 -0
  181. package/dist/services/configPolicy.mjs.map +1 -0
  182. package/dist/services/domains.cjs +1 -1
  183. package/dist/services/domains.cjs.map +1 -1
  184. package/dist/services/domains.d.cts +2 -67
  185. package/dist/services/domains.d.cts.map +1 -1
  186. package/dist/services/domains.d.mts +2 -67
  187. package/dist/services/domains.d.mts.map +1 -1
  188. package/dist/services/domains.mjs +1 -1
  189. package/dist/services/domains.mjs.map +1 -1
  190. package/dist/types/{index.cjs → config.cjs} +1 -1
  191. package/dist/types/config.cjs.map +1 -0
  192. package/dist/types/{index.d.cts → config.d.cts} +2 -2
  193. package/dist/types/config.d.cts.map +1 -0
  194. package/dist/types/{index.d.mts → config.d.mts} +2 -2
  195. package/dist/types/config.d.mts.map +1 -0
  196. package/dist/types/{index.mjs → config.mjs} +1 -1
  197. package/dist/types/config.mjs.map +1 -0
  198. package/dist/types/domains.d.cts +38 -0
  199. package/dist/types/domains.d.cts.map +1 -0
  200. package/dist/types/domains.d.mts +38 -0
  201. package/dist/types/domains.d.mts.map +1 -0
  202. package/dist/types.cjs +1 -1
  203. package/dist/types.d.cts +2 -2
  204. package/dist/types.d.mts +2 -2
  205. package/dist/types.mjs +1 -1
  206. package/package.json +3 -3
  207. package/dist/container/index.cjs +0 -2
  208. package/dist/container/index.cjs.map +0 -1
  209. package/dist/container/index.d.cts.map +0 -1
  210. package/dist/container/index.d.mts.map +0 -1
  211. package/dist/container/index.mjs +0 -2
  212. package/dist/container/index.mjs.map +0 -1
  213. package/dist/extensions/doom.cjs.map +0 -1
  214. package/dist/extensions/doom.d.cts.map +0 -1
  215. package/dist/extensions/doom.d.mts.map +0 -1
  216. package/dist/extensions/doom.mjs.map +0 -1
  217. package/dist/extensions/pi.cjs.map +0 -1
  218. package/dist/extensions/pi.d.cts.map +0 -1
  219. package/dist/extensions/pi.d.mts.map +0 -1
  220. package/dist/extensions/pi.mjs.map +0 -1
  221. package/dist/schemas/config/piConfig.cjs +0 -1
  222. package/dist/schemas/config/piConfig.mjs +0 -1
  223. package/dist/services/atomicJson.cjs +0 -2
  224. package/dist/services/atomicJson.d.cts.map +0 -1
  225. package/dist/services/atomicJson.d.mts.map +0 -1
  226. package/dist/services/atomicJson.mjs +0 -2
  227. package/dist/services/config.cjs +0 -1
  228. package/dist/services/config.mjs +0 -1
  229. package/dist/services/configWriter.cjs +0 -2
  230. package/dist/services/configWriter.cjs.map +0 -1
  231. package/dist/services/configWriter.mjs +0 -2
  232. package/dist/services/configWriter.mjs.map +0 -1
  233. package/dist/services/harnessState.cjs.map +0 -1
  234. package/dist/services/harnessState.mjs.map +0 -1
  235. package/dist/services/harnessStore.cjs.map +0 -1
  236. package/dist/services/harnessStore.mjs.map +0 -1
  237. package/dist/services/init.cjs +0 -1
  238. package/dist/services/init.mjs +0 -1
  239. package/dist/services/layeredConfig.cjs +0 -2
  240. package/dist/services/layeredConfig.mjs +0 -2
  241. package/dist/services/majorModes.cjs +0 -2
  242. package/dist/services/majorModes.cjs.map +0 -1
  243. package/dist/services/majorModes.d.cts.map +0 -1
  244. package/dist/services/majorModes.d.mts.map +0 -1
  245. package/dist/services/majorModes.mjs +0 -2
  246. package/dist/services/majorModes.mjs.map +0 -1
  247. package/dist/services/profiles.mjs +0 -4
  248. package/dist/types/index.cjs.map +0 -1
  249. package/dist/types/index.d.cts.map +0 -1
  250. package/dist/types/index.d.mts.map +0 -1
  251. package/dist/types/index.mjs.map +0 -1
  252. /package/dist/{services → adapters}/harnessState.cjs +0 -0
  253. /package/dist/{services → adapters}/harnessState.mjs +0 -0
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # @agimon-ai/doompi-config
2
2
 
3
- Shared Doom configuration loading and dependency injection.
3
+ **The one place Doompi decides what a session means.**
4
4
 
5
- Loads and validates `.doom/`: major modes and their layers, domains, profiles, and the hook
6
- registry. Owns the resolved harness state file that every other reader derives from.
5
+ Five YAML files are enough places to look. The rest of Doompi should not grow a second
6
+ opinion about them. This package loads and validates `.doom/`, resolves the user and
7
+ repository layers, and writes the harness state every other package reads.
7
8
 
8
- Part of [Doompi](https://www.npmjs.com/package/@agimon-ai/doompi), a coding agent that loads
9
- only the skills and tools you name. This package is composed by Doompi and is not usually
10
- installed on its own.
9
+ It owns major modes and their extension layers, domains and their plugins, profiles, hook
10
+ groups, runtime config, and the `doompi init` defaults. A bad declaration fails here
11
+ instead of becoming a stranger error halfway through startup.
12
+
13
+ This is part of [Doompi](https://www.npmjs.com/package/@agimon-ai/doompi). Most users get
14
+ it with the distribution; install it directly when building against Doompi's config
15
+ contract.
11
16
 
12
17
  ## Install
13
18
 
@@ -17,19 +22,51 @@ npm install @agimon-ai/doompi-config
17
22
 
18
23
  ## How it loads
19
24
 
20
- Doompi loads this unconditionally. It is core, not a layer, so it does not appear in
21
- `.doom/modes.yaml`: making it optional would only create broken configurations.
25
+ Doompi loads this package as core. It is not a layer and does not belong in
26
+ `.doom/modes.yaml`; making the config reader optional would only create configs nobody can
27
+ read.
28
+
29
+ The bare package name follows its Pi manifest and selects the Doom adapter. The explicit
30
+ `/extensions/pi` and `/extensions/doom` subpaths remain available when choosing an adapter
31
+ by hand.
32
+
33
+ ## What it owns
22
34
 
23
- Extension entry: `@agimon-ai/doompi-config/pi`
35
+ - Loading user config from `~/.pi/.doom/` and repository config from `.doom/`.
36
+ - Validating modes, domains, profiles, hooks, and runtime settings.
37
+ - Resolving repository overrides without losing the declaration root for relative paths.
38
+ - Writing the resolved harness state used by child sessions and other extensions.
39
+ - Seeding a working config through `doompi init`.
24
40
 
25
- ## Notes
41
+ ## Package-owned layer config
42
+
43
+ A layer orders extensions and packages; it does not own their settings. Put configuration
44
+ on the package entry that consumes it:
45
+
46
+ ```yaml
47
+ layers:
48
+ team:
49
+ packages:
50
+ - name: '@agimon-ai/doompi-team'
51
+ config:
52
+ models:
53
+ - model: provider/model-id
54
+ thinking: high
55
+ ```
26
56
 
27
- This is the package that defines the config contract. Changing the shape of `.doom/*.yaml`
28
- starts here.
57
+ Doompi Config validates the package envelope and preserves `config` as an opaque mapping.
58
+ The selected package validates its own fields. Bare package strings, optional package
59
+ objects, explicit adapter subpaths, and local paths keep their existing resolution and
60
+ ordering behavior. The removed sibling `layer.config` form is rejected rather than
61
+ silently assigned to whichever package happens to read it.
29
62
 
30
- ## Entry points
63
+ ## Public API
31
64
 
32
- 18 subpaths are declared in the `exports` field of `package.json`.
65
+ The root export contains the common loaders and types. Focused imports such as `/config`,
66
+ `/domains`, `/majorModes`, `/profiles`, `/harnessState`, and `/container` keep consumers
67
+ out of internal files. `resolvePackageConfigurations` returns one package's selected
68
+ configuration entries in layer and package order. `package.json` is the source of truth
69
+ for the complete export list.
33
70
 
34
71
  ## License
35
72
 
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:crypto"),n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);function i(e,i){n.default.mkdirSync(r.default.dirname(e),{mode:448,recursive:!0});let a=`${e}.${process.pid}.${(0,t.randomUUID)()}.tmp`;try{n.default.writeFileSync(a,`${JSON.stringify(i,null,2)}\n`,{mode:384}),n.default.renameSync(a,e)}catch(e){throw n.default.rmSync(a,{force:!0}),e}}exports.writePrivateAtomicJson=i;
2
+ //# sourceMappingURL=atomicJson.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomicJson.cjs","names":["path","randomUUID"],"sources":["../../src/services/atomicJson.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Private atomic JSON writes.\n *\n * Write to a uniquely named temporary file in the destination directory, then\n * rename onto the target. Rename is atomic within a filesystem, so a reader\n * sees either the previous file or the complete new one and never a partial\n * write. The temporary name carries the pid and a random suffix, so two\n * processes writing the same target cannot collide on it.\n *\n * This prevents torn reads. It does NOT prevent lost updates: two writers doing\n * read-modify-write still clobber each other, because the rename only publishes\n * what the caller already computed. Harness state avoids that by giving every\n * process a file it owns rather than by locking.\n *\n * AVOID:\n * - Treating this as a lock\n * - Leaving the temporary file behind when the write fails\n */\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst TEMPORARY_SUFFIX = '.tmp';\n\n/** Writes JSON readable by its owner only, replacing the target atomically. */\nexport function writePrivateAtomicJson(filePath: string, value: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}${TEMPORARY_SUFFIX}`;\n try {\n fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\\n`, { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporaryPath, filePath);\n } catch (error) {\n fs.rmSync(temporaryPath, { force: true });\n throw error;\n }\n}\n"],"mappings":"wKA4BA,SAAgB,EAAuB,EAAkB,EAAsB,CAC7E,EAAA,QAAG,UAAUA,EAAAA,QAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACtF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,IAAA,EAAGC,EAAAA,WAAAA,CAAW,QAC/D,GAAI,CACF,EAAA,QAAG,cAAc,EAAe,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,IAAK,CAAE,KAAM,GAAkB,CAAC,EAClG,EAAA,QAAG,WAAW,EAAe,CAAQ,CACvC,OAAS,EAAO,CAEd,MADA,EAAA,QAAG,OAAO,EAAe,CAAE,MAAO,EAAK,CAAC,EAClC,CACR,CACF"}
1
+ {"version":3,"file":"atomicJson.cjs","names":["path","randomUUID"],"sources":["../../src/adapters/atomicJson.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Private atomic JSON writes.\n *\n * Write to a uniquely named temporary file in the destination directory, then\n * rename onto the target. Rename is atomic within a filesystem, so a reader\n * sees either the previous file or the complete new one and never a partial\n * write. The temporary name carries the pid and a random suffix, so two\n * processes writing the same target cannot collide on it.\n *\n * This prevents torn reads. It does NOT prevent lost updates: two writers doing\n * read-modify-write still clobber each other, because the rename only publishes\n * what the caller already computed. Harness state avoids that by giving every\n * process a file it owns rather than by locking.\n *\n * AVOID:\n * - Treating this as a lock\n * - Leaving the temporary file behind when the write fails\n */\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst TEMPORARY_SUFFIX = '.tmp';\n\n/** Writes JSON readable by its owner only, replacing the target atomically. */\nexport function writePrivateAtomicJson(filePath: string, value: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}${TEMPORARY_SUFFIX}`;\n try {\n fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\\n`, { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporaryPath, filePath);\n } catch (error) {\n fs.rmSync(temporaryPath, { force: true });\n throw error;\n }\n}\n"],"mappings":"oKA4BA,SAAgB,EAAuB,EAAkB,EAAsB,CAC7E,EAAA,QAAG,UAAUA,EAAAA,QAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACtF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,IAAA,EAAGC,EAAAA,WAAAA,CAAW,QAC/D,GAAI,CACF,EAAA,QAAG,cAAc,EAAe,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,IAAK,CAAE,KAAM,GAAkB,CAAC,EAClG,EAAA,QAAG,WAAW,EAAe,CAAQ,CACvC,OAAS,EAAO,CAEd,MADA,EAAA,QAAG,OAAO,EAAe,CAAE,MAAO,EAAK,CAAC,EAClC,CACR,CACF"}
@@ -1,4 +1,4 @@
1
- //#region src/services/atomicJson.d.ts
1
+ //#region src/adapters/atomicJson.d.ts
2
2
  /** Writes JSON readable by its owner only, replacing the target atomically. */
3
3
  declare function writePrivateAtomicJson(filePath: string, value: unknown): void;
4
4
  //#endregion
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomicJson.d.cts","names":[],"sources":["../../src/adapters/atomicJson.ts"],"mappings":";;iBA4BgB,uBAAuB,kBAAkB"}
@@ -1,4 +1,4 @@
1
- //#region src/services/atomicJson.d.ts
1
+ //#region src/adapters/atomicJson.d.ts
2
2
  /** Writes JSON readable by its owner only, replacing the target atomically. */
3
3
  declare function writePrivateAtomicJson(filePath: string, value: unknown): void;
4
4
  //#endregion
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomicJson.d.mts","names":[],"sources":["../../src/adapters/atomicJson.ts"],"mappings":";;iBA4BgB,uBAAuB,kBAAkB"}
@@ -0,0 +1,2 @@
1
+ import{randomUUID as e}from"node:crypto";import t from"node:fs";import n from"node:path";function r(r,i){t.mkdirSync(n.dirname(r),{mode:448,recursive:!0});let a=`${r}.${process.pid}.${e()}.tmp`;try{t.writeFileSync(a,`${JSON.stringify(i,null,2)}\n`,{mode:384}),t.renameSync(a,r)}catch(e){throw t.rmSync(a,{force:!0}),e}}export{r as writePrivateAtomicJson};
2
+ //# sourceMappingURL=atomicJson.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomicJson.mjs","names":[],"sources":["../../src/services/atomicJson.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Private atomic JSON writes.\n *\n * Write to a uniquely named temporary file in the destination directory, then\n * rename onto the target. Rename is atomic within a filesystem, so a reader\n * sees either the previous file or the complete new one and never a partial\n * write. The temporary name carries the pid and a random suffix, so two\n * processes writing the same target cannot collide on it.\n *\n * This prevents torn reads. It does NOT prevent lost updates: two writers doing\n * read-modify-write still clobber each other, because the rename only publishes\n * what the caller already computed. Harness state avoids that by giving every\n * process a file it owns rather than by locking.\n *\n * AVOID:\n * - Treating this as a lock\n * - Leaving the temporary file behind when the write fails\n */\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst TEMPORARY_SUFFIX = '.tmp';\n\n/** Writes JSON readable by its owner only, replacing the target atomically. */\nexport function writePrivateAtomicJson(filePath: string, value: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}${TEMPORARY_SUFFIX}`;\n try {\n fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\\n`, { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporaryPath, filePath);\n } catch (error) {\n fs.rmSync(temporaryPath, { force: true });\n throw error;\n }\n}\n"],"mappings":"yFA4BA,SAAgB,EAAuB,EAAkB,EAAsB,CAC7E,EAAG,UAAU,EAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACtF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,GAAG,EAAW,QAC/D,GAAI,CACF,EAAG,cAAc,EAAe,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,IAAK,CAAE,KAAM,GAAkB,CAAC,EAClG,EAAG,WAAW,EAAe,CAAQ,CACvC,OAAS,EAAO,CAEd,MADA,EAAG,OAAO,EAAe,CAAE,MAAO,EAAK,CAAC,EAClC,CACR,CACF"}
1
+ {"version":3,"file":"atomicJson.mjs","names":[],"sources":["../../src/adapters/atomicJson.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Private atomic JSON writes.\n *\n * Write to a uniquely named temporary file in the destination directory, then\n * rename onto the target. Rename is atomic within a filesystem, so a reader\n * sees either the previous file or the complete new one and never a partial\n * write. The temporary name carries the pid and a random suffix, so two\n * processes writing the same target cannot collide on it.\n *\n * This prevents torn reads. It does NOT prevent lost updates: two writers doing\n * read-modify-write still clobber each other, because the rename only publishes\n * what the caller already computed. Harness state avoids that by giving every\n * process a file it owns rather than by locking.\n *\n * AVOID:\n * - Treating this as a lock\n * - Leaving the temporary file behind when the write fails\n */\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst TEMPORARY_SUFFIX = '.tmp';\n\n/** Writes JSON readable by its owner only, replacing the target atomically. */\nexport function writePrivateAtomicJson(filePath: string, value: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}${TEMPORARY_SUFFIX}`;\n try {\n fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\\n`, { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporaryPath, filePath);\n } catch (error) {\n fs.rmSync(temporaryPath, { force: true });\n throw error;\n }\n}\n"],"mappings":"yFA4BA,SAAgB,EAAuB,EAAkB,EAAsB,CAC7E,EAAG,UAAU,EAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACtF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,GAAG,EAAW,QAC/D,GAAI,CACF,EAAG,cAAc,EAAe,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,IAAK,CAAE,KAAM,GAAkB,CAAC,EAClG,EAAG,WAAW,EAAe,CAAQ,CACvC,OAAS,EAAO,CAEd,MADA,EAAG,OAAO,EAAe,CAAE,MAAO,EAAK,CAAC,EAClC,CACR,CACF"}
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:os");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("yaml");const a=`.doom`,o=`config.yaml`,s=`auto`,ee=[`modes`,`projectTrust`,`editor`,`voice`,`selection`],te=[`majorMode`,`domains`,`profile`],ne=[`planning`],c=[`main`,`subagents`,`plansDirectory`],re=[`model`,`thinking`],ie=[`command`],l=[`engine`,`language`,`recorder`,`adapters`,`autoCapture`],u=[`binary`,`device`],d=`whisper-cpp`,f=`openai-whisper`,p=`mlx-whisper`,ae=[d,f,p],oe=[`binary`,`model`],se=[`path`,`id`],m=[`model`,`startPhrases`,`stopPhrases`,`utteranceIdleMs`,`transcriptionTimeoutMs`,`tts`],h=[`engine`,`voice`,`rate`],g=1500,_=1e4,v=1e3,y=12e4,b=/^[^/\s]+\/[^/\s]+(?:\/[^/\s]+)*$/u,x=/[\p{P}\p{S}]+/gu,S=[`off`,`minimal`,`low`,`medium`,`high`,`xhigh`,`max`],C=[s,d,f,p],w=[`macos-say`],T=new Set(S),ce=new Set([`ask`,`always`,`never`]),E=new Set(C),D=new Set(w);function O(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function k(e,t,n,r){let i=Object.keys(e).filter(e=>!t.includes(e));if(i.length>0)throw Error(`Doom config at ${r} has unsupported ${n} field(s): ${i.join(`, `)}`)}function A(e,t,n){if(e!==void 0){if(typeof e!=`string`||e.trim().length===0)throw Error(`Doom config at ${n} requires ${t} to be a non-empty string`);return e.trim()}}function j(e){for(let t of e){let e=t.codePointAt(0);if(e!==void 0&&(e<=31||e===127))return!0}return!1}function M(e,t,n,r){let i=A(e,t,n);if(i){if(i.length>r)throw Error(`Doom config at ${n} requires ${t} to be at most ${r} characters`);if(j(i))throw Error(`Doom config at ${n} does not allow control characters in ${t}`);return i}}function N(e,t){let n=`voice.autoCapture.model`,r=A(e,n,t);if(!r||!b.test(r))throw Error(`Doom config at ${t} requires ${n} in provider/model-id form`);return r}function P(e){return e.normalize(`NFKC`).toLocaleLowerCase(`en-US`).replace(x,` `).replace(/\s+/gu,` `).trim()}function F(e,t,n){if(e===void 0)return;if(!Array.isArray(e)||e.some(e=>typeof e!=`string`))throw Error(`Doom config at ${n} requires ${t} to be an array of strings`);if(e.length>16)throw Error(`Doom config at ${n} requires ${t} to contain at most 16 phrases`);let r=e.map(e=>e.trim()),i=new Set;for(let e of r){if(e.length===0)throw Error(`Doom config at ${n} requires ${t} to contain non-empty strings`);if(e.length>64)throw Error(`Doom config at ${n} requires ${t} phrases to be at most 64 characters`);if(j(e))throw Error(`Doom config at ${n} does not allow control characters in ${t}`);let r=P(e);if(!r)throw Error(`Doom config at ${n} requires ${t} to contain words`);if(i.has(r))throw Error(`Doom config at ${n} does not allow normalized duplicates in ${t}`);i.add(r)}return r}function I(e,t){let n=`voice.autoCapture.tts`;if(!O(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);if(k(e,h,n,t),typeof e.engine!=`string`||!D.has(e.engine))throw Error(`Doom config at ${t} requires ${n}.engine to be one of: ${[...D].join(`, `)}`);let r=M(e.voice,`${n}.voice`,t,64),i=e.rate;if(i!==void 0&&(typeof i!=`number`||!Number.isInteger(i)||i<80||i>500))throw Error(`Doom config at ${t} requires ${n}.rate to be an integer from 80 to 500`);return{engine:e.engine,...r?{voice:r}:{},...i===void 0?{}:{rate:i}}}function L(e,t){if(e===void 0)return;let n=`voice.autoCapture`;if(!O(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);k(e,m,n,t);let r=F(e.startPhrases,`${n}.startPhrases`,t),i=F(e.stopPhrases,`${n}.stopPhrases`,t),a=e.utteranceIdleMs;if(a!==void 0&&(typeof a!=`number`||!Number.isInteger(a)||a<g||a>_))throw Error(`Doom config at ${t} requires ${n}.utteranceIdleMs to be an integer from ${g} to ${_}`);let o=e.transcriptionTimeoutMs;if(o!==void 0&&(typeof o!=`number`||!Number.isInteger(o)||o<v||o>y))throw Error(`Doom config at ${t} requires ${n}.transcriptionTimeoutMs to be an integer from ${v} to ${y}`);let s=new Set((r??[]).map(P));if((i??[]).some(e=>s.has(P(e))))throw Error(`Doom config at ${t} does not allow a phrase in both startPhrases and stopPhrases`);return{model:N(e.model,t),...r?{startPhrases:r}:{},...i?{stopPhrases:i}:{},...a===void 0?{}:{utteranceIdleMs:a},...o===void 0?{}:{transcriptionTimeoutMs:o},tts:I(e.tts,t)}}function R(e,t,n){if(e===void 0)return;if(!O(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);k(e,re,t,n);let r=e.thinking;if(r!==void 0&&(typeof r!=`string`||!T.has(r)))throw Error(`Doom config at ${n} requires ${t}.thinking to be one of: ${[...T].join(`, `)}`);let i=A(e.model,`${t}.model`,n);return{...i?{model:i}:{},...r?{thinking:r}:{}}}function z(e,t,n=`modes.planning`){if(!O(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);return k(e,c,n,t),{main:R(e.main,`${n}.main`,t),subagents:R(e.subagents,`${n}.subagents`,t),plansDirectory:A(e.plansDirectory,`${n}.plansDirectory`,t)}}function B(e,t,n,r){if(!O(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);k(e,se,t,n);let i=A(e.path,`${t}.path`,n),a=A(e.id,`${t}.id`,n);if(!!i==!!a)throw Error(`Doom config at ${n} requires exactly one of ${t}.path or ${t}.id`);if(a&&!r)throw Error(`Doom config at ${n} does not support ${t}.id for whisper-cpp`);return{...i?{path:i}:{},...a?{id:a}:{}}}function V(e,t,n,r){if(e===void 0)return;if(!O(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);k(e,oe,t,n);let i=A(e.binary,`${t}.binary`,n);return{...i?{binary:i}:{},model:B(e.model,`${t}.model`,n,r)}}function H(e,t){if(e===void 0)return;if(!O(e))throw Error(`Doom config at ${t} requires voice to be an object`);k(e,l,`voice`,t);let n={};if(e.engine!==void 0){if(typeof e.engine!=`string`||!E.has(e.engine))throw Error(`Doom config at ${t} requires voice.engine to be one of: ${[...E].join(`, `)}`);n.engine=e.engine}let r=A(e.language,`voice.language`,t);if(r&&(n.language=r),e.recorder!==void 0){if(!O(e.recorder))throw Error(`Doom config at ${t} requires voice.recorder to be an object`);k(e.recorder,u,`voice.recorder`,t),n.recorder={};let r=A(e.recorder.binary,`voice.recorder.binary`,t),i=A(e.recorder.device,`voice.recorder.device`,t);r&&(n.recorder.binary=r),i&&(n.recorder.device=i)}if(e.adapters!==void 0){if(!O(e.adapters))throw Error(`Doom config at ${t} requires voice.adapters to be an object`);k(e.adapters,ae,`voice.adapters`,t),n.adapters={[d]:V(e.adapters[d],`voice.adapters.${d}`,t,!1),[f]:V(e.adapters[f],`voice.adapters.${f}`,t,!0),[p]:V(e.adapters[p],`voice.adapters.${p}`,t,!0)}}let i=L(e.autoCapture,t);return i&&(n.autoCapture=i),n}function U(e,t){if(e===void 0)return;if(!O(e))throw Error(`Doom config at ${t} requires selection to be an object`);k(e,te,`selection`,t);let n=e.domains;if(n!==void 0&&(!Array.isArray(n)||n.some(e=>typeof e!=`string`)))throw Error(`Doom config at ${t} requires selection.domains to be an array of strings`);return{majorMode:A(e.majorMode,`selection.majorMode`,t),domains:n,profile:A(e.profile,`selection.profile`,t)}}function W(e,t){if(e!==void 0){if(!O(e))throw Error(`Doom config at ${t} requires editor to be an object`);return k(e,ie,`editor`,t),{command:A(e.command,`editor.command`,t)}}}function G(e,t){let n;try{n=(0,i.parse)(e)??{}}catch(e){throw Error(`Could not parse Doom config at ${t}`,{cause:e})}if(!O(n))throw Error(`Doom config at ${t} must be a YAML object`);k(n,ee,`root`,t);let r;if(n.modes!==void 0){if(!O(n.modes))throw Error(`Doom config at ${t} requires modes to be an object`);k(n.modes,ne,`modes`,t),r=n.modes.planning===void 0?{}:{planning:z(n.modes.planning,t)}}let a=n.projectTrust===void 0?`ask`:n.projectTrust;if(typeof a!=`string`||!ce.has(a))throw Error(`Doom config at ${t} projectTrust must be ask, always, or never`);return{modes:r,projectTrust:a,editor:W(n.editor,t),voice:H(n.voice,t),selection:U(n.selection,t)}}function K(e){return t.default.existsSync(e)?G(t.default.readFileSync(e,`utf8`),e):{projectTrust:`ask`}}async function q(e){try{return G(await t.default.promises.readFile(e,`utf8`),e)}catch(e){if(e.code===`ENOENT`)return{projectTrust:`ask`};throw e}}function J(e,t){return e||t?{...e,...t}:void 0}function Y(e,t){if(t.voice?.autoCapture)throw Error(`Repository Doom config voice.autoCapture is global-only; configure it in ~/.pi/.doom/config.yaml`);let n=e.modes?.planning,r=t.modes?.planning,i=n||r?{main:J(n?.main,r?.main),subagents:J(n?.subagents,r?.subagents),plansDirectory:r?.plansDirectory??n?.plansDirectory}:void 0,a=le(e.voice,t.voice);return{modes:i?{planning:i}:void 0,projectTrust:t.projectTrust,editor:e.editor,voice:a,selection:e.selection||t.selection?{...e.selection,...t.selection}:void 0}}function X(e,t){if(!(!e&&!t))return{...e,...t,model:t?.model??e.model}}function le(e,t){if(!(!e&&!t))return{...e,...t,recorder:e?.recorder||t?.recorder?{...e?.recorder,...t?.recorder}:void 0,adapters:e?.adapters||t?.adapters?{[d]:X(e?.adapters?.[d],t?.adapters?.[d]),[f]:X(e?.adapters?.[f],t?.adapters?.[f]),[p]:X(e?.adapters?.[p],t?.adapters?.[p])}:void 0,...e?.autoCapture?{autoCapture:e.autoCapture}:{}}}function ue(e){return{engine:e.engine??s,language:e.language??`auto`,recorder:{...e.recorder,device:e.recorder?.device??`none:default`},adapters:e.adapters??{},...e.autoCapture?{autoCapture:{...e.autoCapture,startPhrases:[...e.autoCapture.startPhrases??[]],stopPhrases:[...e.autoCapture.stopPhrases??[]],utteranceIdleMs:e.autoCapture.utteranceIdleMs??3e3,transcriptionTimeoutMs:e.autoCapture.transcriptionTimeoutMs??15e3,tts:{...e.autoCapture.tts}}}:{}}}function Z(e=n.default.homedir()){return r.default.join(e,`.pi`,a)}function Q(e=n.default.homedir()){return r.default.join(Z(e),o)}function de(e,t,i=n.default.homedir()){if(!e)return r.default.join(i,`.pi`,`plans`);if(e===`~`)return i;if(e.startsWith(`~/`))return r.default.resolve(i,e.slice(2));if(e.startsWith(`~`))throw Error(`Doom planning plansDirectory supports only '~' or '~/' home aliases`);return r.default.isAbsolute(e)?r.default.normalize(e):r.default.resolve(t,e)}function $(e){return r.default.join(e,a,o)}function fe(e,t=n.default.homedir()){return Y(K(Q(t)),K($(e)))}async function pe(e,t=n.default.homedir()){return Y(await q(Q(t)),await q($(e)))}exports.DOOM_PLANNING_THINKING_LEVELS=S,exports.DOOM_VOICE_ENGINES=C,exports.DOOM_VOICE_TTS_ENGINES=w,exports.globalDoomConfigDirectory=Z,exports.globalDoomConfigPath=Q,exports.loadDoomConfig=fe,exports.loadDoomConfigAsync=pe,exports.mergeDoomConfigs=Y,exports.parseDoomConfig=G,exports.parsePlanningModeConfig=z,exports.repositoryDoomConfigPath=$,exports.resolvePlanningPlansDirectory=de,exports.resolveVoiceConfig=ue;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/configPolicy.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("node:os");i=e.__toESM(i,1);const a=`.doom`,o=`config.yaml`;function s(e){return n.default.existsSync(e)?t.parseDoomConfig(n.default.readFileSync(e,`utf8`),e):{projectTrust:`ask`}}async function c(e){try{return t.parseDoomConfig(await n.default.promises.readFile(e,`utf8`),e)}catch(e){if(e.code===`ENOENT`)return{projectTrust:`ask`};throw e}}function l(e=i.default.homedir()){return r.default.join(e,`.pi`,a)}function u(e=i.default.homedir()){return r.default.join(l(e),o)}function d(e,t,n=i.default.homedir()){if(!e)return r.default.join(n,`.pi`,`plans`);if(e===`~`)return n;if(e.startsWith(`~/`))return r.default.resolve(n,e.slice(2));if(e.startsWith(`~`))throw Error(`Doom planning plansDirectory supports only '~' or '~/' home aliases`);return r.default.isAbsolute(e)?r.default.normalize(e):r.default.resolve(t,e)}function f(e){return r.default.join(e,a,o)}function p(e,n=i.default.homedir()){return t.mergeDoomConfigs(s(u(n)),s(f(e)))}async function m(e,n=i.default.homedir()){let r=await c(u(n)),a=await c(f(e));return t.mergeDoomConfigs(r,a)}exports.globalDoomConfigDirectory=l,exports.globalDoomConfigPath=u,exports.loadDoomConfig=p,exports.loadDoomConfigAsync=m,exports.repositoryDoomConfigPath=f,exports.resolvePlanningPlansDirectory=d;
2
2
  //# sourceMappingURL=config.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs","names":["parseYaml","fs","os","path"],"sources":["../../src/adapters/config.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport type {\n DoomConfig,\n DoomSelectionConfig,\n EditorConfig,\n PlanningAgentConfig,\n PlanningModeConfig,\n PlanningThinkingLevel,\n ProjectTrust,\n ResolvedVoiceConfig,\n VoiceAdapterConfig,\n VoiceAutoCaptureConfig,\n VoiceConfig,\n VoiceEngine,\n VoiceModelConfig,\n VoiceTtsConfig,\n VoiceTtsEngine,\n} from '../types/index.ts';\n\nconst DOOM_DIR = '.doom';\nconst CONFIG_FILE = 'config.yaml';\nconst GLOBAL_CONFIG_DIR = '.pi';\nconst PLANS_DIR = 'plans';\nconst DEFAULT_PROJECT_TRUST: ProjectTrust = 'ask';\nconst DEFAULT_VOICE_ENGINE: VoiceEngine = 'auto';\nconst DEFAULT_VOICE_LANGUAGE = 'auto';\nconst DEFAULT_RECORDER_DEVICE = 'none:default';\nconst ROOT_KEYS = ['modes', 'projectTrust', 'editor', 'voice', 'selection'] as const;\nconst SELECTION_KEYS = ['majorMode', 'domains', 'profile'] as const;\nconst MODE_KEYS = ['planning'] as const;\nconst PLANNING_KEYS = ['main', 'subagents', 'plansDirectory'] as const;\nconst AGENT_KEYS = ['model', 'thinking'] as const;\nconst EDITOR_KEYS = ['command'] as const;\nconst VOICE_KEYS = ['engine', 'language', 'recorder', 'adapters', 'autoCapture'] as const;\nconst RECORDER_KEYS = ['binary', 'device'] as const;\nconst WHISPER_CPP_ENGINE = 'whisper-cpp' as const;\nconst OPENAI_WHISPER_ENGINE = 'openai-whisper' as const;\nconst MLX_WHISPER_ENGINE = 'mlx-whisper' as const;\nconst MACOS_SAY_TTS_ENGINE = 'macos-say' as const;\nconst ADAPTERS_KEYS = [WHISPER_CPP_ENGINE, OPENAI_WHISPER_ENGINE, MLX_WHISPER_ENGINE] as const;\nconst ADAPTER_KEYS = ['binary', 'model'] as const;\nconst MODEL_KEYS = ['path', 'id'] as const;\nconst AUTO_CAPTURE_KEYS = [\n 'model',\n 'startPhrases',\n 'stopPhrases',\n 'utteranceIdleMs',\n 'transcriptionTimeoutMs',\n 'tts',\n] as const;\nconst TTS_KEYS = ['engine', 'voice', 'rate'] as const;\nconst DEFAULT_UTTERANCE_IDLE_MS = 3_000;\nconst MIN_UTTERANCE_IDLE_MS = 1_500;\nconst MAX_UTTERANCE_IDLE_MS = 10_000;\nconst DEFAULT_TRANSCRIPTION_TIMEOUT_MS = 15_000;\nconst MIN_TRANSCRIPTION_TIMEOUT_MS = 1_000;\nconst MAX_TRANSCRIPTION_TIMEOUT_MS = 120_000;\nconst MAX_CONTROL_PHRASES = 16;\nconst MAX_CONTROL_PHRASE_LENGTH = 64;\nconst MAX_TTS_VOICE_LENGTH = 64;\nconst MIN_TTS_RATE = 80;\nconst MAX_TTS_RATE = 500;\nconst MODEL_REFERENCE_PATTERN = /^[^/\\s]+\\/[^/\\s]+(?:\\/[^/\\s]+)*$/u;\nconst PHRASE_PUNCTUATION_PATTERN = /[\\p{P}\\p{S}]+/gu;\n/**\n * The accepted values, in the order a picker should offer them.\n *\n * Exported because the config UI has to offer exactly what the parser accepts,\n * and a second hand-written list in the contributing package would drift. The\n * validating Sets below are built from these for the same reason.\n */\nexport const DOOM_PLANNING_THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;\nexport const DOOM_VOICE_ENGINES = [\n DEFAULT_VOICE_ENGINE,\n WHISPER_CPP_ENGINE,\n OPENAI_WHISPER_ENGINE,\n MLX_WHISPER_ENGINE,\n] as const;\nexport const DOOM_VOICE_TTS_ENGINES = [MACOS_SAY_TTS_ENGINE] as const;\n\nconst THINKING_VALUES = new Set<PlanningThinkingLevel>(DOOM_PLANNING_THINKING_LEVELS);\nconst TRUST_VALUES = new Set<ProjectTrust>(['ask', 'always', 'never']);\nconst ENGINE_VALUES = new Set<VoiceEngine>(DOOM_VOICE_ENGINES);\nconst TTS_ENGINE_VALUES = new Set<VoiceTtsEngine>(DOOM_VOICE_TTS_ENGINES);\ntype ConfigObject = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is ConfigObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\nfunction assertKeys(value: ConfigObject, allowed: readonly string[], location: string, filePath: string): void {\n const unknown = Object.keys(value).filter((key) => !allowed.includes(key));\n if (unknown.length > 0)\n throw new Error(`Doom config at ${filePath} has unsupported ${location} field(s): ${unknown.join(', ')}`);\n}\nfunction optionalString(value: unknown, location: string, filePath: string): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'string' || value.trim().length === 0)\n throw new Error(`Doom config at ${filePath} requires ${location} to be a non-empty string`);\n return value.trim();\n}\nfunction hasControlCharacters(value: string): boolean {\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f)) return true;\n }\n return false;\n}\nfunction boundedOptionalString(\n value: unknown,\n location: string,\n filePath: string,\n maximumLength: number,\n): string | undefined {\n const text = optionalString(value, location, filePath);\n if (!text) return undefined;\n if (text.length > maximumLength)\n throw new Error(`Doom config at ${filePath} requires ${location} to be at most ${maximumLength} characters`);\n if (hasControlCharacters(text))\n throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);\n return text;\n}\nfunction parseModelReference(value: unknown, filePath: string): string {\n const location = 'voice.autoCapture.model';\n const model = optionalString(value, location, filePath);\n if (!model || !MODEL_REFERENCE_PATTERN.test(model))\n throw new Error(`Doom config at ${filePath} requires ${location} in provider/model-id form`);\n return model;\n}\nfunction normalizeControlPhrase(value: string): string {\n return value\n .normalize('NFKC')\n .toLocaleLowerCase('en-US')\n .replace(PHRASE_PUNCTUATION_PATTERN, ' ')\n .replace(/\\s+/gu, ' ')\n .trim();\n}\nfunction parseControlPhrases(value: unknown, location: string, filePath: string): string[] | undefined {\n if (value === undefined) return undefined;\n if (!Array.isArray(value) || value.some((phrase) => typeof phrase !== 'string'))\n throw new Error(`Doom config at ${filePath} requires ${location} to be an array of strings`);\n if (value.length > MAX_CONTROL_PHRASES)\n throw new Error(\n `Doom config at ${filePath} requires ${location} to contain at most ${MAX_CONTROL_PHRASES} phrases`,\n );\n const phrases = value.map((phrase) => phrase.trim());\n const normalized = new Set<string>();\n for (const phrase of phrases) {\n if (phrase.length === 0)\n throw new Error(`Doom config at ${filePath} requires ${location} to contain non-empty strings`);\n if (phrase.length > MAX_CONTROL_PHRASE_LENGTH)\n throw new Error(\n `Doom config at ${filePath} requires ${location} phrases to be at most ${MAX_CONTROL_PHRASE_LENGTH} characters`,\n );\n if (hasControlCharacters(phrase))\n throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);\n const key = normalizeControlPhrase(phrase);\n if (!key) throw new Error(`Doom config at ${filePath} requires ${location} to contain words`);\n if (normalized.has(key))\n throw new Error(`Doom config at ${filePath} does not allow normalized duplicates in ${location}`);\n normalized.add(key);\n }\n return phrases;\n}\nfunction parseTtsConfig(value: unknown, filePath: string): VoiceTtsConfig {\n const location = 'voice.autoCapture.tts';\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, TTS_KEYS, location, filePath);\n if (typeof value.engine !== 'string' || !TTS_ENGINE_VALUES.has(value.engine as VoiceTtsEngine))\n throw new Error(\n `Doom config at ${filePath} requires ${location}.engine to be one of: ${[...TTS_ENGINE_VALUES].join(', ')}`,\n );\n const voice = boundedOptionalString(value.voice, `${location}.voice`, filePath, MAX_TTS_VOICE_LENGTH);\n const rate = value.rate;\n if (\n rate !== undefined &&\n (typeof rate !== 'number' || !Number.isInteger(rate) || rate < MIN_TTS_RATE || rate > MAX_TTS_RATE)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.rate to be an integer from ${MIN_TTS_RATE} to ${MAX_TTS_RATE}`,\n );\n }\n return {\n engine: value.engine as VoiceTtsEngine,\n ...(voice ? { voice } : {}),\n ...(rate === undefined ? {} : { rate }),\n };\n}\nfunction parseAutoCapture(value: unknown, filePath: string): VoiceAutoCaptureConfig | undefined {\n if (value === undefined) return undefined;\n const location = 'voice.autoCapture';\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, AUTO_CAPTURE_KEYS, location, filePath);\n const startPhrases = parseControlPhrases(value.startPhrases, `${location}.startPhrases`, filePath);\n const stopPhrases = parseControlPhrases(value.stopPhrases, `${location}.stopPhrases`, filePath);\n const utteranceIdleMs = value.utteranceIdleMs;\n if (\n utteranceIdleMs !== undefined &&\n (typeof utteranceIdleMs !== 'number' ||\n !Number.isInteger(utteranceIdleMs) ||\n utteranceIdleMs < MIN_UTTERANCE_IDLE_MS ||\n utteranceIdleMs > MAX_UTTERANCE_IDLE_MS)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.utteranceIdleMs to be an integer from ${MIN_UTTERANCE_IDLE_MS} to ${MAX_UTTERANCE_IDLE_MS}`,\n );\n }\n const transcriptionTimeoutMs = value.transcriptionTimeoutMs;\n if (\n transcriptionTimeoutMs !== undefined &&\n (typeof transcriptionTimeoutMs !== 'number' ||\n !Number.isInteger(transcriptionTimeoutMs) ||\n transcriptionTimeoutMs < MIN_TRANSCRIPTION_TIMEOUT_MS ||\n transcriptionTimeoutMs > MAX_TRANSCRIPTION_TIMEOUT_MS)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.transcriptionTimeoutMs to be an integer from ${MIN_TRANSCRIPTION_TIMEOUT_MS} to ${MAX_TRANSCRIPTION_TIMEOUT_MS}`,\n );\n }\n const startKeys = new Set((startPhrases ?? []).map(normalizeControlPhrase));\n if ((stopPhrases ?? []).some((phrase) => startKeys.has(normalizeControlPhrase(phrase))))\n throw new Error(`Doom config at ${filePath} does not allow a phrase in both startPhrases and stopPhrases`);\n return {\n model: parseModelReference(value.model, filePath),\n ...(startPhrases ? { startPhrases } : {}),\n ...(stopPhrases ? { stopPhrases } : {}),\n ...(utteranceIdleMs === undefined ? {} : { utteranceIdleMs }),\n ...(transcriptionTimeoutMs === undefined ? {} : { transcriptionTimeoutMs }),\n tts: parseTtsConfig(value.tts, filePath),\n };\n}\nfunction parseAgent(value: unknown, location: string, filePath: string): PlanningAgentConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, AGENT_KEYS, location, filePath);\n const thinking = value.thinking;\n if (\n thinking !== undefined &&\n (typeof thinking !== 'string' || !THINKING_VALUES.has(thinking as PlanningThinkingLevel))\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.thinking to be one of: ${[...THINKING_VALUES].join(', ')}`,\n );\n }\n const model = optionalString(value.model, `${location}.model`, filePath);\n return { ...(model ? { model } : {}), ...(thinking ? { thinking: thinking as PlanningThinkingLevel } : {}) };\n}\nexport function parsePlanningModeConfig(\n value: unknown,\n filePath: string,\n location = 'modes.planning',\n): PlanningModeConfig {\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, PLANNING_KEYS, location, filePath);\n return {\n main: parseAgent(value.main, `${location}.main`, filePath),\n subagents: parseAgent(value.subagents, `${location}.subagents`, filePath),\n plansDirectory: optionalString(value.plansDirectory, `${location}.plansDirectory`, filePath),\n };\n}\nfunction parseModel(value: unknown, location: string, filePath: string, allowId: boolean): VoiceModelConfig {\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, MODEL_KEYS, location, filePath);\n const modelPath = optionalString(value.path, `${location}.path`, filePath);\n const id = optionalString(value.id, `${location}.id`, filePath);\n if (Boolean(modelPath) === Boolean(id))\n throw new Error(`Doom config at ${filePath} requires exactly one of ${location}.path or ${location}.id`);\n if (id && !allowId) throw new Error(`Doom config at ${filePath} does not support ${location}.id for whisper-cpp`);\n return { ...(modelPath ? { path: modelPath } : {}), ...(id ? { id } : {}) };\n}\nfunction parseAdapter(\n value: unknown,\n location: string,\n filePath: string,\n allowId: boolean,\n): VoiceAdapterConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, ADAPTER_KEYS, location, filePath);\n const binary = optionalString(value.binary, `${location}.binary`, filePath);\n return { ...(binary ? { binary } : {}), model: parseModel(value.model, `${location}.model`, filePath, allowId) };\n}\nfunction parseVoice(value: unknown, filePath: string): VoiceConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires voice to be an object`);\n assertKeys(value, VOICE_KEYS, 'voice', filePath);\n const result: VoiceConfig = {};\n if (value.engine !== undefined) {\n if (typeof value.engine !== 'string' || !ENGINE_VALUES.has(value.engine as VoiceEngine))\n throw new Error(\n `Doom config at ${filePath} requires voice.engine to be one of: ${[...ENGINE_VALUES].join(', ')}`,\n );\n result.engine = value.engine as VoiceEngine;\n }\n const language = optionalString(value.language, 'voice.language', filePath);\n if (language) result.language = language;\n if (value.recorder !== undefined) {\n if (!isObject(value.recorder))\n throw new Error(`Doom config at ${filePath} requires voice.recorder to be an object`);\n assertKeys(value.recorder, RECORDER_KEYS, 'voice.recorder', filePath);\n result.recorder = {};\n const binary = optionalString(value.recorder.binary, 'voice.recorder.binary', filePath);\n const device = optionalString(value.recorder.device, 'voice.recorder.device', filePath);\n if (binary) result.recorder.binary = binary;\n if (device) result.recorder.device = device;\n }\n if (value.adapters !== undefined) {\n if (!isObject(value.adapters))\n throw new Error(`Doom config at ${filePath} requires voice.adapters to be an object`);\n assertKeys(value.adapters, ADAPTERS_KEYS, 'voice.adapters', filePath);\n result.adapters = {\n [WHISPER_CPP_ENGINE]: parseAdapter(\n value.adapters[WHISPER_CPP_ENGINE],\n `voice.adapters.${WHISPER_CPP_ENGINE}`,\n filePath,\n false,\n ),\n [OPENAI_WHISPER_ENGINE]: parseAdapter(\n value.adapters[OPENAI_WHISPER_ENGINE],\n `voice.adapters.${OPENAI_WHISPER_ENGINE}`,\n filePath,\n true,\n ),\n [MLX_WHISPER_ENGINE]: parseAdapter(\n value.adapters[MLX_WHISPER_ENGINE],\n `voice.adapters.${MLX_WHISPER_ENGINE}`,\n filePath,\n true,\n ),\n };\n }\n const autoCapture = parseAutoCapture(value.autoCapture, filePath);\n if (autoCapture) result.autoCapture = autoCapture;\n return result;\n}\nfunction parseSelection(value: unknown, filePath: string): DoomSelectionConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires selection to be an object`);\n assertKeys(value, SELECTION_KEYS, 'selection', filePath);\n const domains = value.domains;\n if (domains !== undefined && (!Array.isArray(domains) || domains.some((name) => typeof name !== 'string'))) {\n throw new Error(`Doom config at ${filePath} requires selection.domains to be an array of strings`);\n }\n return {\n majorMode: optionalString(value.majorMode, 'selection.majorMode', filePath),\n // An empty list is meaningful: it selects no content domains at all.\n domains: domains as string[] | undefined,\n profile: optionalString(value.profile, 'selection.profile', filePath),\n };\n}\nfunction parseEditor(value: unknown, filePath: string): EditorConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires editor to be an object`);\n assertKeys(value, EDITOR_KEYS, 'editor', filePath);\n return { command: optionalString(value.command, 'editor.command', filePath) };\n}\nexport function parseDoomConfig(content: string, filePath: string): DoomConfig {\n let parsed: unknown;\n try {\n parsed = parseYaml(content) ?? {};\n } catch (error) {\n throw new Error(`Could not parse Doom config at ${filePath}`, { cause: error });\n }\n if (!isObject(parsed)) throw new Error(`Doom config at ${filePath} must be a YAML object`);\n assertKeys(parsed, ROOT_KEYS, 'root', filePath);\n let modes: DoomConfig['modes'];\n if (parsed.modes !== undefined) {\n if (!isObject(parsed.modes)) throw new Error(`Doom config at ${filePath} requires modes to be an object`);\n assertKeys(parsed.modes, MODE_KEYS, 'modes', filePath);\n modes =\n parsed.modes.planning === undefined ? {} : { planning: parsePlanningModeConfig(parsed.modes.planning, filePath) };\n }\n const trust = parsed.projectTrust === undefined ? DEFAULT_PROJECT_TRUST : parsed.projectTrust;\n if (typeof trust !== 'string' || !TRUST_VALUES.has(trust as ProjectTrust))\n throw new Error(`Doom config at ${filePath} projectTrust must be ask, always, or never`);\n return {\n modes,\n projectTrust: trust as ProjectTrust,\n editor: parseEditor(parsed.editor, filePath),\n voice: parseVoice(parsed.voice, filePath),\n selection: parseSelection(parsed.selection, filePath),\n };\n}\nfunction readConfig(filePath: string): DoomConfig {\n return fs.existsSync(filePath)\n ? parseDoomConfig(fs.readFileSync(filePath, 'utf8'), filePath)\n : { projectTrust: DEFAULT_PROJECT_TRUST };\n}\n\nasync function readConfigAsync(filePath: string): Promise<DoomConfig> {\n try {\n return parseDoomConfig(await fs.promises.readFile(filePath, 'utf8'), filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { projectTrust: DEFAULT_PROJECT_TRUST };\n throw error;\n }\n}\nfunction mergeAgent(\n globalValue?: PlanningAgentConfig,\n repositoryValue?: PlanningAgentConfig,\n): PlanningAgentConfig | undefined {\n return globalValue || repositoryValue ? { ...globalValue, ...repositoryValue } : undefined;\n}\nexport function mergeDoomConfigs(globalConfig: DoomConfig, repositoryConfig: DoomConfig): DoomConfig {\n if (repositoryConfig.voice?.autoCapture)\n throw new Error('Repository Doom config voice.autoCapture is global-only; configure it in ~/.pi/.doom/config.yaml');\n const globalPlanning = globalConfig.modes?.planning;\n const repositoryPlanning = repositoryConfig.modes?.planning;\n const planning =\n globalPlanning || repositoryPlanning\n ? {\n main: mergeAgent(globalPlanning?.main, repositoryPlanning?.main),\n subagents: mergeAgent(globalPlanning?.subagents, repositoryPlanning?.subagents),\n plansDirectory: repositoryPlanning?.plansDirectory ?? globalPlanning?.plansDirectory,\n }\n : undefined;\n const voice = mergeVoice(globalConfig.voice, repositoryConfig.voice);\n return {\n modes: planning ? { planning } : undefined,\n projectTrust: repositoryConfig.projectTrust,\n editor: globalConfig.editor,\n voice,\n // Per key, so a repository can pin one axis and inherit the rest.\n selection:\n globalConfig.selection || repositoryConfig.selection\n ? { ...globalConfig.selection, ...repositoryConfig.selection }\n : undefined,\n };\n}\nfunction mergeAdapter(\n globalValue?: VoiceAdapterConfig,\n repositoryValue?: VoiceAdapterConfig,\n): VoiceAdapterConfig | undefined {\n if (!globalValue && !repositoryValue) return undefined;\n return { ...globalValue, ...repositoryValue, model: repositoryValue?.model ?? globalValue!.model };\n}\nfunction mergeVoice(globalValue?: VoiceConfig, repositoryValue?: VoiceConfig): VoiceConfig | undefined {\n if (!globalValue && !repositoryValue) return undefined;\n return {\n ...globalValue,\n ...repositoryValue,\n recorder:\n globalValue?.recorder || repositoryValue?.recorder\n ? { ...globalValue?.recorder, ...repositoryValue?.recorder }\n : undefined,\n adapters:\n globalValue?.adapters || repositoryValue?.adapters\n ? {\n [WHISPER_CPP_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[WHISPER_CPP_ENGINE],\n repositoryValue?.adapters?.[WHISPER_CPP_ENGINE],\n ),\n [OPENAI_WHISPER_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[OPENAI_WHISPER_ENGINE],\n repositoryValue?.adapters?.[OPENAI_WHISPER_ENGINE],\n ),\n [MLX_WHISPER_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[MLX_WHISPER_ENGINE],\n repositoryValue?.adapters?.[MLX_WHISPER_ENGINE],\n ),\n }\n : undefined,\n ...(globalValue?.autoCapture ? { autoCapture: globalValue.autoCapture } : {}),\n };\n}\nexport function resolveVoiceConfig(config: VoiceConfig): ResolvedVoiceConfig {\n return {\n engine: config.engine ?? DEFAULT_VOICE_ENGINE,\n language: config.language ?? DEFAULT_VOICE_LANGUAGE,\n recorder: { ...config.recorder, device: config.recorder?.device ?? DEFAULT_RECORDER_DEVICE },\n adapters: config.adapters ?? {},\n ...(config.autoCapture\n ? {\n autoCapture: {\n ...config.autoCapture,\n startPhrases: [...(config.autoCapture.startPhrases ?? [])],\n stopPhrases: [...(config.autoCapture.stopPhrases ?? [])],\n utteranceIdleMs: config.autoCapture.utteranceIdleMs ?? DEFAULT_UTTERANCE_IDLE_MS,\n transcriptionTimeoutMs: config.autoCapture.transcriptionTimeoutMs ?? DEFAULT_TRANSCRIPTION_TIMEOUT_MS,\n tts: { ...config.autoCapture.tts },\n },\n }\n : {}),\n };\n}\nexport function globalDoomConfigDirectory(homeDirectory = os.homedir()): string {\n return path.join(homeDirectory, GLOBAL_CONFIG_DIR, DOOM_DIR);\n}\nexport function globalDoomConfigPath(homeDirectory = os.homedir()): string {\n return path.join(globalDoomConfigDirectory(homeDirectory), CONFIG_FILE);\n}\nexport function resolvePlanningPlansDirectory(\n configured: string | undefined,\n repoRoot: string,\n homeDirectory = os.homedir(),\n): string {\n if (!configured) return path.join(homeDirectory, GLOBAL_CONFIG_DIR, PLANS_DIR);\n if (configured === '~') return homeDirectory;\n if (configured.startsWith('~/')) return path.resolve(homeDirectory, configured.slice(2));\n if (configured.startsWith('~')) {\n throw new Error(\"Doom planning plansDirectory supports only '~' or '~/' home aliases\");\n }\n return path.isAbsolute(configured) ? path.normalize(configured) : path.resolve(repoRoot, configured);\n}\nexport function repositoryDoomConfigPath(repoRoot: string): string {\n return path.join(repoRoot, DOOM_DIR, CONFIG_FILE);\n}\nexport function loadDoomConfig(repoRoot: string, homeDirectory = os.homedir()): DoomConfig {\n return mergeDoomConfigs(\n readConfig(globalDoomConfigPath(homeDirectory)),\n readConfig(repositoryDoomConfigPath(repoRoot)),\n );\n}\n\nexport async function loadDoomConfigAsync(repoRoot: string, homeDirectory = os.homedir()): Promise<DoomConfig> {\n const globalConfig = await readConfigAsync(globalDoomConfigPath(homeDirectory));\n const repositoryConfig = await readConfigAsync(repositoryDoomConfigPath(repoRoot));\n return mergeDoomConfigs(globalConfig, repositoryConfig);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.cjs","names":["fs","parseDoomConfig","os","path","mergeDoomConfigs"],"sources":["../../src/adapters/config.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { mergeDoomConfigs, parseDoomConfig } from '../services/configPolicy.ts';\nimport type { DoomConfig } from '../types/config.ts';\n\nconst DOOM_DIR = '.doom';\nconst CONFIG_FILE = 'config.yaml';\nconst GLOBAL_CONFIG_DIR = '.pi';\nconst PLANS_DIR = 'plans';\n\nfunction readConfig(filePath: string): DoomConfig {\n return fs.existsSync(filePath)\n ? parseDoomConfig(fs.readFileSync(filePath, 'utf8'), filePath)\n : { projectTrust: 'ask' };\n}\n\nasync function readConfigAsync(filePath: string): Promise<DoomConfig> {\n try {\n return parseDoomConfig(await fs.promises.readFile(filePath, 'utf8'), filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { projectTrust: 'ask' };\n throw error;\n }\n}\n\nexport function globalDoomConfigDirectory(homeDirectory = os.homedir()): string {\n return path.join(homeDirectory, GLOBAL_CONFIG_DIR, DOOM_DIR);\n}\n\nexport function globalDoomConfigPath(homeDirectory = os.homedir()): string {\n return path.join(globalDoomConfigDirectory(homeDirectory), CONFIG_FILE);\n}\n\nexport function resolvePlanningPlansDirectory(\n configured: string | undefined,\n repoRoot: string,\n homeDirectory = os.homedir(),\n): string {\n if (!configured) return path.join(homeDirectory, GLOBAL_CONFIG_DIR, PLANS_DIR);\n if (configured === '~') return homeDirectory;\n if (configured.startsWith('~/')) return path.resolve(homeDirectory, configured.slice(2));\n if (configured.startsWith('~')) {\n throw new Error(\"Doom planning plansDirectory supports only '~' or '~/' home aliases\");\n }\n return path.isAbsolute(configured) ? path.normalize(configured) : path.resolve(repoRoot, configured);\n}\n\nexport function repositoryDoomConfigPath(repoRoot: string): string {\n return path.join(repoRoot, DOOM_DIR, CONFIG_FILE);\n}\n\nexport function loadDoomConfig(repoRoot: string, homeDirectory = os.homedir()): DoomConfig {\n return mergeDoomConfigs(\n readConfig(globalDoomConfigPath(homeDirectory)),\n readConfig(repositoryDoomConfigPath(repoRoot)),\n );\n}\n\nexport async function loadDoomConfigAsync(repoRoot: string, homeDirectory = os.homedir()): Promise<DoomConfig> {\n const globalConfig = await readConfigAsync(globalDoomConfigPath(homeDirectory));\n const repositoryConfig = await readConfigAsync(repositoryDoomConfigPath(repoRoot));\n return mergeDoomConfigs(globalConfig, repositoryConfig);\n}\n"],"mappings":"+NAMA,MAAM,EAAW,QACX,EAAc,cAIpB,SAAS,EAAW,EAA8B,CAChD,OAAOA,EAAAA,QAAG,WAAW,CAAQ,EACzBC,EAAAA,gBAAgBD,EAAAA,QAAG,aAAa,EAAU,MAAM,EAAG,CAAQ,EAC3D,CAAE,aAAc,KAAM,CAC5B,CAEA,eAAe,EAAgB,EAAuC,CACpE,GAAI,CACF,OAAOC,EAAAA,gBAAgB,MAAMD,EAAAA,QAAG,SAAS,SAAS,EAAU,MAAM,EAAG,CAAQ,CAC/E,OAAS,EAAO,CACd,GAAK,EAAgC,OAAS,SAAU,MAAO,CAAE,aAAc,KAAM,EACrF,MAAM,CACR,CACF,CAEA,SAAgB,EAA0B,EAAgBE,EAAAA,QAAG,QAAQ,EAAW,CAC9E,OAAOC,EAAAA,QAAK,KAAK,EAAe,MAAmB,CAAQ,CAC7D,CAEA,SAAgB,EAAqB,EAAgBD,EAAAA,QAAG,QAAQ,EAAW,CACzE,OAAOC,EAAAA,QAAK,KAAK,EAA0B,CAAa,EAAG,CAAW,CACxE,CAEA,SAAgB,EACd,EACA,EACA,EAAgBD,EAAAA,QAAG,QAAQ,EACnB,CACR,GAAI,CAAC,EAAY,OAAOC,EAAAA,QAAK,KAAK,EAAe,MAAmB,OAAS,EAC7E,GAAI,IAAe,IAAK,OAAO,EAC/B,GAAI,EAAW,WAAW,IAAI,EAAG,OAAOA,EAAAA,QAAK,QAAQ,EAAe,EAAW,MAAM,CAAC,CAAC,EACvF,GAAI,EAAW,WAAW,GAAG,EAC3B,MAAU,MAAM,qEAAqE,EAEvF,OAAOA,EAAAA,QAAK,WAAW,CAAU,EAAIA,EAAAA,QAAK,UAAU,CAAU,EAAIA,EAAAA,QAAK,QAAQ,EAAU,CAAU,CACrG,CAEA,SAAgB,EAAyB,EAA0B,CACjE,OAAOA,EAAAA,QAAK,KAAK,EAAU,EAAU,CAAW,CAClD,CAEA,SAAgB,EAAe,EAAkB,EAAgBD,EAAAA,QAAG,QAAQ,EAAe,CACzF,OAAOE,EAAAA,iBACL,EAAW,EAAqB,CAAa,CAAC,EAC9C,EAAW,EAAyB,CAAQ,CAAC,CAC/C,CACF,CAEA,eAAsB,EAAoB,EAAkB,EAAgBF,EAAAA,QAAG,QAAQ,EAAwB,CAC7G,IAAM,EAAe,MAAM,EAAgB,EAAqB,CAAa,CAAC,EACxE,EAAmB,MAAM,EAAgB,EAAyB,CAAQ,CAAC,EACjF,OAAOE,EAAAA,iBAAiB,EAAc,CAAgB,CACxD"}
@@ -1,19 +1,5 @@
1
- import { DoomConfig, PlanningModeConfig, ResolvedVoiceConfig, VoiceConfig } from "../types/index.cjs";
1
+ import { DoomConfig } from "../types/config.cjs";
2
2
  //#region src/adapters/config.d.ts
3
- /**
4
- * The accepted values, in the order a picker should offer them.
5
- *
6
- * Exported because the config UI has to offer exactly what the parser accepts,
7
- * and a second hand-written list in the contributing package would drift. The
8
- * validating Sets below are built from these for the same reason.
9
- */
10
- declare const DOOM_PLANNING_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
11
- declare const DOOM_VOICE_ENGINES: readonly ["auto", "whisper-cpp", "openai-whisper", "mlx-whisper"];
12
- declare const DOOM_VOICE_TTS_ENGINES: readonly ["macos-say"];
13
- declare function parsePlanningModeConfig(value: unknown, filePath: string, location?: string): PlanningModeConfig;
14
- declare function parseDoomConfig(content: string, filePath: string): DoomConfig;
15
- declare function mergeDoomConfigs(globalConfig: DoomConfig, repositoryConfig: DoomConfig): DoomConfig;
16
- declare function resolveVoiceConfig(config: VoiceConfig): ResolvedVoiceConfig;
17
3
  declare function globalDoomConfigDirectory(homeDirectory?: string): string;
18
4
  declare function globalDoomConfigPath(homeDirectory?: string): string;
19
5
  declare function resolvePlanningPlansDirectory(configured: string | undefined, repoRoot: string, homeDirectory?: string): string;
@@ -21,5 +7,5 @@ declare function repositoryDoomConfigPath(repoRoot: string): string;
21
7
  declare function loadDoomConfig(repoRoot: string, homeDirectory?: string): DoomConfig;
22
8
  declare function loadDoomConfigAsync(repoRoot: string, homeDirectory?: string): Promise<DoomConfig>;
23
9
  //#endregion
24
- export { DOOM_PLANNING_THINKING_LEVELS, DOOM_VOICE_ENGINES, DOOM_VOICE_TTS_ENGINES, globalDoomConfigDirectory, globalDoomConfigPath, loadDoomConfig, loadDoomConfigAsync, mergeDoomConfigs, parseDoomConfig, parsePlanningModeConfig, repositoryDoomConfigPath, resolvePlanningPlansDirectory, resolveVoiceConfig };
10
+ export { globalDoomConfigDirectory, globalDoomConfigPath, loadDoomConfig, loadDoomConfigAsync, repositoryDoomConfigPath, resolvePlanningPlansDirectory };
25
11
  //# sourceMappingURL=config.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.cts","names":[],"sources":["../../src/adapters/config.ts"],"mappings":";;;;;;;;;cA0Ea;cACA;cAMA;iBAwKG,wBACd,gBACA,kBACA,oBACC;iBAyGa,gBAAgB,iBAAiB,mBAAmB;iBA+CpD,iBAAiB,cAAc,YAAY,kBAAkB,aAAa;iBA8D1E,mBAAmB,QAAQ,cAAc;iBAoBzC,0BAA0B;iBAG1B,qBAAqB;iBAGrB,8BACd,gCACA,kBACA;iBAUc,yBAAyB;iBAGzB,eAAe,kBAAkB,yBAA+B;iBAO1D,oBAAoB,kBAAkB,yBAA+B,QAAQ"}
1
+ {"version":3,"file":"config.d.cts","names":[],"sources":["../../src/adapters/config.ts"],"mappings":";;iBA0BgB,0BAA0B;iBAI1B,qBAAqB;iBAIrB,8BACd,gCACA,kBACA;iBAWc,yBAAyB;iBAIzB,eAAe,kBAAkB,yBAA+B;iBAO1D,oBAAoB,kBAAkB,yBAA+B,QAAQ"}
@@ -1,19 +1,5 @@
1
- import { DoomConfig, PlanningModeConfig, ResolvedVoiceConfig, VoiceConfig } from "../types/index.mjs";
1
+ import { DoomConfig } from "../types/config.mjs";
2
2
  //#region src/adapters/config.d.ts
3
- /**
4
- * The accepted values, in the order a picker should offer them.
5
- *
6
- * Exported because the config UI has to offer exactly what the parser accepts,
7
- * and a second hand-written list in the contributing package would drift. The
8
- * validating Sets below are built from these for the same reason.
9
- */
10
- declare const DOOM_PLANNING_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
11
- declare const DOOM_VOICE_ENGINES: readonly ["auto", "whisper-cpp", "openai-whisper", "mlx-whisper"];
12
- declare const DOOM_VOICE_TTS_ENGINES: readonly ["macos-say"];
13
- declare function parsePlanningModeConfig(value: unknown, filePath: string, location?: string): PlanningModeConfig;
14
- declare function parseDoomConfig(content: string, filePath: string): DoomConfig;
15
- declare function mergeDoomConfigs(globalConfig: DoomConfig, repositoryConfig: DoomConfig): DoomConfig;
16
- declare function resolveVoiceConfig(config: VoiceConfig): ResolvedVoiceConfig;
17
3
  declare function globalDoomConfigDirectory(homeDirectory?: string): string;
18
4
  declare function globalDoomConfigPath(homeDirectory?: string): string;
19
5
  declare function resolvePlanningPlansDirectory(configured: string | undefined, repoRoot: string, homeDirectory?: string): string;
@@ -21,5 +7,5 @@ declare function repositoryDoomConfigPath(repoRoot: string): string;
21
7
  declare function loadDoomConfig(repoRoot: string, homeDirectory?: string): DoomConfig;
22
8
  declare function loadDoomConfigAsync(repoRoot: string, homeDirectory?: string): Promise<DoomConfig>;
23
9
  //#endregion
24
- export { DOOM_PLANNING_THINKING_LEVELS, DOOM_VOICE_ENGINES, DOOM_VOICE_TTS_ENGINES, globalDoomConfigDirectory, globalDoomConfigPath, loadDoomConfig, loadDoomConfigAsync, mergeDoomConfigs, parseDoomConfig, parsePlanningModeConfig, repositoryDoomConfigPath, resolvePlanningPlansDirectory, resolveVoiceConfig };
10
+ export { globalDoomConfigDirectory, globalDoomConfigPath, loadDoomConfig, loadDoomConfigAsync, repositoryDoomConfigPath, resolvePlanningPlansDirectory };
25
11
  //# sourceMappingURL=config.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/adapters/config.ts"],"mappings":";;;;;;;;;cA0Ea;cACA;cAMA;iBAwKG,wBACd,gBACA,kBACA,oBACC;iBAyGa,gBAAgB,iBAAiB,mBAAmB;iBA+CpD,iBAAiB,cAAc,YAAY,kBAAkB,aAAa;iBA8D1E,mBAAmB,QAAQ,cAAc;iBAoBzC,0BAA0B;iBAG1B,qBAAqB;iBAGrB,8BACd,gCACA,kBACA;iBAUc,yBAAyB;iBAGzB,eAAe,kBAAkB,yBAA+B;iBAO1D,oBAAoB,kBAAkB,yBAA+B,QAAQ"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../../src/adapters/config.ts"],"mappings":";;iBA0BgB,0BAA0B;iBAI1B,qBAAqB;iBAIrB,8BACd,gCACA,kBACA;iBAWc,yBAAyB;iBAIzB,eAAe,kBAAkB,yBAA+B;iBAO1D,oBAAoB,kBAAkB,yBAA+B,QAAQ"}
@@ -1,2 +1,2 @@
1
- import e from"node:fs";import t from"node:os";import n from"node:path";import{parse as r}from"yaml";const i=`.doom`,a=`config.yaml`,o=`auto`,s=[`modes`,`projectTrust`,`editor`,`voice`,`selection`],c=[`majorMode`,`domains`,`profile`],ee=[`planning`],l=[`main`,`subagents`,`plansDirectory`],te=[`model`,`thinking`],ne=[`command`],re=[`engine`,`language`,`recorder`,`adapters`,`autoCapture`],u=[`binary`,`device`],d=`whisper-cpp`,f=`openai-whisper`,p=`mlx-whisper`,ie=[d,f,p],ae=[`binary`,`model`],oe=[`path`,`id`],m=[`model`,`startPhrases`,`stopPhrases`,`utteranceIdleMs`,`transcriptionTimeoutMs`,`tts`],h=[`engine`,`voice`,`rate`],g=1500,_=1e4,v=1e3,y=12e4,b=/^[^/\s]+\/[^/\s]+(?:\/[^/\s]+)*$/u,x=/[\p{P}\p{S}]+/gu,S=[`off`,`minimal`,`low`,`medium`,`high`,`xhigh`,`max`],C=[o,d,f,p],w=[`macos-say`],T=new Set(S),E=new Set([`ask`,`always`,`never`]),D=new Set(C),O=new Set(w);function k(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function A(e,t,n,r){let i=Object.keys(e).filter(e=>!t.includes(e));if(i.length>0)throw Error(`Doom config at ${r} has unsupported ${n} field(s): ${i.join(`, `)}`)}function j(e,t,n){if(e!==void 0){if(typeof e!=`string`||e.trim().length===0)throw Error(`Doom config at ${n} requires ${t} to be a non-empty string`);return e.trim()}}function M(e){for(let t of e){let e=t.codePointAt(0);if(e!==void 0&&(e<=31||e===127))return!0}return!1}function se(e,t,n,r){let i=j(e,t,n);if(i){if(i.length>r)throw Error(`Doom config at ${n} requires ${t} to be at most ${r} characters`);if(M(i))throw Error(`Doom config at ${n} does not allow control characters in ${t}`);return i}}function N(e,t){let n=`voice.autoCapture.model`,r=j(e,n,t);if(!r||!b.test(r))throw Error(`Doom config at ${t} requires ${n} in provider/model-id form`);return r}function P(e){return e.normalize(`NFKC`).toLocaleLowerCase(`en-US`).replace(x,` `).replace(/\s+/gu,` `).trim()}function F(e,t,n){if(e===void 0)return;if(!Array.isArray(e)||e.some(e=>typeof e!=`string`))throw Error(`Doom config at ${n} requires ${t} to be an array of strings`);if(e.length>16)throw Error(`Doom config at ${n} requires ${t} to contain at most 16 phrases`);let r=e.map(e=>e.trim()),i=new Set;for(let e of r){if(e.length===0)throw Error(`Doom config at ${n} requires ${t} to contain non-empty strings`);if(e.length>64)throw Error(`Doom config at ${n} requires ${t} phrases to be at most 64 characters`);if(M(e))throw Error(`Doom config at ${n} does not allow control characters in ${t}`);let r=P(e);if(!r)throw Error(`Doom config at ${n} requires ${t} to contain words`);if(i.has(r))throw Error(`Doom config at ${n} does not allow normalized duplicates in ${t}`);i.add(r)}return r}function I(e,t){let n=`voice.autoCapture.tts`;if(!k(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);if(A(e,h,n,t),typeof e.engine!=`string`||!O.has(e.engine))throw Error(`Doom config at ${t} requires ${n}.engine to be one of: ${[...O].join(`, `)}`);let r=se(e.voice,`${n}.voice`,t,64),i=e.rate;if(i!==void 0&&(typeof i!=`number`||!Number.isInteger(i)||i<80||i>500))throw Error(`Doom config at ${t} requires ${n}.rate to be an integer from 80 to 500`);return{engine:e.engine,...r?{voice:r}:{},...i===void 0?{}:{rate:i}}}function L(e,t){if(e===void 0)return;let n=`voice.autoCapture`;if(!k(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);A(e,m,n,t);let r=F(e.startPhrases,`${n}.startPhrases`,t),i=F(e.stopPhrases,`${n}.stopPhrases`,t),a=e.utteranceIdleMs;if(a!==void 0&&(typeof a!=`number`||!Number.isInteger(a)||a<g||a>_))throw Error(`Doom config at ${t} requires ${n}.utteranceIdleMs to be an integer from ${g} to ${_}`);let o=e.transcriptionTimeoutMs;if(o!==void 0&&(typeof o!=`number`||!Number.isInteger(o)||o<v||o>y))throw Error(`Doom config at ${t} requires ${n}.transcriptionTimeoutMs to be an integer from ${v} to ${y}`);let s=new Set((r??[]).map(P));if((i??[]).some(e=>s.has(P(e))))throw Error(`Doom config at ${t} does not allow a phrase in both startPhrases and stopPhrases`);return{model:N(e.model,t),...r?{startPhrases:r}:{},...i?{stopPhrases:i}:{},...a===void 0?{}:{utteranceIdleMs:a},...o===void 0?{}:{transcriptionTimeoutMs:o},tts:I(e.tts,t)}}function R(e,t,n){if(e===void 0)return;if(!k(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);A(e,te,t,n);let r=e.thinking;if(r!==void 0&&(typeof r!=`string`||!T.has(r)))throw Error(`Doom config at ${n} requires ${t}.thinking to be one of: ${[...T].join(`, `)}`);let i=j(e.model,`${t}.model`,n);return{...i?{model:i}:{},...r?{thinking:r}:{}}}function z(e,t,n=`modes.planning`){if(!k(e))throw Error(`Doom config at ${t} requires ${n} to be an object`);return A(e,l,n,t),{main:R(e.main,`${n}.main`,t),subagents:R(e.subagents,`${n}.subagents`,t),plansDirectory:j(e.plansDirectory,`${n}.plansDirectory`,t)}}function B(e,t,n,r){if(!k(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);A(e,oe,t,n);let i=j(e.path,`${t}.path`,n),a=j(e.id,`${t}.id`,n);if(!!i==!!a)throw Error(`Doom config at ${n} requires exactly one of ${t}.path or ${t}.id`);if(a&&!r)throw Error(`Doom config at ${n} does not support ${t}.id for whisper-cpp`);return{...i?{path:i}:{},...a?{id:a}:{}}}function V(e,t,n,r){if(e===void 0)return;if(!k(e))throw Error(`Doom config at ${n} requires ${t} to be an object`);A(e,ae,t,n);let i=j(e.binary,`${t}.binary`,n);return{...i?{binary:i}:{},model:B(e.model,`${t}.model`,n,r)}}function H(e,t){if(e===void 0)return;if(!k(e))throw Error(`Doom config at ${t} requires voice to be an object`);A(e,re,`voice`,t);let n={};if(e.engine!==void 0){if(typeof e.engine!=`string`||!D.has(e.engine))throw Error(`Doom config at ${t} requires voice.engine to be one of: ${[...D].join(`, `)}`);n.engine=e.engine}let r=j(e.language,`voice.language`,t);if(r&&(n.language=r),e.recorder!==void 0){if(!k(e.recorder))throw Error(`Doom config at ${t} requires voice.recorder to be an object`);A(e.recorder,u,`voice.recorder`,t),n.recorder={};let r=j(e.recorder.binary,`voice.recorder.binary`,t),i=j(e.recorder.device,`voice.recorder.device`,t);r&&(n.recorder.binary=r),i&&(n.recorder.device=i)}if(e.adapters!==void 0){if(!k(e.adapters))throw Error(`Doom config at ${t} requires voice.adapters to be an object`);A(e.adapters,ie,`voice.adapters`,t),n.adapters={[d]:V(e.adapters[d],`voice.adapters.${d}`,t,!1),[f]:V(e.adapters[f],`voice.adapters.${f}`,t,!0),[p]:V(e.adapters[p],`voice.adapters.${p}`,t,!0)}}let i=L(e.autoCapture,t);return i&&(n.autoCapture=i),n}function U(e,t){if(e===void 0)return;if(!k(e))throw Error(`Doom config at ${t} requires selection to be an object`);A(e,c,`selection`,t);let n=e.domains;if(n!==void 0&&(!Array.isArray(n)||n.some(e=>typeof e!=`string`)))throw Error(`Doom config at ${t} requires selection.domains to be an array of strings`);return{majorMode:j(e.majorMode,`selection.majorMode`,t),domains:n,profile:j(e.profile,`selection.profile`,t)}}function W(e,t){if(e!==void 0){if(!k(e))throw Error(`Doom config at ${t} requires editor to be an object`);return A(e,ne,`editor`,t),{command:j(e.command,`editor.command`,t)}}}function G(e,t){let n;try{n=r(e)??{}}catch(e){throw Error(`Could not parse Doom config at ${t}`,{cause:e})}if(!k(n))throw Error(`Doom config at ${t} must be a YAML object`);A(n,s,`root`,t);let i;if(n.modes!==void 0){if(!k(n.modes))throw Error(`Doom config at ${t} requires modes to be an object`);A(n.modes,ee,`modes`,t),i=n.modes.planning===void 0?{}:{planning:z(n.modes.planning,t)}}let a=n.projectTrust===void 0?`ask`:n.projectTrust;if(typeof a!=`string`||!E.has(a))throw Error(`Doom config at ${t} projectTrust must be ask, always, or never`);return{modes:i,projectTrust:a,editor:W(n.editor,t),voice:H(n.voice,t),selection:U(n.selection,t)}}function K(t){return e.existsSync(t)?G(e.readFileSync(t,`utf8`),t):{projectTrust:`ask`}}async function q(t){try{return G(await e.promises.readFile(t,`utf8`),t)}catch(e){if(e.code===`ENOENT`)return{projectTrust:`ask`};throw e}}function J(e,t){return e||t?{...e,...t}:void 0}function Y(e,t){if(t.voice?.autoCapture)throw Error(`Repository Doom config voice.autoCapture is global-only; configure it in ~/.pi/.doom/config.yaml`);let n=e.modes?.planning,r=t.modes?.planning,i=n||r?{main:J(n?.main,r?.main),subagents:J(n?.subagents,r?.subagents),plansDirectory:r?.plansDirectory??n?.plansDirectory}:void 0,a=ce(e.voice,t.voice);return{modes:i?{planning:i}:void 0,projectTrust:t.projectTrust,editor:e.editor,voice:a,selection:e.selection||t.selection?{...e.selection,...t.selection}:void 0}}function X(e,t){if(!(!e&&!t))return{...e,...t,model:t?.model??e.model}}function ce(e,t){if(!(!e&&!t))return{...e,...t,recorder:e?.recorder||t?.recorder?{...e?.recorder,...t?.recorder}:void 0,adapters:e?.adapters||t?.adapters?{[d]:X(e?.adapters?.[d],t?.adapters?.[d]),[f]:X(e?.adapters?.[f],t?.adapters?.[f]),[p]:X(e?.adapters?.[p],t?.adapters?.[p])}:void 0,...e?.autoCapture?{autoCapture:e.autoCapture}:{}}}function le(e){return{engine:e.engine??o,language:e.language??`auto`,recorder:{...e.recorder,device:e.recorder?.device??`none:default`},adapters:e.adapters??{},...e.autoCapture?{autoCapture:{...e.autoCapture,startPhrases:[...e.autoCapture.startPhrases??[]],stopPhrases:[...e.autoCapture.stopPhrases??[]],utteranceIdleMs:e.autoCapture.utteranceIdleMs??3e3,transcriptionTimeoutMs:e.autoCapture.transcriptionTimeoutMs??15e3,tts:{...e.autoCapture.tts}}}:{}}}function Z(e=t.homedir()){return n.join(e,`.pi`,i)}function Q(e=t.homedir()){return n.join(Z(e),a)}function ue(e,r,i=t.homedir()){if(!e)return n.join(i,`.pi`,`plans`);if(e===`~`)return i;if(e.startsWith(`~/`))return n.resolve(i,e.slice(2));if(e.startsWith(`~`))throw Error(`Doom planning plansDirectory supports only '~' or '~/' home aliases`);return n.isAbsolute(e)?n.normalize(e):n.resolve(r,e)}function $(e){return n.join(e,i,a)}function de(e,n=t.homedir()){return Y(K(Q(n)),K($(e)))}async function fe(e,n=t.homedir()){return Y(await q(Q(n)),await q($(e)))}export{S as DOOM_PLANNING_THINKING_LEVELS,C as DOOM_VOICE_ENGINES,w as DOOM_VOICE_TTS_ENGINES,Z as globalDoomConfigDirectory,Q as globalDoomConfigPath,de as loadDoomConfig,fe as loadDoomConfigAsync,Y as mergeDoomConfigs,G as parseDoomConfig,z as parsePlanningModeConfig,$ as repositoryDoomConfigPath,ue as resolvePlanningPlansDirectory,le as resolveVoiceConfig};
1
+ import{mergeDoomConfigs as e,parseDoomConfig as t}from"../services/configPolicy.mjs";import n from"node:fs";import r from"node:path";import i from"node:os";const a=`.doom`,o=`config.yaml`;function s(e){return n.existsSync(e)?t(n.readFileSync(e,`utf8`),e):{projectTrust:`ask`}}async function c(e){try{return t(await n.promises.readFile(e,`utf8`),e)}catch(e){if(e.code===`ENOENT`)return{projectTrust:`ask`};throw e}}function l(e=i.homedir()){return r.join(e,`.pi`,a)}function u(e=i.homedir()){return r.join(l(e),o)}function d(e,t,n=i.homedir()){if(!e)return r.join(n,`.pi`,`plans`);if(e===`~`)return n;if(e.startsWith(`~/`))return r.resolve(n,e.slice(2));if(e.startsWith(`~`))throw Error(`Doom planning plansDirectory supports only '~' or '~/' home aliases`);return r.isAbsolute(e)?r.normalize(e):r.resolve(t,e)}function f(e){return r.join(e,a,o)}function p(t,n=i.homedir()){return e(s(u(n)),s(f(t)))}async function m(t,n=i.homedir()){let r=await c(u(n)),a=await c(f(t));return e(r,a)}export{l as globalDoomConfigDirectory,u as globalDoomConfigPath,p as loadDoomConfig,m as loadDoomConfigAsync,f as repositoryDoomConfigPath,d as resolvePlanningPlansDirectory};
2
2
  //# sourceMappingURL=config.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":["parseYaml"],"sources":["../../src/adapters/config.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport type {\n DoomConfig,\n DoomSelectionConfig,\n EditorConfig,\n PlanningAgentConfig,\n PlanningModeConfig,\n PlanningThinkingLevel,\n ProjectTrust,\n ResolvedVoiceConfig,\n VoiceAdapterConfig,\n VoiceAutoCaptureConfig,\n VoiceConfig,\n VoiceEngine,\n VoiceModelConfig,\n VoiceTtsConfig,\n VoiceTtsEngine,\n} from '../types/index.ts';\n\nconst DOOM_DIR = '.doom';\nconst CONFIG_FILE = 'config.yaml';\nconst GLOBAL_CONFIG_DIR = '.pi';\nconst PLANS_DIR = 'plans';\nconst DEFAULT_PROJECT_TRUST: ProjectTrust = 'ask';\nconst DEFAULT_VOICE_ENGINE: VoiceEngine = 'auto';\nconst DEFAULT_VOICE_LANGUAGE = 'auto';\nconst DEFAULT_RECORDER_DEVICE = 'none:default';\nconst ROOT_KEYS = ['modes', 'projectTrust', 'editor', 'voice', 'selection'] as const;\nconst SELECTION_KEYS = ['majorMode', 'domains', 'profile'] as const;\nconst MODE_KEYS = ['planning'] as const;\nconst PLANNING_KEYS = ['main', 'subagents', 'plansDirectory'] as const;\nconst AGENT_KEYS = ['model', 'thinking'] as const;\nconst EDITOR_KEYS = ['command'] as const;\nconst VOICE_KEYS = ['engine', 'language', 'recorder', 'adapters', 'autoCapture'] as const;\nconst RECORDER_KEYS = ['binary', 'device'] as const;\nconst WHISPER_CPP_ENGINE = 'whisper-cpp' as const;\nconst OPENAI_WHISPER_ENGINE = 'openai-whisper' as const;\nconst MLX_WHISPER_ENGINE = 'mlx-whisper' as const;\nconst MACOS_SAY_TTS_ENGINE = 'macos-say' as const;\nconst ADAPTERS_KEYS = [WHISPER_CPP_ENGINE, OPENAI_WHISPER_ENGINE, MLX_WHISPER_ENGINE] as const;\nconst ADAPTER_KEYS = ['binary', 'model'] as const;\nconst MODEL_KEYS = ['path', 'id'] as const;\nconst AUTO_CAPTURE_KEYS = [\n 'model',\n 'startPhrases',\n 'stopPhrases',\n 'utteranceIdleMs',\n 'transcriptionTimeoutMs',\n 'tts',\n] as const;\nconst TTS_KEYS = ['engine', 'voice', 'rate'] as const;\nconst DEFAULT_UTTERANCE_IDLE_MS = 3_000;\nconst MIN_UTTERANCE_IDLE_MS = 1_500;\nconst MAX_UTTERANCE_IDLE_MS = 10_000;\nconst DEFAULT_TRANSCRIPTION_TIMEOUT_MS = 15_000;\nconst MIN_TRANSCRIPTION_TIMEOUT_MS = 1_000;\nconst MAX_TRANSCRIPTION_TIMEOUT_MS = 120_000;\nconst MAX_CONTROL_PHRASES = 16;\nconst MAX_CONTROL_PHRASE_LENGTH = 64;\nconst MAX_TTS_VOICE_LENGTH = 64;\nconst MIN_TTS_RATE = 80;\nconst MAX_TTS_RATE = 500;\nconst MODEL_REFERENCE_PATTERN = /^[^/\\s]+\\/[^/\\s]+(?:\\/[^/\\s]+)*$/u;\nconst PHRASE_PUNCTUATION_PATTERN = /[\\p{P}\\p{S}]+/gu;\n/**\n * The accepted values, in the order a picker should offer them.\n *\n * Exported because the config UI has to offer exactly what the parser accepts,\n * and a second hand-written list in the contributing package would drift. The\n * validating Sets below are built from these for the same reason.\n */\nexport const DOOM_PLANNING_THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;\nexport const DOOM_VOICE_ENGINES = [\n DEFAULT_VOICE_ENGINE,\n WHISPER_CPP_ENGINE,\n OPENAI_WHISPER_ENGINE,\n MLX_WHISPER_ENGINE,\n] as const;\nexport const DOOM_VOICE_TTS_ENGINES = [MACOS_SAY_TTS_ENGINE] as const;\n\nconst THINKING_VALUES = new Set<PlanningThinkingLevel>(DOOM_PLANNING_THINKING_LEVELS);\nconst TRUST_VALUES = new Set<ProjectTrust>(['ask', 'always', 'never']);\nconst ENGINE_VALUES = new Set<VoiceEngine>(DOOM_VOICE_ENGINES);\nconst TTS_ENGINE_VALUES = new Set<VoiceTtsEngine>(DOOM_VOICE_TTS_ENGINES);\ntype ConfigObject = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is ConfigObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\nfunction assertKeys(value: ConfigObject, allowed: readonly string[], location: string, filePath: string): void {\n const unknown = Object.keys(value).filter((key) => !allowed.includes(key));\n if (unknown.length > 0)\n throw new Error(`Doom config at ${filePath} has unsupported ${location} field(s): ${unknown.join(', ')}`);\n}\nfunction optionalString(value: unknown, location: string, filePath: string): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'string' || value.trim().length === 0)\n throw new Error(`Doom config at ${filePath} requires ${location} to be a non-empty string`);\n return value.trim();\n}\nfunction hasControlCharacters(value: string): boolean {\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f)) return true;\n }\n return false;\n}\nfunction boundedOptionalString(\n value: unknown,\n location: string,\n filePath: string,\n maximumLength: number,\n): string | undefined {\n const text = optionalString(value, location, filePath);\n if (!text) return undefined;\n if (text.length > maximumLength)\n throw new Error(`Doom config at ${filePath} requires ${location} to be at most ${maximumLength} characters`);\n if (hasControlCharacters(text))\n throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);\n return text;\n}\nfunction parseModelReference(value: unknown, filePath: string): string {\n const location = 'voice.autoCapture.model';\n const model = optionalString(value, location, filePath);\n if (!model || !MODEL_REFERENCE_PATTERN.test(model))\n throw new Error(`Doom config at ${filePath} requires ${location} in provider/model-id form`);\n return model;\n}\nfunction normalizeControlPhrase(value: string): string {\n return value\n .normalize('NFKC')\n .toLocaleLowerCase('en-US')\n .replace(PHRASE_PUNCTUATION_PATTERN, ' ')\n .replace(/\\s+/gu, ' ')\n .trim();\n}\nfunction parseControlPhrases(value: unknown, location: string, filePath: string): string[] | undefined {\n if (value === undefined) return undefined;\n if (!Array.isArray(value) || value.some((phrase) => typeof phrase !== 'string'))\n throw new Error(`Doom config at ${filePath} requires ${location} to be an array of strings`);\n if (value.length > MAX_CONTROL_PHRASES)\n throw new Error(\n `Doom config at ${filePath} requires ${location} to contain at most ${MAX_CONTROL_PHRASES} phrases`,\n );\n const phrases = value.map((phrase) => phrase.trim());\n const normalized = new Set<string>();\n for (const phrase of phrases) {\n if (phrase.length === 0)\n throw new Error(`Doom config at ${filePath} requires ${location} to contain non-empty strings`);\n if (phrase.length > MAX_CONTROL_PHRASE_LENGTH)\n throw new Error(\n `Doom config at ${filePath} requires ${location} phrases to be at most ${MAX_CONTROL_PHRASE_LENGTH} characters`,\n );\n if (hasControlCharacters(phrase))\n throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);\n const key = normalizeControlPhrase(phrase);\n if (!key) throw new Error(`Doom config at ${filePath} requires ${location} to contain words`);\n if (normalized.has(key))\n throw new Error(`Doom config at ${filePath} does not allow normalized duplicates in ${location}`);\n normalized.add(key);\n }\n return phrases;\n}\nfunction parseTtsConfig(value: unknown, filePath: string): VoiceTtsConfig {\n const location = 'voice.autoCapture.tts';\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, TTS_KEYS, location, filePath);\n if (typeof value.engine !== 'string' || !TTS_ENGINE_VALUES.has(value.engine as VoiceTtsEngine))\n throw new Error(\n `Doom config at ${filePath} requires ${location}.engine to be one of: ${[...TTS_ENGINE_VALUES].join(', ')}`,\n );\n const voice = boundedOptionalString(value.voice, `${location}.voice`, filePath, MAX_TTS_VOICE_LENGTH);\n const rate = value.rate;\n if (\n rate !== undefined &&\n (typeof rate !== 'number' || !Number.isInteger(rate) || rate < MIN_TTS_RATE || rate > MAX_TTS_RATE)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.rate to be an integer from ${MIN_TTS_RATE} to ${MAX_TTS_RATE}`,\n );\n }\n return {\n engine: value.engine as VoiceTtsEngine,\n ...(voice ? { voice } : {}),\n ...(rate === undefined ? {} : { rate }),\n };\n}\nfunction parseAutoCapture(value: unknown, filePath: string): VoiceAutoCaptureConfig | undefined {\n if (value === undefined) return undefined;\n const location = 'voice.autoCapture';\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, AUTO_CAPTURE_KEYS, location, filePath);\n const startPhrases = parseControlPhrases(value.startPhrases, `${location}.startPhrases`, filePath);\n const stopPhrases = parseControlPhrases(value.stopPhrases, `${location}.stopPhrases`, filePath);\n const utteranceIdleMs = value.utteranceIdleMs;\n if (\n utteranceIdleMs !== undefined &&\n (typeof utteranceIdleMs !== 'number' ||\n !Number.isInteger(utteranceIdleMs) ||\n utteranceIdleMs < MIN_UTTERANCE_IDLE_MS ||\n utteranceIdleMs > MAX_UTTERANCE_IDLE_MS)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.utteranceIdleMs to be an integer from ${MIN_UTTERANCE_IDLE_MS} to ${MAX_UTTERANCE_IDLE_MS}`,\n );\n }\n const transcriptionTimeoutMs = value.transcriptionTimeoutMs;\n if (\n transcriptionTimeoutMs !== undefined &&\n (typeof transcriptionTimeoutMs !== 'number' ||\n !Number.isInteger(transcriptionTimeoutMs) ||\n transcriptionTimeoutMs < MIN_TRANSCRIPTION_TIMEOUT_MS ||\n transcriptionTimeoutMs > MAX_TRANSCRIPTION_TIMEOUT_MS)\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.transcriptionTimeoutMs to be an integer from ${MIN_TRANSCRIPTION_TIMEOUT_MS} to ${MAX_TRANSCRIPTION_TIMEOUT_MS}`,\n );\n }\n const startKeys = new Set((startPhrases ?? []).map(normalizeControlPhrase));\n if ((stopPhrases ?? []).some((phrase) => startKeys.has(normalizeControlPhrase(phrase))))\n throw new Error(`Doom config at ${filePath} does not allow a phrase in both startPhrases and stopPhrases`);\n return {\n model: parseModelReference(value.model, filePath),\n ...(startPhrases ? { startPhrases } : {}),\n ...(stopPhrases ? { stopPhrases } : {}),\n ...(utteranceIdleMs === undefined ? {} : { utteranceIdleMs }),\n ...(transcriptionTimeoutMs === undefined ? {} : { transcriptionTimeoutMs }),\n tts: parseTtsConfig(value.tts, filePath),\n };\n}\nfunction parseAgent(value: unknown, location: string, filePath: string): PlanningAgentConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, AGENT_KEYS, location, filePath);\n const thinking = value.thinking;\n if (\n thinking !== undefined &&\n (typeof thinking !== 'string' || !THINKING_VALUES.has(thinking as PlanningThinkingLevel))\n ) {\n throw new Error(\n `Doom config at ${filePath} requires ${location}.thinking to be one of: ${[...THINKING_VALUES].join(', ')}`,\n );\n }\n const model = optionalString(value.model, `${location}.model`, filePath);\n return { ...(model ? { model } : {}), ...(thinking ? { thinking: thinking as PlanningThinkingLevel } : {}) };\n}\nexport function parsePlanningModeConfig(\n value: unknown,\n filePath: string,\n location = 'modes.planning',\n): PlanningModeConfig {\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, PLANNING_KEYS, location, filePath);\n return {\n main: parseAgent(value.main, `${location}.main`, filePath),\n subagents: parseAgent(value.subagents, `${location}.subagents`, filePath),\n plansDirectory: optionalString(value.plansDirectory, `${location}.plansDirectory`, filePath),\n };\n}\nfunction parseModel(value: unknown, location: string, filePath: string, allowId: boolean): VoiceModelConfig {\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, MODEL_KEYS, location, filePath);\n const modelPath = optionalString(value.path, `${location}.path`, filePath);\n const id = optionalString(value.id, `${location}.id`, filePath);\n if (Boolean(modelPath) === Boolean(id))\n throw new Error(`Doom config at ${filePath} requires exactly one of ${location}.path or ${location}.id`);\n if (id && !allowId) throw new Error(`Doom config at ${filePath} does not support ${location}.id for whisper-cpp`);\n return { ...(modelPath ? { path: modelPath } : {}), ...(id ? { id } : {}) };\n}\nfunction parseAdapter(\n value: unknown,\n location: string,\n filePath: string,\n allowId: boolean,\n): VoiceAdapterConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);\n assertKeys(value, ADAPTER_KEYS, location, filePath);\n const binary = optionalString(value.binary, `${location}.binary`, filePath);\n return { ...(binary ? { binary } : {}), model: parseModel(value.model, `${location}.model`, filePath, allowId) };\n}\nfunction parseVoice(value: unknown, filePath: string): VoiceConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires voice to be an object`);\n assertKeys(value, VOICE_KEYS, 'voice', filePath);\n const result: VoiceConfig = {};\n if (value.engine !== undefined) {\n if (typeof value.engine !== 'string' || !ENGINE_VALUES.has(value.engine as VoiceEngine))\n throw new Error(\n `Doom config at ${filePath} requires voice.engine to be one of: ${[...ENGINE_VALUES].join(', ')}`,\n );\n result.engine = value.engine as VoiceEngine;\n }\n const language = optionalString(value.language, 'voice.language', filePath);\n if (language) result.language = language;\n if (value.recorder !== undefined) {\n if (!isObject(value.recorder))\n throw new Error(`Doom config at ${filePath} requires voice.recorder to be an object`);\n assertKeys(value.recorder, RECORDER_KEYS, 'voice.recorder', filePath);\n result.recorder = {};\n const binary = optionalString(value.recorder.binary, 'voice.recorder.binary', filePath);\n const device = optionalString(value.recorder.device, 'voice.recorder.device', filePath);\n if (binary) result.recorder.binary = binary;\n if (device) result.recorder.device = device;\n }\n if (value.adapters !== undefined) {\n if (!isObject(value.adapters))\n throw new Error(`Doom config at ${filePath} requires voice.adapters to be an object`);\n assertKeys(value.adapters, ADAPTERS_KEYS, 'voice.adapters', filePath);\n result.adapters = {\n [WHISPER_CPP_ENGINE]: parseAdapter(\n value.adapters[WHISPER_CPP_ENGINE],\n `voice.adapters.${WHISPER_CPP_ENGINE}`,\n filePath,\n false,\n ),\n [OPENAI_WHISPER_ENGINE]: parseAdapter(\n value.adapters[OPENAI_WHISPER_ENGINE],\n `voice.adapters.${OPENAI_WHISPER_ENGINE}`,\n filePath,\n true,\n ),\n [MLX_WHISPER_ENGINE]: parseAdapter(\n value.adapters[MLX_WHISPER_ENGINE],\n `voice.adapters.${MLX_WHISPER_ENGINE}`,\n filePath,\n true,\n ),\n };\n }\n const autoCapture = parseAutoCapture(value.autoCapture, filePath);\n if (autoCapture) result.autoCapture = autoCapture;\n return result;\n}\nfunction parseSelection(value: unknown, filePath: string): DoomSelectionConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires selection to be an object`);\n assertKeys(value, SELECTION_KEYS, 'selection', filePath);\n const domains = value.domains;\n if (domains !== undefined && (!Array.isArray(domains) || domains.some((name) => typeof name !== 'string'))) {\n throw new Error(`Doom config at ${filePath} requires selection.domains to be an array of strings`);\n }\n return {\n majorMode: optionalString(value.majorMode, 'selection.majorMode', filePath),\n // An empty list is meaningful: it selects no content domains at all.\n domains: domains as string[] | undefined,\n profile: optionalString(value.profile, 'selection.profile', filePath),\n };\n}\nfunction parseEditor(value: unknown, filePath: string): EditorConfig | undefined {\n if (value === undefined) return undefined;\n if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires editor to be an object`);\n assertKeys(value, EDITOR_KEYS, 'editor', filePath);\n return { command: optionalString(value.command, 'editor.command', filePath) };\n}\nexport function parseDoomConfig(content: string, filePath: string): DoomConfig {\n let parsed: unknown;\n try {\n parsed = parseYaml(content) ?? {};\n } catch (error) {\n throw new Error(`Could not parse Doom config at ${filePath}`, { cause: error });\n }\n if (!isObject(parsed)) throw new Error(`Doom config at ${filePath} must be a YAML object`);\n assertKeys(parsed, ROOT_KEYS, 'root', filePath);\n let modes: DoomConfig['modes'];\n if (parsed.modes !== undefined) {\n if (!isObject(parsed.modes)) throw new Error(`Doom config at ${filePath} requires modes to be an object`);\n assertKeys(parsed.modes, MODE_KEYS, 'modes', filePath);\n modes =\n parsed.modes.planning === undefined ? {} : { planning: parsePlanningModeConfig(parsed.modes.planning, filePath) };\n }\n const trust = parsed.projectTrust === undefined ? DEFAULT_PROJECT_TRUST : parsed.projectTrust;\n if (typeof trust !== 'string' || !TRUST_VALUES.has(trust as ProjectTrust))\n throw new Error(`Doom config at ${filePath} projectTrust must be ask, always, or never`);\n return {\n modes,\n projectTrust: trust as ProjectTrust,\n editor: parseEditor(parsed.editor, filePath),\n voice: parseVoice(parsed.voice, filePath),\n selection: parseSelection(parsed.selection, filePath),\n };\n}\nfunction readConfig(filePath: string): DoomConfig {\n return fs.existsSync(filePath)\n ? parseDoomConfig(fs.readFileSync(filePath, 'utf8'), filePath)\n : { projectTrust: DEFAULT_PROJECT_TRUST };\n}\n\nasync function readConfigAsync(filePath: string): Promise<DoomConfig> {\n try {\n return parseDoomConfig(await fs.promises.readFile(filePath, 'utf8'), filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { projectTrust: DEFAULT_PROJECT_TRUST };\n throw error;\n }\n}\nfunction mergeAgent(\n globalValue?: PlanningAgentConfig,\n repositoryValue?: PlanningAgentConfig,\n): PlanningAgentConfig | undefined {\n return globalValue || repositoryValue ? { ...globalValue, ...repositoryValue } : undefined;\n}\nexport function mergeDoomConfigs(globalConfig: DoomConfig, repositoryConfig: DoomConfig): DoomConfig {\n if (repositoryConfig.voice?.autoCapture)\n throw new Error('Repository Doom config voice.autoCapture is global-only; configure it in ~/.pi/.doom/config.yaml');\n const globalPlanning = globalConfig.modes?.planning;\n const repositoryPlanning = repositoryConfig.modes?.planning;\n const planning =\n globalPlanning || repositoryPlanning\n ? {\n main: mergeAgent(globalPlanning?.main, repositoryPlanning?.main),\n subagents: mergeAgent(globalPlanning?.subagents, repositoryPlanning?.subagents),\n plansDirectory: repositoryPlanning?.plansDirectory ?? globalPlanning?.plansDirectory,\n }\n : undefined;\n const voice = mergeVoice(globalConfig.voice, repositoryConfig.voice);\n return {\n modes: planning ? { planning } : undefined,\n projectTrust: repositoryConfig.projectTrust,\n editor: globalConfig.editor,\n voice,\n // Per key, so a repository can pin one axis and inherit the rest.\n selection:\n globalConfig.selection || repositoryConfig.selection\n ? { ...globalConfig.selection, ...repositoryConfig.selection }\n : undefined,\n };\n}\nfunction mergeAdapter(\n globalValue?: VoiceAdapterConfig,\n repositoryValue?: VoiceAdapterConfig,\n): VoiceAdapterConfig | undefined {\n if (!globalValue && !repositoryValue) return undefined;\n return { ...globalValue, ...repositoryValue, model: repositoryValue?.model ?? globalValue!.model };\n}\nfunction mergeVoice(globalValue?: VoiceConfig, repositoryValue?: VoiceConfig): VoiceConfig | undefined {\n if (!globalValue && !repositoryValue) return undefined;\n return {\n ...globalValue,\n ...repositoryValue,\n recorder:\n globalValue?.recorder || repositoryValue?.recorder\n ? { ...globalValue?.recorder, ...repositoryValue?.recorder }\n : undefined,\n adapters:\n globalValue?.adapters || repositoryValue?.adapters\n ? {\n [WHISPER_CPP_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[WHISPER_CPP_ENGINE],\n repositoryValue?.adapters?.[WHISPER_CPP_ENGINE],\n ),\n [OPENAI_WHISPER_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[OPENAI_WHISPER_ENGINE],\n repositoryValue?.adapters?.[OPENAI_WHISPER_ENGINE],\n ),\n [MLX_WHISPER_ENGINE]: mergeAdapter(\n globalValue?.adapters?.[MLX_WHISPER_ENGINE],\n repositoryValue?.adapters?.[MLX_WHISPER_ENGINE],\n ),\n }\n : undefined,\n ...(globalValue?.autoCapture ? { autoCapture: globalValue.autoCapture } : {}),\n };\n}\nexport function resolveVoiceConfig(config: VoiceConfig): ResolvedVoiceConfig {\n return {\n engine: config.engine ?? DEFAULT_VOICE_ENGINE,\n language: config.language ?? DEFAULT_VOICE_LANGUAGE,\n recorder: { ...config.recorder, device: config.recorder?.device ?? DEFAULT_RECORDER_DEVICE },\n adapters: config.adapters ?? {},\n ...(config.autoCapture\n ? {\n autoCapture: {\n ...config.autoCapture,\n startPhrases: [...(config.autoCapture.startPhrases ?? [])],\n stopPhrases: [...(config.autoCapture.stopPhrases ?? [])],\n utteranceIdleMs: config.autoCapture.utteranceIdleMs ?? DEFAULT_UTTERANCE_IDLE_MS,\n transcriptionTimeoutMs: config.autoCapture.transcriptionTimeoutMs ?? DEFAULT_TRANSCRIPTION_TIMEOUT_MS,\n tts: { ...config.autoCapture.tts },\n },\n }\n : {}),\n };\n}\nexport function globalDoomConfigDirectory(homeDirectory = os.homedir()): string {\n return path.join(homeDirectory, GLOBAL_CONFIG_DIR, DOOM_DIR);\n}\nexport function globalDoomConfigPath(homeDirectory = os.homedir()): string {\n return path.join(globalDoomConfigDirectory(homeDirectory), CONFIG_FILE);\n}\nexport function resolvePlanningPlansDirectory(\n configured: string | undefined,\n repoRoot: string,\n homeDirectory = os.homedir(),\n): string {\n if (!configured) return path.join(homeDirectory, GLOBAL_CONFIG_DIR, PLANS_DIR);\n if (configured === '~') return homeDirectory;\n if (configured.startsWith('~/')) return path.resolve(homeDirectory, configured.slice(2));\n if (configured.startsWith('~')) {\n throw new Error(\"Doom planning plansDirectory supports only '~' or '~/' home aliases\");\n }\n return path.isAbsolute(configured) ? path.normalize(configured) : path.resolve(repoRoot, configured);\n}\nexport function repositoryDoomConfigPath(repoRoot: string): string {\n return path.join(repoRoot, DOOM_DIR, CONFIG_FILE);\n}\nexport function loadDoomConfig(repoRoot: string, homeDirectory = os.homedir()): DoomConfig {\n return mergeDoomConfigs(\n readConfig(globalDoomConfigPath(homeDirectory)),\n readConfig(repositoryDoomConfigPath(repoRoot)),\n );\n}\n\nexport async function loadDoomConfigAsync(repoRoot: string, homeDirectory = os.homedir()): Promise<DoomConfig> {\n const globalConfig = await readConfigAsync(globalDoomConfigPath(homeDirectory));\n const repositoryConfig = await readConfigAsync(repositoryDoomConfigPath(repoRoot));\n return mergeDoomConfigs(globalConfig, repositoryConfig);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../src/adapters/config.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { mergeDoomConfigs, parseDoomConfig } from '../services/configPolicy.ts';\nimport type { DoomConfig } from '../types/config.ts';\n\nconst DOOM_DIR = '.doom';\nconst CONFIG_FILE = 'config.yaml';\nconst GLOBAL_CONFIG_DIR = '.pi';\nconst PLANS_DIR = 'plans';\n\nfunction readConfig(filePath: string): DoomConfig {\n return fs.existsSync(filePath)\n ? parseDoomConfig(fs.readFileSync(filePath, 'utf8'), filePath)\n : { projectTrust: 'ask' };\n}\n\nasync function readConfigAsync(filePath: string): Promise<DoomConfig> {\n try {\n return parseDoomConfig(await fs.promises.readFile(filePath, 'utf8'), filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { projectTrust: 'ask' };\n throw error;\n }\n}\n\nexport function globalDoomConfigDirectory(homeDirectory = os.homedir()): string {\n return path.join(homeDirectory, GLOBAL_CONFIG_DIR, DOOM_DIR);\n}\n\nexport function globalDoomConfigPath(homeDirectory = os.homedir()): string {\n return path.join(globalDoomConfigDirectory(homeDirectory), CONFIG_FILE);\n}\n\nexport function resolvePlanningPlansDirectory(\n configured: string | undefined,\n repoRoot: string,\n homeDirectory = os.homedir(),\n): string {\n if (!configured) return path.join(homeDirectory, GLOBAL_CONFIG_DIR, PLANS_DIR);\n if (configured === '~') return homeDirectory;\n if (configured.startsWith('~/')) return path.resolve(homeDirectory, configured.slice(2));\n if (configured.startsWith('~')) {\n throw new Error(\"Doom planning plansDirectory supports only '~' or '~/' home aliases\");\n }\n return path.isAbsolute(configured) ? path.normalize(configured) : path.resolve(repoRoot, configured);\n}\n\nexport function repositoryDoomConfigPath(repoRoot: string): string {\n return path.join(repoRoot, DOOM_DIR, CONFIG_FILE);\n}\n\nexport function loadDoomConfig(repoRoot: string, homeDirectory = os.homedir()): DoomConfig {\n return mergeDoomConfigs(\n readConfig(globalDoomConfigPath(homeDirectory)),\n readConfig(repositoryDoomConfigPath(repoRoot)),\n );\n}\n\nexport async function loadDoomConfigAsync(repoRoot: string, homeDirectory = os.homedir()): Promise<DoomConfig> {\n const globalConfig = await readConfigAsync(globalDoomConfigPath(homeDirectory));\n const repositoryConfig = await readConfigAsync(repositoryDoomConfigPath(repoRoot));\n return mergeDoomConfigs(globalConfig, repositoryConfig);\n}\n"],"mappings":"4JAMA,MAAM,EAAW,QACX,EAAc,cAIpB,SAAS,EAAW,EAA8B,CAChD,OAAO,EAAG,WAAW,CAAQ,EACzB,EAAgB,EAAG,aAAa,EAAU,MAAM,EAAG,CAAQ,EAC3D,CAAE,aAAc,KAAM,CAC5B,CAEA,eAAe,EAAgB,EAAuC,CACpE,GAAI,CACF,OAAO,EAAgB,MAAM,EAAG,SAAS,SAAS,EAAU,MAAM,EAAG,CAAQ,CAC/E,OAAS,EAAO,CACd,GAAK,EAAgC,OAAS,SAAU,MAAO,CAAE,aAAc,KAAM,EACrF,MAAM,CACR,CACF,CAEA,SAAgB,EAA0B,EAAgB,EAAG,QAAQ,EAAW,CAC9E,OAAO,EAAK,KAAK,EAAe,MAAmB,CAAQ,CAC7D,CAEA,SAAgB,EAAqB,EAAgB,EAAG,QAAQ,EAAW,CACzE,OAAO,EAAK,KAAK,EAA0B,CAAa,EAAG,CAAW,CACxE,CAEA,SAAgB,EACd,EACA,EACA,EAAgB,EAAG,QAAQ,EACnB,CACR,GAAI,CAAC,EAAY,OAAO,EAAK,KAAK,EAAe,MAAmB,OAAS,EAC7E,GAAI,IAAe,IAAK,OAAO,EAC/B,GAAI,EAAW,WAAW,IAAI,EAAG,OAAO,EAAK,QAAQ,EAAe,EAAW,MAAM,CAAC,CAAC,EACvF,GAAI,EAAW,WAAW,GAAG,EAC3B,MAAU,MAAM,qEAAqE,EAEvF,OAAO,EAAK,WAAW,CAAU,EAAI,EAAK,UAAU,CAAU,EAAI,EAAK,QAAQ,EAAU,CAAU,CACrG,CAEA,SAAgB,EAAyB,EAA0B,CACjE,OAAO,EAAK,KAAK,EAAU,EAAU,CAAW,CAClD,CAEA,SAAgB,EAAe,EAAkB,EAAgB,EAAG,QAAQ,EAAe,CACzF,OAAO,EACL,EAAW,EAAqB,CAAa,CAAC,EAC9C,EAAW,EAAyB,CAAQ,CAAC,CAC/C,CACF,CAEA,eAAsB,EAAoB,EAAkB,EAAgB,EAAG,QAAQ,EAAwB,CAC7G,IAAM,EAAe,MAAM,EAAgB,EAAqB,CAAa,CAAC,EACxE,EAAmB,MAAM,EAAgB,EAAyB,CAAQ,CAAC,EACjF,OAAO,EAAiB,EAAc,CAAgB,CACxD"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/configPolicy.cjs");let n=require("node:crypto"),r=require("node:path");r=e.__toESM(r,1);let i=require("yaml"),a=require("node:fs/promises");a=e.__toESM(a,1);async function o(e){try{return await a.default.readFile(e,`utf8`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return``;throw e}}function s(e){if(typeof e!=`object`||!e)return!1;let t=e.items;return Array.isArray(t)&&t.length===0}function c(e,t){for(let n=t.length-1;n>0;--n){let r=t.slice(0,n);if(e.hasIn(r)){if(!s(e.getIn(r,!0)))return;e.deleteIn(r)}}}async function l(e,s,l={}){if(s.length===0)return;let u=await o(e),d=(0,i.parseDocument)(u,{keepSourceTokens:!0});if(d.errors.length>0)throw Error(`Could not parse Doom config at ${e}`);for(let e of s){let t=[...e.keyPath];if(e.value!==void 0){d.setIn(t,e.value);continue}d.hasIn(t)&&(d.deleteIn(t),l.prune!==!1&&c(d,t))}let f=d.toString();t.parseDoomConfig(f,e),await a.default.mkdir(r.default.dirname(e),{mode:448,recursive:!0});let p=`${e}.${process.pid}.${(0,n.randomUUID)()}.tmp`;try{await a.default.writeFile(p,f,{encoding:`utf8`,mode:384}),await a.default.rename(p,e)}catch(e){throw await a.default.rm(p,{force:!0}),e}}async function u(e,t,n){await l(e,[{keyPath:t,value:n}])}async function d(e,t){await l(e,[{keyPath:t}])}exports.setDoomConfigValue=u,exports.unsetDoomConfigValue=d,exports.writeDoomConfigValues=l;
2
+ //# sourceMappingURL=configWriter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configWriter.cjs","names":["fs","parseDocument","path","randomUUID"],"sources":["../../src/adapters/configWriter.ts"],"sourcesContent":["/**\n * Writes into the global Doom config file, one key path at a time.\n *\n * Extracted from doom-file-edit, which owned the only writer in the repo and had\n * it hard-coded to `editor.command`. Several extensions now edit their own\n * settings from the config panel, and every one of them needs the same four\n * properties: keep the user's comments, never publish a file the parser would\n * reject, never leave a half-written file behind, and apply a set of related\n * edits together.\n *\n * That last one is not a convenience. `voice.adapters.<engine>.model` accepts a\n * path or an id but not both, so switching between them is a set and an unset\n * that must land in the same write, or the file is briefly invalid.\n *\n * Atomicity is the same rename trick as `atomicJson.ts` next door, and carries\n * the same caveat: it prevents torn reads, not lost updates. Two sessions with\n * the panel open still clobber each other, because the rename only publishes\n * what the caller already computed.\n *\n * AVOID:\n * - Treating this as a lock\n * - Writing an empty string; the parser rejects blank values, so clearing a key\n * means removing it\n */\n\nimport { randomUUID } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { parseDocument } from 'yaml';\nimport { parseDoomConfig } from '../services/configPolicy.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\n\nexport interface DoomConfigEdit {\n /** Key path into the document, e.g. `['voice', 'adapters', 'mlx-whisper', 'model', 'id']`. */\n readonly keyPath: readonly string[];\n /** The value to write. `undefined` removes the key. */\n readonly value?: string;\n}\n\nexport interface WriteDoomConfigOptions {\n /**\n * Remove ancestors left holding nothing. Defaults to true, and is not\n * cosmetic: an adapter left as `{}` fails validation, because the parser\n * requires a model once the adapter exists at all.\n */\n readonly prune?: boolean;\n}\n\nasync function readIfPresent(filePath: string): Promise<string> {\n try {\n return await fs.readFile(filePath, 'utf8');\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return '';\n throw error;\n }\n}\n\nfunction isEmptyCollection(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const items = (value as { items?: unknown }).items;\n return Array.isArray(items) && items.length === 0;\n}\n\n/** Drops ancestors of a removed key that are now empty, deepest first. */\nfunction pruneEmptyAncestors(document: ReturnType<typeof parseDocument>, keyPath: readonly string[]): void {\n for (let depth = keyPath.length - 1; depth > 0; depth -= 1) {\n const ancestor = keyPath.slice(0, depth);\n if (!document.hasIn(ancestor)) continue;\n if (!isEmptyCollection(document.getIn(ancestor, true))) return;\n document.deleteIn(ancestor);\n }\n}\n\n/**\n * Applies every edit as one document mutation, or throws having written nothing.\n *\n * Validation runs against the serialized result rather than each edit, so a pair\n * of edits only valid together is accepted, and a single edit that breaks an\n * invariant elsewhere is still caught.\n */\nexport async function writeDoomConfigValues(\n filePath: string,\n edits: readonly DoomConfigEdit[],\n options: WriteDoomConfigOptions = {},\n): Promise<void> {\n if (edits.length === 0) return;\n const content = await readIfPresent(filePath);\n // Not `content || '{}\\n'`: seeding an absent file with a flow map makes yaml\n // keep that style, so the first write produces `{ editor: { command: ... } }`\n // and every later one grows that one line. An empty source starts a block map.\n const document = parseDocument(content, { keepSourceTokens: true });\n if (document.errors.length > 0) throw new Error(`Could not parse Doom config at ${filePath}`);\n\n for (const edit of edits) {\n const keyPath = [...edit.keyPath];\n if (edit.value !== undefined) {\n document.setIn(keyPath, edit.value);\n continue;\n }\n // deleteIn throws when an ancestor is missing rather than treating the key as\n // already absent, and clearing an unset key is worth supporting.\n if (!document.hasIn(keyPath)) continue;\n document.deleteIn(keyPath);\n if (options.prune !== false) pruneEmptyAncestors(document, keyPath);\n }\n\n const output = document.toString();\n parseDoomConfig(output, filePath);\n\n await fs.mkdir(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await fs.writeFile(temporaryPath, output, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });\n await fs.rename(temporaryPath, filePath);\n } catch (error) {\n await fs.rm(temporaryPath, { force: true });\n throw error;\n }\n}\n\nexport async function setDoomConfigValue(filePath: string, keyPath: readonly string[], value: string): Promise<void> {\n await writeDoomConfigValues(filePath, [{ keyPath, value }]);\n}\n\nexport async function unsetDoomConfigValue(filePath: string, keyPath: readonly string[]): Promise<void> {\n await writeDoomConfigValues(filePath, [{ keyPath }]);\n}\n"],"mappings":"yOAkDA,eAAe,EAAc,EAAmC,CAC9D,GAAI,CACF,OAAO,MAAMA,EAAAA,QAAG,SAAS,EAAU,MAAM,CAC3C,OAAS,EAAO,CACd,GAAI,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,SAAU,MAAO,GACjF,MAAM,CACR,CACF,CAEA,SAAS,EAAkB,EAAyB,CAClD,GAAsB,OAAO,GAAU,WAAnC,EAA6C,MAAO,GACxD,IAAM,EAAS,EAA8B,MAC7C,OAAO,MAAM,QAAQ,CAAK,GAAK,EAAM,SAAW,CAClD,CAGA,SAAS,EAAoB,EAA4C,EAAkC,CACzG,IAAK,IAAI,EAAQ,EAAQ,OAAS,EAAG,EAAQ,EAAG,IAAY,CAC1D,IAAM,EAAW,EAAQ,MAAM,EAAG,CAAK,EAClC,KAAS,MAAM,CAAQ,EAC5B,IAAI,CAAC,EAAkB,EAAS,MAAM,EAAU,EAAI,CAAC,EAAG,OACxD,EAAS,SAAS,CAAQ,CAD8B,CAE1D,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAkC,CAAC,EACpB,CACf,GAAI,EAAM,SAAW,EAAG,OACxB,IAAM,EAAU,MAAM,EAAc,CAAQ,EAItC,GAAA,EAAWC,EAAAA,cAAAA,CAAc,EAAS,CAAE,iBAAkB,EAAK,CAAC,EAClE,GAAI,EAAS,OAAO,OAAS,EAAG,MAAU,MAAM,kCAAkC,GAAU,EAE5F,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAU,CAAC,GAAG,EAAK,OAAO,EAChC,GAAI,EAAK,QAAU,IAAA,GAAW,CAC5B,EAAS,MAAM,EAAS,EAAK,KAAK,EAClC,QACF,CAGK,EAAS,MAAM,CAAO,IAC3B,EAAS,SAAS,CAAO,EACrB,EAAQ,QAAU,IAAO,EAAoB,EAAU,CAAO,EACpE,CAEA,IAAM,EAAS,EAAS,SAAS,EACjC,EAAA,gBAAgB,EAAQ,CAAQ,EAEhC,MAAMD,EAAAA,QAAG,MAAME,EAAAA,QAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,IAAA,EAAGC,EAAAA,WAAAA,CAAW,EAAE,MACjE,GAAI,CACF,MAAMH,EAAAA,QAAG,UAAU,EAAe,EAAQ,CAAE,SAAU,OAAQ,KAAM,GAAkB,CAAC,EACvF,MAAMA,EAAAA,QAAG,OAAO,EAAe,CAAQ,CACzC,OAAS,EAAO,CAEd,MADA,MAAMA,EAAAA,QAAG,GAAG,EAAe,CAAE,MAAO,EAAK,CAAC,EACpC,CACR,CACF,CAEA,eAAsB,EAAmB,EAAkB,EAA4B,EAA8B,CACnH,MAAM,EAAsB,EAAU,CAAC,CAAE,UAAS,OAAM,CAAC,CAAC,CAC5D,CAEA,eAAsB,EAAqB,EAAkB,EAA2C,CACtG,MAAM,EAAsB,EAAU,CAAC,CAAE,SAAQ,CAAC,CAAC,CACrD"}
@@ -1,4 +1,4 @@
1
- //#region src/services/configWriter.d.ts
1
+ //#region src/adapters/configWriter.d.ts
2
2
  /**
3
3
  * Writes into the global Doom config file, one key path at a time.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"configWriter.d.cts","names":[],"sources":["../../src/services/configWriter.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;WAEN;;WAEA;;UAGM;;;;;;WAMN;;;;;;;;;iBAmCW,sBACpB,kBACA,gBAAgB,kBAChB,UAAS,yBACR;iBAoCmB,mBAAmB,kBAAkB,4BAA4B,gBAAgB;iBAIjF,qBAAqB,kBAAkB,6BAA6B"}
1
+ {"version":3,"file":"configWriter.d.cts","names":[],"sources":["../../src/adapters/configWriter.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;WAEN;;WAEA;;UAGM;;;;;;WAMN;;;;;;;;;iBAmCW,sBACpB,kBACA,gBAAgB,kBAChB,UAAS,yBACR;iBAoCmB,mBAAmB,kBAAkB,4BAA4B,gBAAgB;iBAIjF,qBAAqB,kBAAkB,6BAA6B"}
@@ -1,4 +1,4 @@
1
- //#region src/services/configWriter.d.ts
1
+ //#region src/adapters/configWriter.d.ts
2
2
  /**
3
3
  * Writes into the global Doom config file, one key path at a time.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"configWriter.d.mts","names":[],"sources":["../../src/services/configWriter.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;WAEN;;WAEA;;UAGM;;;;;;WAMN;;;;;;;;;iBAmCW,sBACpB,kBACA,gBAAgB,kBAChB,UAAS,yBACR;iBAoCmB,mBAAmB,kBAAkB,4BAA4B,gBAAgB;iBAIjF,qBAAqB,kBAAkB,6BAA6B"}
1
+ {"version":3,"file":"configWriter.d.mts","names":[],"sources":["../../src/adapters/configWriter.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;WAEN;;WAEA;;UAGM;;;;;;WAMN;;;;;;;;;iBAmCW,sBACpB,kBACA,gBAAgB,kBAChB,UAAS,yBACR;iBAoCmB,mBAAmB,kBAAkB,4BAA4B,gBAAgB;iBAIjF,qBAAqB,kBAAkB,6BAA6B"}
@@ -0,0 +1,2 @@
1
+ import{parseDoomConfig as e}from"../services/configPolicy.mjs";import{randomUUID as t}from"node:crypto";import n from"node:path";import{parseDocument as r}from"yaml";import i from"node:fs/promises";async function a(e){try{return await i.readFile(e,`utf8`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return``;throw e}}function o(e){if(typeof e!=`object`||!e)return!1;let t=e.items;return Array.isArray(t)&&t.length===0}function s(e,t){for(let n=t.length-1;n>0;--n){let r=t.slice(0,n);if(e.hasIn(r)){if(!o(e.getIn(r,!0)))return;e.deleteIn(r)}}}async function c(o,c,l={}){if(c.length===0)return;let u=await a(o),d=r(u,{keepSourceTokens:!0});if(d.errors.length>0)throw Error(`Could not parse Doom config at ${o}`);for(let e of c){let t=[...e.keyPath];if(e.value!==void 0){d.setIn(t,e.value);continue}d.hasIn(t)&&(d.deleteIn(t),l.prune!==!1&&s(d,t))}let f=d.toString();e(f,o),await i.mkdir(n.dirname(o),{mode:448,recursive:!0});let p=`${o}.${process.pid}.${t()}.tmp`;try{await i.writeFile(p,f,{encoding:`utf8`,mode:384}),await i.rename(p,o)}catch(e){throw await i.rm(p,{force:!0}),e}}async function l(e,t,n){await c(e,[{keyPath:t,value:n}])}async function u(e,t){await c(e,[{keyPath:t}])}export{l as setDoomConfigValue,u as unsetDoomConfigValue,c as writeDoomConfigValues};
2
+ //# sourceMappingURL=configWriter.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configWriter.mjs","names":[],"sources":["../../src/adapters/configWriter.ts"],"sourcesContent":["/**\n * Writes into the global Doom config file, one key path at a time.\n *\n * Extracted from doom-file-edit, which owned the only writer in the repo and had\n * it hard-coded to `editor.command`. Several extensions now edit their own\n * settings from the config panel, and every one of them needs the same four\n * properties: keep the user's comments, never publish a file the parser would\n * reject, never leave a half-written file behind, and apply a set of related\n * edits together.\n *\n * That last one is not a convenience. `voice.adapters.<engine>.model` accepts a\n * path or an id but not both, so switching between them is a set and an unset\n * that must land in the same write, or the file is briefly invalid.\n *\n * Atomicity is the same rename trick as `atomicJson.ts` next door, and carries\n * the same caveat: it prevents torn reads, not lost updates. Two sessions with\n * the panel open still clobber each other, because the rename only publishes\n * what the caller already computed.\n *\n * AVOID:\n * - Treating this as a lock\n * - Writing an empty string; the parser rejects blank values, so clearing a key\n * means removing it\n */\n\nimport { randomUUID } from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { parseDocument } from 'yaml';\nimport { parseDoomConfig } from '../services/configPolicy.ts';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIRECTORY_MODE = 0o700;\n\nexport interface DoomConfigEdit {\n /** Key path into the document, e.g. `['voice', 'adapters', 'mlx-whisper', 'model', 'id']`. */\n readonly keyPath: readonly string[];\n /** The value to write. `undefined` removes the key. */\n readonly value?: string;\n}\n\nexport interface WriteDoomConfigOptions {\n /**\n * Remove ancestors left holding nothing. Defaults to true, and is not\n * cosmetic: an adapter left as `{}` fails validation, because the parser\n * requires a model once the adapter exists at all.\n */\n readonly prune?: boolean;\n}\n\nasync function readIfPresent(filePath: string): Promise<string> {\n try {\n return await fs.readFile(filePath, 'utf8');\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return '';\n throw error;\n }\n}\n\nfunction isEmptyCollection(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n const items = (value as { items?: unknown }).items;\n return Array.isArray(items) && items.length === 0;\n}\n\n/** Drops ancestors of a removed key that are now empty, deepest first. */\nfunction pruneEmptyAncestors(document: ReturnType<typeof parseDocument>, keyPath: readonly string[]): void {\n for (let depth = keyPath.length - 1; depth > 0; depth -= 1) {\n const ancestor = keyPath.slice(0, depth);\n if (!document.hasIn(ancestor)) continue;\n if (!isEmptyCollection(document.getIn(ancestor, true))) return;\n document.deleteIn(ancestor);\n }\n}\n\n/**\n * Applies every edit as one document mutation, or throws having written nothing.\n *\n * Validation runs against the serialized result rather than each edit, so a pair\n * of edits only valid together is accepted, and a single edit that breaks an\n * invariant elsewhere is still caught.\n */\nexport async function writeDoomConfigValues(\n filePath: string,\n edits: readonly DoomConfigEdit[],\n options: WriteDoomConfigOptions = {},\n): Promise<void> {\n if (edits.length === 0) return;\n const content = await readIfPresent(filePath);\n // Not `content || '{}\\n'`: seeding an absent file with a flow map makes yaml\n // keep that style, so the first write produces `{ editor: { command: ... } }`\n // and every later one grows that one line. An empty source starts a block map.\n const document = parseDocument(content, { keepSourceTokens: true });\n if (document.errors.length > 0) throw new Error(`Could not parse Doom config at ${filePath}`);\n\n for (const edit of edits) {\n const keyPath = [...edit.keyPath];\n if (edit.value !== undefined) {\n document.setIn(keyPath, edit.value);\n continue;\n }\n // deleteIn throws when an ancestor is missing rather than treating the key as\n // already absent, and clearing an unset key is worth supporting.\n if (!document.hasIn(keyPath)) continue;\n document.deleteIn(keyPath);\n if (options.prune !== false) pruneEmptyAncestors(document, keyPath);\n }\n\n const output = document.toString();\n parseDoomConfig(output, filePath);\n\n await fs.mkdir(path.dirname(filePath), { mode: PRIVATE_DIRECTORY_MODE, recursive: true });\n const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await fs.writeFile(temporaryPath, output, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });\n await fs.rename(temporaryPath, filePath);\n } catch (error) {\n await fs.rm(temporaryPath, { force: true });\n throw error;\n }\n}\n\nexport async function setDoomConfigValue(filePath: string, keyPath: readonly string[], value: string): Promise<void> {\n await writeDoomConfigValues(filePath, [{ keyPath, value }]);\n}\n\nexport async function unsetDoomConfigValue(filePath: string, keyPath: readonly string[]): Promise<void> {\n await writeDoomConfigValues(filePath, [{ keyPath }]);\n}\n"],"mappings":"sMAkDA,eAAe,EAAc,EAAmC,CAC9D,GAAI,CACF,OAAO,MAAM,EAAG,SAAS,EAAU,MAAM,CAC3C,OAAS,EAAO,CACd,GAAI,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,SAAU,MAAO,GACjF,MAAM,CACR,CACF,CAEA,SAAS,EAAkB,EAAyB,CAClD,GAAsB,OAAO,GAAU,WAAnC,EAA6C,MAAO,GACxD,IAAM,EAAS,EAA8B,MAC7C,OAAO,MAAM,QAAQ,CAAK,GAAK,EAAM,SAAW,CAClD,CAGA,SAAS,EAAoB,EAA4C,EAAkC,CACzG,IAAK,IAAI,EAAQ,EAAQ,OAAS,EAAG,EAAQ,EAAG,IAAY,CAC1D,IAAM,EAAW,EAAQ,MAAM,EAAG,CAAK,EAClC,KAAS,MAAM,CAAQ,EAC5B,IAAI,CAAC,EAAkB,EAAS,MAAM,EAAU,EAAI,CAAC,EAAG,OACxD,EAAS,SAAS,CAAQ,CAD8B,CAE1D,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAkC,CAAC,EACpB,CACf,GAAI,EAAM,SAAW,EAAG,OACxB,IAAM,EAAU,MAAM,EAAc,CAAQ,EAItC,EAAW,EAAc,EAAS,CAAE,iBAAkB,EAAK,CAAC,EAClE,GAAI,EAAS,OAAO,OAAS,EAAG,MAAU,MAAM,kCAAkC,GAAU,EAE5F,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAU,CAAC,GAAG,EAAK,OAAO,EAChC,GAAI,EAAK,QAAU,IAAA,GAAW,CAC5B,EAAS,MAAM,EAAS,EAAK,KAAK,EAClC,QACF,CAGK,EAAS,MAAM,CAAO,IAC3B,EAAS,SAAS,CAAO,EACrB,EAAQ,QAAU,IAAO,EAAoB,EAAU,CAAO,EACpE,CAEA,IAAM,EAAS,EAAS,SAAS,EACjC,EAAgB,EAAQ,CAAQ,EAEhC,MAAM,EAAG,MAAM,EAAK,QAAQ,CAAQ,EAAG,CAAE,KAAM,IAAwB,UAAW,EAAK,CAAC,EACxF,IAAM,EAAgB,GAAG,EAAS,GAAG,QAAQ,IAAI,GAAG,EAAW,EAAE,MACjE,GAAI,CACF,MAAM,EAAG,UAAU,EAAe,EAAQ,CAAE,SAAU,OAAQ,KAAM,GAAkB,CAAC,EACvF,MAAM,EAAG,OAAO,EAAe,CAAQ,CACzC,OAAS,EAAO,CAEd,MADA,MAAM,EAAG,GAAG,EAAe,CAAE,MAAO,EAAK,CAAC,EACpC,CACR,CACF,CAEA,eAAsB,EAAmB,EAAkB,EAA4B,EAA8B,CACnH,MAAM,EAAsB,EAAU,CAAC,CAAE,UAAS,OAAM,CAAC,CAAC,CAC5D,CAEA,eAAsB,EAAqB,EAAkB,EAA2C,CACtG,MAAM,EAAsB,EAAU,CAAC,CAAE,SAAQ,CAAC,CAAC,CACrD"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../services/domains.cjs"),n=require("./layeredConfig.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:os");a=e.__toESM(a,1);const o=`.doom/domains.yaml`;function s(e){if(!Array.isArray(e)||e.some(e=>typeof e!=`string`||!e.trim()))throw Error(`defaultDomains in ${o} must be an array of non-empty strings`);return[...new Set(e.map(e=>e.trim()))]}function c(e,r=a.default.homedir()){let i=n.readDoomConfigSources(`domains.yaml`,e,r),c={},l;for(let e of i)Object.assign(c,e.document.aliases??{}),Object.hasOwn(e.document,`defaultDomains`)&&(l=s(e.document.defaultDomains));let u=n.mergeNamedSources(i,e=>e.domains);if(l){for(let e of t.expandDomainNames({domains:u,aliases:c},l))if(!Object.hasOwn(u,e))throw Error(`Unknown default domain "${e}" in ${o}`)}return{domains:u,aliases:c,...l?{defaultDomains:l}:{}}}function l(e,t){let n=c(e,t);return[...new Set([...Object.keys(n.domains),...Object.keys(n.aliases)])].sort()}function u(e,n,r){let i=c(e,r),a=t.expandDomainNames(i,n);return a.length===0||a.some(e=>i.domains[e]?.sharedSkills!==!1)}function d(e,n,a,o){let s=c(e,o),l=[],u=new Set;for(let e of t.expandDomainNames(s,n)){let t=s.domains[e];if(!t)throw Error(`Unknown domain: ${e}`);for(let e of t.plugins??[]){let n=typeof e!=`string`,a=i.default.resolve(t.baseDirectory,n?e.dir:e);if(!r.default.existsSync(a))throw Error(`Domain plugin does not exist: ${a}`);u.has(a)||(u.add(a),l.push(n?{directory:a,skills:e.skills,agents:e.agents,hooks:e.hooks,mcp:e.mcp}:{directory:a}))}}for(let e of a){if(!r.default.existsSync(e))throw Error(`Plugin directory does not exist: ${e}`);let t=i.default.resolve(e);u.has(t)||(u.add(t),l.push({directory:t}))}return l}function f(e,t,n,r){return d(e,t,n,r).map(e=>e.directory)}exports.DOOM_DIR=n.DOOM_DIR,exports.listDomainNames=l,exports.loadDomains=c,exports.resolvePluginDirectories=f,exports.resolvePluginEntries=d,exports.resolveSharedSkills=u;
2
+ //# sourceMappingURL=domains.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domains.cjs","names":["os","readDoomConfigSources","mergeNamedSources","expandDomainNames","path","fs"],"sources":["../../src/adapters/domains.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { expandDomainNames } from '../services/domains.ts';\nimport type { DomainDefinition, DomainManifest, PluginEntry } from '../types/domains.ts';\nimport { mergeNamedSources, readDoomConfigSources } from './layeredConfig.ts';\n\nexport { DOOM_DIR } from './layeredConfig.ts';\n\ninterface DomainDocument {\n domains?: Record<string, DomainDefinition>;\n aliases?: Record<string, string[]>;\n defaultDomains?: unknown;\n}\n\nconst DOMAINS_FILE = 'domains.yaml';\nconst UNKNOWN_DOMAIN_ERROR = 'Unknown domain';\nconst MISSING_DOMAIN_PLUGIN_ERROR = 'Domain plugin does not exist';\nconst MISSING_PLUGIN_DIRECTORY_ERROR = 'Plugin directory does not exist';\nconst DEFAULT_DOMAINS_PATH = '.doom/domains.yaml';\n\nfunction parseDefaultDomains(value: unknown): string[] {\n if (!Array.isArray(value) || value.some((name) => typeof name !== 'string' || !name.trim())) {\n throw new Error(`defaultDomains in ${DEFAULT_DOMAINS_PATH} must be an array of non-empty strings`);\n }\n return [...new Set(value.map((name: string) => name.trim()))];\n}\n\n/**\n * Reads domains from the global and repository configs, repository last.\n *\n * Either file alone is a complete manifest. A repository domain replaces the\n * global domain of the same name outright, and a repository defaultDomains\n * list replaces the global list when present.\n */\nexport function loadDomains(repoRoot: string, homeDirectory: string = os.homedir()): DomainManifest {\n const sources = readDoomConfigSources<DomainDocument>(DOMAINS_FILE, repoRoot, homeDirectory);\n const aliases: Record<string, string[]> = {};\n let defaultDomains: string[] | undefined;\n for (const source of sources) {\n Object.assign(aliases, source.document.aliases ?? {});\n if (Object.hasOwn(source.document, 'defaultDomains')) {\n defaultDomains = parseDefaultDomains(source.document.defaultDomains);\n }\n }\n const domains = mergeNamedSources(sources, (document) => document.domains);\n if (defaultDomains) {\n for (const name of expandDomainNames({ domains, aliases }, defaultDomains)) {\n if (!Object.hasOwn(domains, name)) {\n throw new Error(`Unknown default domain \"${name}\" in ${DEFAULT_DOMAINS_PATH}`);\n }\n }\n }\n return { domains, aliases, ...(defaultDomains ? { defaultDomains } : {}) };\n}\n\nexport function listDomainNames(repoRoot: string, homeDirectory?: string): string[] {\n const manifest = loadDomains(repoRoot, homeDirectory);\n return [...new Set([...Object.keys(manifest.domains), ...Object.keys(manifest.aliases)])].sort();\n}\n\n/**\n * Whether to prepend .claude/skills.\n *\n * Shared skills are included unless every selected domain opts out, so mixing a\n * trimmed domain with an untrimmed one cannot strip the untrimmed one's skills.\n */\nexport function resolveSharedSkills(repoRoot: string, domainNames: string[], homeDirectory?: string): boolean {\n const manifest = loadDomains(repoRoot, homeDirectory);\n const expanded = expandDomainNames(manifest, domainNames);\n if (expanded.length === 0) return true;\n return expanded.some((name) => manifest.domains[name]?.sharedSkills !== false);\n}\n\n/** Resolves domains to plugin entries, preserving each plugin's subset filter. */\nexport function resolvePluginEntries(\n repoRoot: string,\n domainNames: string[],\n explicitDirectories: string[],\n homeDirectory?: string,\n): PluginEntry[] {\n const manifest = loadDomains(repoRoot, homeDirectory);\n const entries: PluginEntry[] = [];\n const seen = new Set<string>();\n\n for (const name of expandDomainNames(manifest, domainNames)) {\n const domain = manifest.domains[name];\n if (!domain) throw new Error(`${UNKNOWN_DOMAIN_ERROR}: ${name}`);\n for (const plugin of domain.plugins ?? []) {\n const isSubset = typeof plugin !== 'string';\n const directory = path.resolve(domain.baseDirectory, isSubset ? plugin.dir : plugin);\n if (!fs.existsSync(directory)) throw new Error(`${MISSING_DOMAIN_PLUGIN_ERROR}: ${directory}`);\n if (seen.has(directory)) continue;\n seen.add(directory);\n entries.push(\n isSubset\n ? { directory, skills: plugin.skills, agents: plugin.agents, hooks: plugin.hooks, mcp: plugin.mcp }\n : { directory },\n );\n }\n }\n\n for (const directory of explicitDirectories) {\n if (!fs.existsSync(directory)) throw new Error(`${MISSING_PLUGIN_DIRECTORY_ERROR}: ${directory}`);\n const resolved = path.resolve(directory);\n if (seen.has(resolved)) continue;\n seen.add(resolved);\n entries.push({ directory: resolved });\n }\n return entries;\n}\n\nexport function resolvePluginDirectories(\n repoRoot: string,\n domainNames: string[],\n explicitDirectories: string[],\n homeDirectory?: string,\n): string[] {\n return resolvePluginEntries(repoRoot, domainNames, explicitDirectories, homeDirectory).map(\n (entry) => entry.directory,\n );\n}\n"],"mappings":"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"}