@agimon-ai/po-config 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/agentPluginMcp.cjs +322 -0
- package/dist/agentPluginMcp.cjs.map +1 -0
- package/dist/agentPluginMcp.d.mts +38 -0
- package/dist/agentPluginMcp.d.mts.map +1 -0
- package/dist/agentPluginMcp.mjs +318 -0
- package/dist/agentPluginMcp.mjs.map +1 -0
- package/dist/apiContracts.cjs +203 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +198 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/atomicJson-1W0WhuFF.mjs +44 -0
- package/dist/atomicJson-1W0WhuFF.mjs.map +1 -0
- package/dist/atomicJson-Dt2z_DXl.cjs +52 -0
- package/dist/atomicJson-Dt2z_DXl.cjs.map +1 -0
- package/dist/atomicJson.cjs +3 -0
- package/dist/atomicJson.d.mts +2 -0
- package/dist/atomicJson.mjs +2 -0
- package/dist/config-BUw5kHsx.cjs +219 -0
- package/dist/config-BUw5kHsx.cjs.map +1 -0
- package/dist/config-CKA07FxG.mjs +162 -0
- package/dist/config-CKA07FxG.mjs.map +1 -0
- package/dist/config-Cm3EV1jp.d.mts +320 -0
- package/dist/config-Cm3EV1jp.d.mts.map +1 -0
- package/dist/config-NSHRd6na.mjs +2 -0
- package/dist/config-zZz80Mh5.cjs +1 -0
- package/dist/config.cjs +24 -0
- package/dist/config.d.mts +3 -0
- package/dist/config.mjs +3 -0
- package/dist/configPolicy--TNX7feH.mjs +718 -0
- package/dist/configPolicy--TNX7feH.mjs.map +1 -0
- package/dist/configPolicy-DmsZEp0t.cjs +795 -0
- package/dist/configPolicy-DmsZEp0t.cjs.map +1 -0
- package/dist/configSchema.cjs +0 -0
- package/dist/configSchema.d.mts +2 -0
- package/dist/configSchema.mjs +1 -0
- package/dist/configWriter-BTuinLjs.cjs +150 -0
- package/dist/configWriter-BTuinLjs.cjs.map +1 -0
- package/dist/configWriter-BgkGaLrf.mjs +130 -0
- package/dist/configWriter-BgkGaLrf.mjs.map +1 -0
- package/dist/configWriter.cjs +5 -0
- package/dist/configWriter.d.mts +2 -0
- package/dist/configWriter.mjs +2 -0
- package/dist/domains-CBYkuxs5.cjs +727 -0
- package/dist/domains-CBYkuxs5.cjs.map +1 -0
- package/dist/domains-DZOu9vYM.mjs +633 -0
- package/dist/domains-DZOu9vYM.mjs.map +1 -0
- package/dist/domains.cjs +19 -0
- package/dist/domains.d.mts +3 -0
- package/dist/domains.mjs +3 -0
- package/dist/extensions/mcp-ui.mjs +101 -0
- package/dist/extensions/mcp-ui.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +171 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +166 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +504 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +499 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/harnessState-CCyBeutB.cjs +188 -0
- package/dist/harnessState-CCyBeutB.cjs.map +1 -0
- package/dist/harnessState-Dn_S7fB9.mjs +163 -0
- package/dist/harnessState-Dn_S7fB9.mjs.map +1 -0
- package/dist/harnessState.cjs +6 -0
- package/dist/harnessState.d.mts +2 -0
- package/dist/harnessState.mjs +2 -0
- package/dist/harnessStore-CJSxdSkV.cjs +343 -0
- package/dist/harnessStore-CJSxdSkV.cjs.map +1 -0
- package/dist/harnessStore-CltwLB7M.mjs +262 -0
- package/dist/harnessStore-CltwLB7M.mjs.map +1 -0
- package/dist/harnessStore.cjs +15 -0
- package/dist/harnessStore.d.mts +2 -0
- package/dist/harnessStore.mjs +2 -0
- package/dist/index-BAaTySaM.d.mts +35 -0
- package/dist/index-BAaTySaM.d.mts.map +1 -0
- package/dist/index-BT29GZx5.d.mts +142 -0
- package/dist/index-BT29GZx5.d.mts.map +1 -0
- package/dist/index-B_lp0W3U.d.mts +114 -0
- package/dist/index-B_lp0W3U.d.mts.map +1 -0
- package/dist/index-C6GaYjq-.d.mts +6 -0
- package/dist/index-C6GaYjq-.d.mts.map +1 -0
- package/dist/index-CIQrKS1A.d.mts +50 -0
- package/dist/index-CIQrKS1A.d.mts.map +1 -0
- package/dist/index-CRcoVYXz.d.mts +39 -0
- package/dist/index-CRcoVYXz.d.mts.map +1 -0
- package/dist/index-CVlciCJF.d.mts +40 -0
- package/dist/index-CVlciCJF.d.mts.map +1 -0
- package/dist/index-CeOk2kqs.d.mts +118 -0
- package/dist/index-CeOk2kqs.d.mts.map +1 -0
- package/dist/index-Ch7ptOkW.d.mts +54 -0
- package/dist/index-Ch7ptOkW.d.mts.map +1 -0
- package/dist/index-Cvdt-Y-i.d.mts +66 -0
- package/dist/index-Cvdt-Y-i.d.mts.map +1 -0
- package/dist/index-DJpG-I9K.d.mts +122 -0
- package/dist/index-DJpG-I9K.d.mts.map +1 -0
- package/dist/index-DNk4q0Qm.d.mts +81 -0
- package/dist/index-DNk4q0Qm.d.mts.map +1 -0
- package/dist/index.cjs +121 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +15 -0
- package/dist/init-C_9nyp3-.cjs +448 -0
- package/dist/init-C_9nyp3-.cjs.map +1 -0
- package/dist/init-DJ6wm-dr.mjs +415 -0
- package/dist/init-DJ6wm-dr.mjs.map +1 -0
- package/dist/init.cjs +7 -0
- package/dist/init.d.mts +2 -0
- package/dist/init.mjs +2 -0
- package/dist/layeredConfig-B922ibgn.mjs +67 -0
- package/dist/layeredConfig-B922ibgn.mjs.map +1 -0
- package/dist/layeredConfig-DdI5ysUf.cjs +94 -0
- package/dist/layeredConfig-DdI5ysUf.cjs.map +1 -0
- package/dist/layeredConfig.cjs +6 -0
- package/dist/layeredConfig.d.mts +2 -0
- package/dist/layeredConfig.mjs +2 -0
- package/dist/majorModes-CmPLEpMT.mjs +333 -0
- package/dist/majorModes-CmPLEpMT.mjs.map +1 -0
- package/dist/majorModes-DPhVMuhT.cjs +383 -0
- package/dist/majorModes-DPhVMuhT.cjs.map +1 -0
- package/dist/majorModes.cjs +10 -0
- package/dist/majorModes.d.mts +2 -0
- package/dist/majorModes.mjs +2 -0
- package/dist/piConfig-Bx26HMb_.d.mts +33 -0
- package/dist/piConfig-Bx26HMb_.d.mts.map +1 -0
- package/dist/piConfig-CCBdZXQr.cjs +260 -0
- package/dist/piConfig-CCBdZXQr.cjs.map +1 -0
- package/dist/piConfig-qGyCbcVj.mjs +197 -0
- package/dist/piConfig-qGyCbcVj.mjs.map +1 -0
- package/dist/piConfig.cjs +12 -0
- package/dist/piConfig.d.mts +2 -0
- package/dist/piConfig.mjs +2 -0
- package/dist/piContext.cjs +18 -0
- package/dist/piContext.d.mts +3 -0
- package/dist/piContext.mjs +2 -0
- package/dist/profiles-CTrUCmIh.d.mts +27 -0
- package/dist/profiles-CTrUCmIh.d.mts.map +1 -0
- package/dist/profiles-DnF9DBpd.cjs +552 -0
- package/dist/profiles-DnF9DBpd.cjs.map +1 -0
- package/dist/profiles-fujQKrS0.mjs +483 -0
- package/dist/profiles-fujQKrS0.mjs.map +1 -0
- package/dist/profiles.cjs +13 -0
- package/dist/profiles.d.mts +25 -0
- package/dist/profiles.d.mts.map +1 -0
- package/dist/profiles.mjs +2 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/selectionSwitch.cjs +70 -0
- package/dist/selectionSwitch.cjs.map +1 -0
- package/dist/selectionSwitch.d.mts +30 -0
- package/dist/selectionSwitch.d.mts.map +1 -0
- package/dist/selectionSwitch.mjs +64 -0
- package/dist/selectionSwitch.mjs.map +1 -0
- package/dist/sessionConfig-Boh4Fdje.cjs +342 -0
- package/dist/sessionConfig-Boh4Fdje.cjs.map +1 -0
- package/dist/sessionConfig-CtLoc-lO.mjs +240 -0
- package/dist/sessionConfig-CtLoc-lO.mjs.map +1 -0
- package/dist/types.cjs +9 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +2 -0
- package/llms.txt +12 -0
- package/package.json +206 -0
- package/src/prompts/po-author-config/SKILL.md +26 -0
- package/src/prompts/po-author-config/agents/openai.yaml +4 -0
- package/src/prompts/po-author-config/references/config-contract.md +201 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { parse } from "yaml";
|
|
2
|
+
//#region src/services/configPolicy/index.ts
|
|
3
|
+
const DEFAULT_PROJECT_TRUST = "ask";
|
|
4
|
+
const DEFAULT_VOICE_MODE = "legacy";
|
|
5
|
+
const DEFAULT_VOICE_ENGINE = "auto";
|
|
6
|
+
const DEFAULT_VOICE_LANGUAGE = "auto";
|
|
7
|
+
const DEFAULT_RECORDER_DEVICE = "none:default";
|
|
8
|
+
const ROOT_KEYS = [
|
|
9
|
+
"modes",
|
|
10
|
+
"computerUse",
|
|
11
|
+
"projectTrust",
|
|
12
|
+
"editor",
|
|
13
|
+
"voice",
|
|
14
|
+
"selection",
|
|
15
|
+
"web",
|
|
16
|
+
"native"
|
|
17
|
+
];
|
|
18
|
+
const SELECTION_KEYS = [
|
|
19
|
+
"majorMode",
|
|
20
|
+
"domains",
|
|
21
|
+
"profile"
|
|
22
|
+
];
|
|
23
|
+
const MODE_KEYS = ["planning", "autocompact"];
|
|
24
|
+
const PLANNING_KEYS = [
|
|
25
|
+
"main",
|
|
26
|
+
"subagents",
|
|
27
|
+
"plansDirectory"
|
|
28
|
+
];
|
|
29
|
+
const AGENT_KEYS = ["model", "thinking"];
|
|
30
|
+
const AUTOCOMPACT_KEYS = [
|
|
31
|
+
"enabled",
|
|
32
|
+
"model",
|
|
33
|
+
"thinking",
|
|
34
|
+
"thresholds",
|
|
35
|
+
"overrides"
|
|
36
|
+
];
|
|
37
|
+
const AUTOCOMPACT_THRESHOLD_KEYS = [
|
|
38
|
+
"pass1",
|
|
39
|
+
"pass2",
|
|
40
|
+
"pass3"
|
|
41
|
+
];
|
|
42
|
+
const AUTOCOMPACT_OVERRIDE_KEYS = ["model", "tokens"];
|
|
43
|
+
const COMPUTER_USE_KEYS = ["enabled"];
|
|
44
|
+
/** A pass that fires below this is noise; one above it never fires before Pi compacts natively. */
|
|
45
|
+
const MIN_AUTOCOMPACT_RATIO = .05;
|
|
46
|
+
const MAX_AUTOCOMPACT_RATIO = .99;
|
|
47
|
+
/**
|
|
48
|
+
* A checkpoint at zero tokens fires before the session has said anything. There is
|
|
49
|
+
* deliberately no ceiling: the package takes the lower of the ratio and the token
|
|
50
|
+
* value, so one larger than the window is inert rather than dangerous.
|
|
51
|
+
*/
|
|
52
|
+
const MIN_AUTOCOMPACT_TOKENS = 1;
|
|
53
|
+
const EDITOR_KEYS = ["command"];
|
|
54
|
+
const VOICE_KEYS = [
|
|
55
|
+
"mode",
|
|
56
|
+
"engine",
|
|
57
|
+
"language",
|
|
58
|
+
"recorder",
|
|
59
|
+
"adapters",
|
|
60
|
+
"autoCapture"
|
|
61
|
+
];
|
|
62
|
+
const RECORDER_KEYS = ["binary", "device"];
|
|
63
|
+
const WHISPER_CPP_ENGINE = "whisper-cpp";
|
|
64
|
+
const OPENAI_WHISPER_ENGINE = "openai-whisper";
|
|
65
|
+
const MLX_WHISPER_ENGINE = "mlx-whisper";
|
|
66
|
+
const MACOS_SAY_TTS_ENGINE = "macos-say";
|
|
67
|
+
const ADAPTERS_KEYS = [
|
|
68
|
+
WHISPER_CPP_ENGINE,
|
|
69
|
+
OPENAI_WHISPER_ENGINE,
|
|
70
|
+
MLX_WHISPER_ENGINE
|
|
71
|
+
];
|
|
72
|
+
const ADAPTER_KEYS = ["binary", "model"];
|
|
73
|
+
const MODEL_KEYS = ["path", "id"];
|
|
74
|
+
const AUTO_CAPTURE_KEYS = [
|
|
75
|
+
"model",
|
|
76
|
+
"startPhrases",
|
|
77
|
+
"stopPhrases",
|
|
78
|
+
"composeOpenPhrases",
|
|
79
|
+
"composeSendPhrases",
|
|
80
|
+
"composeCancelPhrases",
|
|
81
|
+
"utteranceIdleMs",
|
|
82
|
+
"composeUtteranceIdleMs",
|
|
83
|
+
"composeNudgeMs",
|
|
84
|
+
"transcriptionTimeoutMs",
|
|
85
|
+
"tts"
|
|
86
|
+
];
|
|
87
|
+
const TTS_KEYS = [
|
|
88
|
+
"engine",
|
|
89
|
+
"voice",
|
|
90
|
+
"rate"
|
|
91
|
+
];
|
|
92
|
+
const DEFAULT_UTTERANCE_IDLE_MS = 3e3;
|
|
93
|
+
const MIN_UTTERANCE_IDLE_MS = 1500;
|
|
94
|
+
const MAX_UTTERANCE_IDLE_MS = 1e4;
|
|
95
|
+
/**
|
|
96
|
+
* The endpoint window used while a composition draft is collecting.
|
|
97
|
+
*
|
|
98
|
+
* Shorter than `utteranceIdleMs` on purpose. A turn produces exactly one transcript, so
|
|
99
|
+
* with the ordinary ~3 s window a short command spoken after a brief pause arrives glued
|
|
100
|
+
* to the end of the preceding sentence and can never be read as a command. Roughly twice
|
|
101
|
+
* the VAD's own 600 ms trailing-silence window: long enough that ordinary speech rhythm
|
|
102
|
+
* does not split a sentence, short enough that a deliberate pause does. Over-splitting is
|
|
103
|
+
* harmless here and only here, because draft segments are joined back together with a
|
|
104
|
+
* space.
|
|
105
|
+
*/
|
|
106
|
+
const DEFAULT_COMPOSE_UTTERANCE_IDLE_MS = 1200;
|
|
107
|
+
const MIN_COMPOSE_UTTERANCE_IDLE_MS = 800;
|
|
108
|
+
const MAX_COMPOSE_UTTERANCE_IDLE_MS = 3e3;
|
|
109
|
+
/** Silence before Voice reminds the user a draft is still open. `0` disables it. */
|
|
110
|
+
const DEFAULT_COMPOSE_NUDGE_MS = 1e4;
|
|
111
|
+
const MIN_COMPOSE_NUDGE_MS = 5e3;
|
|
112
|
+
const MAX_COMPOSE_NUDGE_MS = 6e4;
|
|
113
|
+
/**
|
|
114
|
+
* Defaults kept as lists so the phrases that shipped first keep working.
|
|
115
|
+
*
|
|
116
|
+
* `hey doom` and `that's it` read as speech; `doom prompt` and `doom send` are what
|
|
117
|
+
* existing users have in their fingers. Dropping the originals would break them for no
|
|
118
|
+
* gain, and the cost of carrying both is two extra comparisons per command candidate.
|
|
119
|
+
*/
|
|
120
|
+
const DEFAULT_COMPOSE_OPEN_PHRASES = ["hey doom", "doom prompt"];
|
|
121
|
+
const DEFAULT_COMPOSE_SEND_PHRASES = ["that's it", "doom send"];
|
|
122
|
+
const DEFAULT_COMPOSE_CANCEL_PHRASES = ["doom cancel", "scratch that"];
|
|
123
|
+
const DEFAULT_TRANSCRIPTION_TIMEOUT_MS = 12e4;
|
|
124
|
+
const MIN_TRANSCRIPTION_TIMEOUT_MS = 1e3;
|
|
125
|
+
const MAX_TRANSCRIPTION_TIMEOUT_MS = 12e4;
|
|
126
|
+
const MAX_CONTROL_PHRASES = 16;
|
|
127
|
+
const MAX_CONTROL_PHRASE_LENGTH = 64;
|
|
128
|
+
const MAX_TTS_VOICE_LENGTH = 64;
|
|
129
|
+
const MIN_TTS_RATE = 80;
|
|
130
|
+
const MAX_TTS_RATE = 500;
|
|
131
|
+
const MODEL_REFERENCE_PATTERN = /^[^/\s]+\/[^/\s]+(?:\/[^/\s]+)*$/u;
|
|
132
|
+
const PHRASE_PUNCTUATION_PATTERN = /[\p{P}\p{S}]+/gu;
|
|
133
|
+
/**
|
|
134
|
+
* The accepted values, in the order a picker should offer them.
|
|
135
|
+
*
|
|
136
|
+
* Exported because the config UI has to offer exactly what the parser accepts,
|
|
137
|
+
* and a second hand-written list in the contributing package would drift. The
|
|
138
|
+
* validating Sets below are built from these for the same reason.
|
|
139
|
+
*/
|
|
140
|
+
const DOOM_PLANNING_THINKING_LEVELS = [
|
|
141
|
+
"off",
|
|
142
|
+
"minimal",
|
|
143
|
+
"low",
|
|
144
|
+
"medium",
|
|
145
|
+
"high",
|
|
146
|
+
"xhigh",
|
|
147
|
+
"max"
|
|
148
|
+
];
|
|
149
|
+
const DOOM_VOICE_MODES = [DEFAULT_VOICE_MODE, "live"];
|
|
150
|
+
const DOOM_VOICE_ENGINES = [
|
|
151
|
+
DEFAULT_VOICE_ENGINE,
|
|
152
|
+
WHISPER_CPP_ENGINE,
|
|
153
|
+
OPENAI_WHISPER_ENGINE,
|
|
154
|
+
MLX_WHISPER_ENGINE
|
|
155
|
+
];
|
|
156
|
+
const DOOM_VOICE_TTS_ENGINES = [MACOS_SAY_TTS_ENGINE];
|
|
157
|
+
const THINKING_VALUES = new Set(DOOM_PLANNING_THINKING_LEVELS);
|
|
158
|
+
const TRUST_VALUES = /* @__PURE__ */ new Set([
|
|
159
|
+
"ask",
|
|
160
|
+
"always",
|
|
161
|
+
"never"
|
|
162
|
+
]);
|
|
163
|
+
const VOICE_MODE_VALUES = new Set(DOOM_VOICE_MODES);
|
|
164
|
+
const ENGINE_VALUES = new Set(DOOM_VOICE_ENGINES);
|
|
165
|
+
const TTS_ENGINE_VALUES = new Set(DOOM_VOICE_TTS_ENGINES);
|
|
166
|
+
function isObject(value) {
|
|
167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Where a lenient parse collects unknown keys instead of throwing.
|
|
171
|
+
*
|
|
172
|
+
* Module scope rather than a parameter threaded through the twelve nested
|
|
173
|
+
* parsers: `assertKeys` is the only reader, parsing is synchronous end to end,
|
|
174
|
+
* and `parseDoomConfig` owns the whole window in a try/finally. Threading would
|
|
175
|
+
* touch every parser to change behaviour in one of them.
|
|
176
|
+
*/
|
|
177
|
+
let unknownKeySink;
|
|
178
|
+
function assertKeys(value, allowed, location, filePath) {
|
|
179
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
180
|
+
if (unknown.length === 0) return;
|
|
181
|
+
if (unknownKeySink !== void 0) {
|
|
182
|
+
for (const key of unknown) unknownKeySink.push({
|
|
183
|
+
filePath,
|
|
184
|
+
path: location === "root" ? key : `${location}.${key}`,
|
|
185
|
+
message: `unsupported ${location} field`
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Doom config at ${filePath} has unsupported ${location} field(s): ${unknown.join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
function optionalString(value, location, filePath) {
|
|
192
|
+
if (value === void 0) return void 0;
|
|
193
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`Doom config at ${filePath} requires ${location} to be a non-empty string`);
|
|
194
|
+
return value.trim();
|
|
195
|
+
}
|
|
196
|
+
function hasControlCharacters(value) {
|
|
197
|
+
for (const character of value) {
|
|
198
|
+
const codePoint = character.codePointAt(0);
|
|
199
|
+
if (codePoint !== void 0 && (codePoint <= 31 || codePoint === 127)) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
function boundedOptionalString(value, location, filePath, maximumLength) {
|
|
204
|
+
const text = optionalString(value, location, filePath);
|
|
205
|
+
if (!text) return void 0;
|
|
206
|
+
if (text.length > maximumLength) throw new Error(`Doom config at ${filePath} requires ${location} to be at most ${maximumLength} characters`);
|
|
207
|
+
if (hasControlCharacters(text)) throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
function parseModelReference(value, filePath) {
|
|
211
|
+
const location = "voice.autoCapture.model";
|
|
212
|
+
const model = optionalString(value, location, filePath);
|
|
213
|
+
if (!model || !MODEL_REFERENCE_PATTERN.test(model)) throw new Error(`Doom config at ${filePath} requires ${location} in provider/model-id form`);
|
|
214
|
+
return model;
|
|
215
|
+
}
|
|
216
|
+
function normalizeControlPhrase(value) {
|
|
217
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(PHRASE_PUNCTUATION_PATTERN, " ").replace(/\s+/gu, " ").trim();
|
|
218
|
+
}
|
|
219
|
+
function parseControlPhrases(value, location, filePath) {
|
|
220
|
+
if (value === void 0) return void 0;
|
|
221
|
+
if (!Array.isArray(value) || value.some((phrase) => typeof phrase !== "string")) throw new Error(`Doom config at ${filePath} requires ${location} to be an array of strings`);
|
|
222
|
+
if (value.length > MAX_CONTROL_PHRASES) throw new Error(`Doom config at ${filePath} requires ${location} to contain at most ${MAX_CONTROL_PHRASES} phrases`);
|
|
223
|
+
const phrases = value.map((phrase) => phrase.trim());
|
|
224
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
225
|
+
for (const phrase of phrases) {
|
|
226
|
+
if (phrase.length === 0) throw new Error(`Doom config at ${filePath} requires ${location} to contain non-empty strings`);
|
|
227
|
+
if (phrase.length > MAX_CONTROL_PHRASE_LENGTH) throw new Error(`Doom config at ${filePath} requires ${location} phrases to be at most ${MAX_CONTROL_PHRASE_LENGTH} characters`);
|
|
228
|
+
if (hasControlCharacters(phrase)) throw new Error(`Doom config at ${filePath} does not allow control characters in ${location}`);
|
|
229
|
+
const key = normalizeControlPhrase(phrase);
|
|
230
|
+
if (!key) throw new Error(`Doom config at ${filePath} requires ${location} to contain words`);
|
|
231
|
+
if (normalized.has(key)) throw new Error(`Doom config at ${filePath} does not allow normalized duplicates in ${location}`);
|
|
232
|
+
normalized.add(key);
|
|
233
|
+
}
|
|
234
|
+
return phrases;
|
|
235
|
+
}
|
|
236
|
+
function parseBoundedInteger(value, location, filePath, minimum, maximum) {
|
|
237
|
+
if (value === void 0) return void 0;
|
|
238
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) throw new Error(`Doom config at ${filePath} requires ${location} to be an integer from ${minimum} to ${maximum}`);
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Rejects a phrase claimed by two control roles at once.
|
|
243
|
+
*
|
|
244
|
+
* Composition commands are adjudicated before stop phrases and before leading
|
|
245
|
+
* start-phrase removal, so a phrase shared with a later role is not ambiguous, it is
|
|
246
|
+
* silently unreachable. Without a screen there is nothing to show which role won.
|
|
247
|
+
*/
|
|
248
|
+
function assertDisjointPhrases(left, right, leftName, rightName, filePath) {
|
|
249
|
+
const keys = new Set((left ?? []).map(normalizeControlPhrase));
|
|
250
|
+
if ((right ?? []).some((phrase) => keys.has(normalizeControlPhrase(phrase)))) throw new Error(`Doom config at ${filePath} does not allow a phrase in both ${leftName} and ${rightName}`);
|
|
251
|
+
}
|
|
252
|
+
function parseTtsConfig(value, filePath) {
|
|
253
|
+
const location = "voice.autoCapture.tts";
|
|
254
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
255
|
+
assertKeys(value, TTS_KEYS, location, filePath);
|
|
256
|
+
if (typeof value.engine !== "string" || !TTS_ENGINE_VALUES.has(value.engine)) throw new Error(`Doom config at ${filePath} requires ${location}.engine to be one of: ${[...TTS_ENGINE_VALUES].join(", ")}`);
|
|
257
|
+
const voice = boundedOptionalString(value.voice, `${location}.voice`, filePath, MAX_TTS_VOICE_LENGTH);
|
|
258
|
+
const rate = value.rate;
|
|
259
|
+
if (rate !== void 0 && (typeof rate !== "number" || !Number.isInteger(rate) || rate < MIN_TTS_RATE || rate > MAX_TTS_RATE)) throw new Error(`Doom config at ${filePath} requires ${location}.rate to be an integer from ${MIN_TTS_RATE} to ${MAX_TTS_RATE}`);
|
|
260
|
+
return {
|
|
261
|
+
engine: value.engine,
|
|
262
|
+
...voice ? { voice } : {},
|
|
263
|
+
...rate === void 0 ? {} : { rate }
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function parseAutoCapture(value, filePath) {
|
|
267
|
+
if (value === void 0) return void 0;
|
|
268
|
+
const location = "voice.autoCapture";
|
|
269
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
270
|
+
assertKeys(value, AUTO_CAPTURE_KEYS, location, filePath);
|
|
271
|
+
const startPhrases = parseControlPhrases(value.startPhrases, `${location}.startPhrases`, filePath);
|
|
272
|
+
const stopPhrases = parseControlPhrases(value.stopPhrases, `${location}.stopPhrases`, filePath);
|
|
273
|
+
const composeOpenPhrases = parseControlPhrases(value.composeOpenPhrases, `${location}.composeOpenPhrases`, filePath);
|
|
274
|
+
const composeSendPhrases = parseControlPhrases(value.composeSendPhrases, `${location}.composeSendPhrases`, filePath);
|
|
275
|
+
const composeCancelPhrases = parseControlPhrases(value.composeCancelPhrases, `${location}.composeCancelPhrases`, filePath);
|
|
276
|
+
const utteranceIdleMs = parseBoundedInteger(value.utteranceIdleMs, `${location}.utteranceIdleMs`, filePath, MIN_UTTERANCE_IDLE_MS, MAX_UTTERANCE_IDLE_MS);
|
|
277
|
+
const composeUtteranceIdleMs = parseBoundedInteger(value.composeUtteranceIdleMs, `${location}.composeUtteranceIdleMs`, filePath, MIN_COMPOSE_UTTERANCE_IDLE_MS, MAX_COMPOSE_UTTERANCE_IDLE_MS);
|
|
278
|
+
const composeNudgeMs = value.composeNudgeMs === 0 ? 0 : parseBoundedInteger(value.composeNudgeMs, `${location}.composeNudgeMs`, filePath, MIN_COMPOSE_NUDGE_MS, MAX_COMPOSE_NUDGE_MS);
|
|
279
|
+
const transcriptionTimeoutMs = parseBoundedInteger(value.transcriptionTimeoutMs, `${location}.transcriptionTimeoutMs`, filePath, MIN_TRANSCRIPTION_TIMEOUT_MS, MAX_TRANSCRIPTION_TIMEOUT_MS);
|
|
280
|
+
assertDisjointPhrases(startPhrases, stopPhrases, "startPhrases", "stopPhrases", filePath);
|
|
281
|
+
assertDisjointPhrases(composeSendPhrases, composeCancelPhrases, "composeSendPhrases", "composeCancelPhrases", filePath);
|
|
282
|
+
assertDisjointPhrases(composeOpenPhrases, composeSendPhrases, "composeOpenPhrases", "composeSendPhrases", filePath);
|
|
283
|
+
assertDisjointPhrases(composeOpenPhrases, composeCancelPhrases, "composeOpenPhrases", "composeCancelPhrases", filePath);
|
|
284
|
+
assertDisjointPhrases(stopPhrases, composeSendPhrases, "stopPhrases", "composeSendPhrases", filePath);
|
|
285
|
+
assertDisjointPhrases(stopPhrases, composeCancelPhrases, "stopPhrases", "composeCancelPhrases", filePath);
|
|
286
|
+
return {
|
|
287
|
+
model: parseModelReference(value.model, filePath),
|
|
288
|
+
...startPhrases ? { startPhrases } : {},
|
|
289
|
+
...stopPhrases ? { stopPhrases } : {},
|
|
290
|
+
...composeOpenPhrases ? { composeOpenPhrases } : {},
|
|
291
|
+
...composeSendPhrases ? { composeSendPhrases } : {},
|
|
292
|
+
...composeCancelPhrases ? { composeCancelPhrases } : {},
|
|
293
|
+
...utteranceIdleMs === void 0 ? {} : { utteranceIdleMs },
|
|
294
|
+
...composeUtteranceIdleMs === void 0 ? {} : { composeUtteranceIdleMs },
|
|
295
|
+
...composeNudgeMs === void 0 ? {} : { composeNudgeMs },
|
|
296
|
+
...transcriptionTimeoutMs === void 0 ? {} : { transcriptionTimeoutMs },
|
|
297
|
+
tts: parseTtsConfig(value.tts, filePath)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function parseThinking(value, location, filePath) {
|
|
301
|
+
if (value === void 0) return void 0;
|
|
302
|
+
if (typeof value !== "string" || !THINKING_VALUES.has(value)) throw new Error(`Doom config at ${filePath} requires ${location}.thinking to be one of: ${[...THINKING_VALUES].join(", ")}`);
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
function parseAgent(value, location, filePath) {
|
|
306
|
+
if (value === void 0) return void 0;
|
|
307
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
308
|
+
assertKeys(value, AGENT_KEYS, location, filePath);
|
|
309
|
+
const thinking = parseThinking(value.thinking, location, filePath);
|
|
310
|
+
const model = optionalString(value.model, `${location}.model`, filePath);
|
|
311
|
+
return {
|
|
312
|
+
...model ? { model } : {},
|
|
313
|
+
...thinking ? { thinking } : {}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* A flag the cockpit can write.
|
|
318
|
+
*
|
|
319
|
+
* The settings page writes strings and nothing else, so `'true'` has to mean
|
|
320
|
+
* what a hand-edited `true` means. Anything else is rejected rather than
|
|
321
|
+
* treated as false, because a typo that silently disables a feature is the
|
|
322
|
+
* worst reading of it.
|
|
323
|
+
*/
|
|
324
|
+
function parseFlexibleBoolean(value, location, filePath) {
|
|
325
|
+
if (value === void 0) return void 0;
|
|
326
|
+
if (typeof value === "boolean") return value;
|
|
327
|
+
if (value === "true") return true;
|
|
328
|
+
if (value === "false") return false;
|
|
329
|
+
throw new Error(`Doom config at ${filePath} requires ${location} to be true or false`);
|
|
330
|
+
}
|
|
331
|
+
/** Same story as the flag: a number, or the string form the settings page writes. */
|
|
332
|
+
function parseFlexibleRatio(value, location, filePath) {
|
|
333
|
+
if (value === void 0) return void 0;
|
|
334
|
+
const ratio = typeof value === "string" ? Number(value.trim()) : value;
|
|
335
|
+
if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < MIN_AUTOCOMPACT_RATIO || ratio > MAX_AUTOCOMPACT_RATIO) throw new Error(`Doom config at ${filePath} requires ${location} to be a number from ${MIN_AUTOCOMPACT_RATIO} to ${MAX_AUTOCOMPACT_RATIO}`);
|
|
336
|
+
return ratio;
|
|
337
|
+
}
|
|
338
|
+
function parseAutocompactThresholds(value, location, filePath) {
|
|
339
|
+
if (value === void 0) return void 0;
|
|
340
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
341
|
+
assertKeys(value, AUTOCOMPACT_THRESHOLD_KEYS, location, filePath);
|
|
342
|
+
const pass1 = parseFlexibleRatio(value.pass1, `${location}.pass1`, filePath);
|
|
343
|
+
const pass2 = parseFlexibleRatio(value.pass2, `${location}.pass2`, filePath);
|
|
344
|
+
const pass3 = parseFlexibleRatio(value.pass3, `${location}.pass3`, filePath);
|
|
345
|
+
return {
|
|
346
|
+
...pass1 === void 0 ? {} : { pass1 },
|
|
347
|
+
...pass2 === void 0 ? {} : { pass2 },
|
|
348
|
+
...pass3 === void 0 ? {} : { pass3 }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/** Absolute token counts, unlike the ratios, are only ever written by hand, so no string form is accepted. */
|
|
352
|
+
function parseAutocompactTokenCount(value, location, filePath) {
|
|
353
|
+
if (value === void 0) return void 0;
|
|
354
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_AUTOCOMPACT_TOKENS) throw new Error(`Doom config at ${filePath} requires ${location} to be an integer of at least ${MIN_AUTOCOMPACT_TOKENS}`);
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
function parseAutocompactOverrideEntry(value, location, filePath) {
|
|
358
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
359
|
+
assertKeys(value, AUTOCOMPACT_OVERRIDE_KEYS, location, filePath);
|
|
360
|
+
const model = optionalString(value.model, `${location}.model`, filePath);
|
|
361
|
+
if (!model) throw new Error(`Doom config at ${filePath} requires ${location}.model`);
|
|
362
|
+
if (!isObject(value.tokens)) throw new Error(`Doom config at ${filePath} requires ${location}.tokens to be an object`);
|
|
363
|
+
assertKeys(value.tokens, AUTOCOMPACT_THRESHOLD_KEYS, `${location}.tokens`, filePath);
|
|
364
|
+
const pass1 = parseAutocompactTokenCount(value.tokens.pass1, `${location}.tokens.pass1`, filePath);
|
|
365
|
+
const pass2 = parseAutocompactTokenCount(value.tokens.pass2, `${location}.tokens.pass2`, filePath);
|
|
366
|
+
const pass3 = parseAutocompactTokenCount(value.tokens.pass3, `${location}.tokens.pass3`, filePath);
|
|
367
|
+
if (pass1 === void 0 && pass2 === void 0 && pass3 === void 0) throw new Error(`Doom config at ${filePath} requires one of ${location}.tokens.pass1, pass2, or pass3`);
|
|
368
|
+
return {
|
|
369
|
+
model,
|
|
370
|
+
tokens: {
|
|
371
|
+
...pass1 === void 0 ? {} : { pass1 },
|
|
372
|
+
...pass2 === void 0 ? {} : { pass2 },
|
|
373
|
+
...pass3 === void 0 ? {} : { pass3 }
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function parseAutocompactOverrides(value, location, filePath) {
|
|
378
|
+
if (value === void 0) return void 0;
|
|
379
|
+
if (!Array.isArray(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be a list`);
|
|
380
|
+
return value.map((entry, index) => parseAutocompactOverrideEntry(entry, `${location}[${index}]`, filePath));
|
|
381
|
+
}
|
|
382
|
+
function parseAutocompactModeConfig(value, filePath, location = "modes.autocompact") {
|
|
383
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
384
|
+
assertKeys(value, AUTOCOMPACT_KEYS, location, filePath);
|
|
385
|
+
const enabled = parseFlexibleBoolean(value.enabled, `${location}.enabled`, filePath);
|
|
386
|
+
const model = optionalString(value.model, `${location}.model`, filePath);
|
|
387
|
+
const thinking = parseThinking(value.thinking, location, filePath);
|
|
388
|
+
const thresholds = parseAutocompactThresholds(value.thresholds, `${location}.thresholds`, filePath);
|
|
389
|
+
const overrides = parseAutocompactOverrides(value.overrides, `${location}.overrides`, filePath);
|
|
390
|
+
return {
|
|
391
|
+
...enabled === void 0 ? {} : { enabled },
|
|
392
|
+
...model ? { model } : {},
|
|
393
|
+
...thinking ? { thinking } : {},
|
|
394
|
+
...thresholds ? { thresholds } : {},
|
|
395
|
+
...overrides ? { overrides } : {}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function parsePlanningModeConfig(value, filePath, location = "modes.planning") {
|
|
399
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
400
|
+
assertKeys(value, PLANNING_KEYS, location, filePath);
|
|
401
|
+
return {
|
|
402
|
+
main: parseAgent(value.main, `${location}.main`, filePath),
|
|
403
|
+
subagents: parseAgent(value.subagents, `${location}.subagents`, filePath),
|
|
404
|
+
plansDirectory: optionalString(value.plansDirectory, `${location}.plansDirectory`, filePath)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function parseModel(value, location, filePath, allowId) {
|
|
408
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
409
|
+
assertKeys(value, MODEL_KEYS, location, filePath);
|
|
410
|
+
const modelPath = optionalString(value.path, `${location}.path`, filePath);
|
|
411
|
+
const id = optionalString(value.id, `${location}.id`, filePath);
|
|
412
|
+
if (Boolean(modelPath) === Boolean(id)) throw new Error(`Doom config at ${filePath} requires exactly one of ${location}.path or ${location}.id`);
|
|
413
|
+
if (id && !allowId) throw new Error(`Doom config at ${filePath} does not support ${location}.id for whisper-cpp`);
|
|
414
|
+
return {
|
|
415
|
+
...modelPath ? { path: modelPath } : {},
|
|
416
|
+
...id ? { id } : {}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function parseAdapter(value, location, filePath, allowId) {
|
|
420
|
+
if (value === void 0) return void 0;
|
|
421
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${location} to be an object`);
|
|
422
|
+
assertKeys(value, ADAPTER_KEYS, location, filePath);
|
|
423
|
+
const binary = optionalString(value.binary, `${location}.binary`, filePath);
|
|
424
|
+
return {
|
|
425
|
+
...binary ? { binary } : {},
|
|
426
|
+
model: parseModel(value.model, `${location}.model`, filePath, allowId)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function parseVoice(value, filePath) {
|
|
430
|
+
if (value === void 0) return void 0;
|
|
431
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires voice to be an object`);
|
|
432
|
+
assertKeys(value, VOICE_KEYS, "voice", filePath);
|
|
433
|
+
const result = {};
|
|
434
|
+
if (value.mode !== void 0) {
|
|
435
|
+
if (typeof value.mode !== "string" || !VOICE_MODE_VALUES.has(value.mode)) throw new Error(`Doom config at ${filePath} requires voice.mode to be one of: ${[...VOICE_MODE_VALUES].join(", ")}`);
|
|
436
|
+
result.mode = value.mode;
|
|
437
|
+
}
|
|
438
|
+
if (value.engine !== void 0) {
|
|
439
|
+
if (typeof value.engine !== "string" || !ENGINE_VALUES.has(value.engine)) throw new Error(`Doom config at ${filePath} requires voice.engine to be one of: ${[...ENGINE_VALUES].join(", ")}`);
|
|
440
|
+
result.engine = value.engine;
|
|
441
|
+
}
|
|
442
|
+
const language = optionalString(value.language, "voice.language", filePath);
|
|
443
|
+
if (language) result.language = language;
|
|
444
|
+
if (value.recorder !== void 0) {
|
|
445
|
+
if (!isObject(value.recorder)) throw new Error(`Doom config at ${filePath} requires voice.recorder to be an object`);
|
|
446
|
+
assertKeys(value.recorder, RECORDER_KEYS, "voice.recorder", filePath);
|
|
447
|
+
result.recorder = {};
|
|
448
|
+
const binary = optionalString(value.recorder.binary, "voice.recorder.binary", filePath);
|
|
449
|
+
const device = optionalString(value.recorder.device, "voice.recorder.device", filePath);
|
|
450
|
+
if (binary) result.recorder.binary = binary;
|
|
451
|
+
if (device) result.recorder.device = device;
|
|
452
|
+
}
|
|
453
|
+
if (value.adapters !== void 0) {
|
|
454
|
+
if (!isObject(value.adapters)) throw new Error(`Doom config at ${filePath} requires voice.adapters to be an object`);
|
|
455
|
+
assertKeys(value.adapters, ADAPTERS_KEYS, "voice.adapters", filePath);
|
|
456
|
+
result.adapters = {
|
|
457
|
+
[WHISPER_CPP_ENGINE]: parseAdapter(value.adapters[WHISPER_CPP_ENGINE], `voice.adapters.${WHISPER_CPP_ENGINE}`, filePath, false),
|
|
458
|
+
[OPENAI_WHISPER_ENGINE]: parseAdapter(value.adapters[OPENAI_WHISPER_ENGINE], `voice.adapters.${OPENAI_WHISPER_ENGINE}`, filePath, true),
|
|
459
|
+
[MLX_WHISPER_ENGINE]: parseAdapter(value.adapters[MLX_WHISPER_ENGINE], `voice.adapters.${MLX_WHISPER_ENGINE}`, filePath, true)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const autoCapture = parseAutoCapture(value.autoCapture, filePath);
|
|
463
|
+
if (autoCapture) result.autoCapture = autoCapture;
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
function parseSelection(value, filePath) {
|
|
467
|
+
if (value === void 0) return void 0;
|
|
468
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires selection to be an object`);
|
|
469
|
+
assertKeys(value, SELECTION_KEYS, "selection", filePath);
|
|
470
|
+
const domains = value.domains;
|
|
471
|
+
if (domains !== void 0 && (!Array.isArray(domains) || domains.some((name) => typeof name !== "string"))) throw new Error(`Doom config at ${filePath} requires selection.domains to be an array of strings`);
|
|
472
|
+
return {
|
|
473
|
+
majorMode: optionalString(value.majorMode, "selection.majorMode", filePath),
|
|
474
|
+
domains,
|
|
475
|
+
profile: optionalString(value.profile, "selection.profile", filePath)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function parseEditor(value, filePath) {
|
|
479
|
+
if (value === void 0) return void 0;
|
|
480
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires editor to be an object`);
|
|
481
|
+
assertKeys(value, EDITOR_KEYS, "editor", filePath);
|
|
482
|
+
return { command: optionalString(value.command, "editor.command", filePath) };
|
|
483
|
+
}
|
|
484
|
+
/** `web` and `native` each choose a template, so both read the same `{ template }` shape. */
|
|
485
|
+
function parseTemplateChoice(value, root, filePath) {
|
|
486
|
+
if (value === void 0) return void 0;
|
|
487
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires ${root} to be an object`);
|
|
488
|
+
assertKeys(value, ["template"], root, filePath);
|
|
489
|
+
if (value.template === void 0) return {};
|
|
490
|
+
const template = optionalString(value.template, `${root}.template`, filePath);
|
|
491
|
+
if (!template || template.length > 160 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(template)) throw new Error(`Doom config at ${filePath} requires ${root}.template to be a kebab-case template identifier`);
|
|
492
|
+
return { template };
|
|
493
|
+
}
|
|
494
|
+
function parseComputerUse(value, filePath) {
|
|
495
|
+
if (value === void 0) return void 0;
|
|
496
|
+
if (!isObject(value)) throw new Error(`Doom config at ${filePath} requires computerUse to be an object`);
|
|
497
|
+
assertKeys(value, COMPUTER_USE_KEYS, "computerUse", filePath);
|
|
498
|
+
const enabled = parseFlexibleBoolean(value.enabled, "computerUse.enabled", filePath);
|
|
499
|
+
return enabled === void 0 ? {} : { enabled };
|
|
500
|
+
}
|
|
501
|
+
function parseDoomConfig(content, filePath, options) {
|
|
502
|
+
if (options?.lenient !== true) return parseStrict(content, filePath);
|
|
503
|
+
const diagnostics = [];
|
|
504
|
+
const previous = unknownKeySink;
|
|
505
|
+
unknownKeySink = diagnostics;
|
|
506
|
+
try {
|
|
507
|
+
return {
|
|
508
|
+
config: parseStrict(content, filePath),
|
|
509
|
+
diagnostics
|
|
510
|
+
};
|
|
511
|
+
} finally {
|
|
512
|
+
unknownKeySink = previous;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function parseStrict(content, filePath) {
|
|
516
|
+
let parsed;
|
|
517
|
+
try {
|
|
518
|
+
parsed = parse(content) ?? {};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
throw new Error(`Could not parse Doom config at ${filePath}`, { cause: error });
|
|
521
|
+
}
|
|
522
|
+
if (!isObject(parsed)) throw new Error(`Doom config at ${filePath} must be a YAML object`);
|
|
523
|
+
assertKeys(parsed, ROOT_KEYS, "root", filePath);
|
|
524
|
+
let modes;
|
|
525
|
+
if (parsed.modes !== void 0) {
|
|
526
|
+
if (!isObject(parsed.modes)) throw new Error(`Doom config at ${filePath} requires modes to be an object`);
|
|
527
|
+
assertKeys(parsed.modes, MODE_KEYS, "modes", filePath);
|
|
528
|
+
modes = {
|
|
529
|
+
...parsed.modes.planning === void 0 ? {} : { planning: parsePlanningModeConfig(parsed.modes.planning, filePath) },
|
|
530
|
+
...parsed.modes.autocompact === void 0 ? {} : { autocompact: parseAutocompactModeConfig(parsed.modes.autocompact, filePath) }
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const trust = parsed.projectTrust === void 0 ? DEFAULT_PROJECT_TRUST : parsed.projectTrust;
|
|
534
|
+
if (typeof trust !== "string" || !TRUST_VALUES.has(trust)) throw new Error(`Doom config at ${filePath} projectTrust must be ask, always, or never`);
|
|
535
|
+
return {
|
|
536
|
+
modes,
|
|
537
|
+
computerUse: parseComputerUse(parsed.computerUse, filePath),
|
|
538
|
+
projectTrust: trust,
|
|
539
|
+
editor: parseEditor(parsed.editor, filePath),
|
|
540
|
+
voice: parseVoice(parsed.voice, filePath),
|
|
541
|
+
selection: parseSelection(parsed.selection, filePath),
|
|
542
|
+
web: parseTemplateChoice(parsed.web, "web", filePath),
|
|
543
|
+
native: parseTemplateChoice(parsed.native, "native", filePath)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function mergeAgent(globalValue, repositoryValue) {
|
|
547
|
+
return globalValue || repositoryValue ? {
|
|
548
|
+
...globalValue,
|
|
549
|
+
...repositoryValue
|
|
550
|
+
} : void 0;
|
|
551
|
+
}
|
|
552
|
+
function mergeDoomConfigs(globalConfig, repositoryConfig) {
|
|
553
|
+
if (repositoryConfig.voice?.mode) throw new Error("Repository Doom config voice.mode is global-only; configure it in ~/.po/config.yaml");
|
|
554
|
+
if (repositoryConfig.voice?.autoCapture) throw new Error("Repository Doom config voice.autoCapture is global-only; configure it in ~/.po/config.yaml");
|
|
555
|
+
if (repositoryConfig.computerUse) throw new Error("Repository Doom config computerUse is global-only; configure it in ~/.po/config.yaml");
|
|
556
|
+
const globalPlanning = globalConfig.modes?.planning;
|
|
557
|
+
const repositoryPlanning = repositoryConfig.modes?.planning;
|
|
558
|
+
const planning = globalPlanning || repositoryPlanning ? {
|
|
559
|
+
main: mergeAgent(globalPlanning?.main, repositoryPlanning?.main),
|
|
560
|
+
subagents: mergeAgent(globalPlanning?.subagents, repositoryPlanning?.subagents),
|
|
561
|
+
plansDirectory: repositoryPlanning?.plansDirectory ?? globalPlanning?.plansDirectory
|
|
562
|
+
} : void 0;
|
|
563
|
+
const globalAutocompact = globalConfig.modes?.autocompact;
|
|
564
|
+
const repositoryAutocompact = repositoryConfig.modes?.autocompact;
|
|
565
|
+
const autocompact = globalAutocompact || repositoryAutocompact ? {
|
|
566
|
+
...globalAutocompact,
|
|
567
|
+
...repositoryAutocompact,
|
|
568
|
+
...globalAutocompact?.thresholds || repositoryAutocompact?.thresholds ? { thresholds: {
|
|
569
|
+
...globalAutocompact?.thresholds,
|
|
570
|
+
...repositoryAutocompact?.thresholds
|
|
571
|
+
} } : {}
|
|
572
|
+
} : void 0;
|
|
573
|
+
const voice = mergeVoice(globalConfig.voice, repositoryConfig.voice);
|
|
574
|
+
return {
|
|
575
|
+
modes: planning || autocompact ? {
|
|
576
|
+
...planning ? { planning } : {},
|
|
577
|
+
...autocompact ? { autocompact } : {}
|
|
578
|
+
} : void 0,
|
|
579
|
+
computerUse: globalConfig.computerUse,
|
|
580
|
+
projectTrust: repositoryConfig.projectTrust,
|
|
581
|
+
editor: globalConfig.editor,
|
|
582
|
+
voice,
|
|
583
|
+
web: globalConfig.web || repositoryConfig.web ? { template: repositoryConfig.web?.template ?? globalConfig.web?.template } : void 0,
|
|
584
|
+
native: globalConfig.native || repositoryConfig.native ? { template: repositoryConfig.native?.template ?? globalConfig.native?.template } : void 0,
|
|
585
|
+
selection: globalConfig.selection || repositoryConfig.selection ? {
|
|
586
|
+
...globalConfig.selection,
|
|
587
|
+
...repositoryConfig.selection
|
|
588
|
+
} : void 0
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function mergeAdapter(globalValue, repositoryValue) {
|
|
592
|
+
if (!globalValue && !repositoryValue) return void 0;
|
|
593
|
+
return {
|
|
594
|
+
...globalValue,
|
|
595
|
+
...repositoryValue,
|
|
596
|
+
model: repositoryValue?.model ?? globalValue.model
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function mergeVoice(globalValue, repositoryValue) {
|
|
600
|
+
if (!globalValue && !repositoryValue) return void 0;
|
|
601
|
+
return {
|
|
602
|
+
...globalValue,
|
|
603
|
+
...repositoryValue,
|
|
604
|
+
...globalValue?.mode ? { mode: globalValue.mode } : {},
|
|
605
|
+
recorder: globalValue?.recorder || repositoryValue?.recorder ? {
|
|
606
|
+
...globalValue?.recorder,
|
|
607
|
+
...repositoryValue?.recorder
|
|
608
|
+
} : void 0,
|
|
609
|
+
adapters: globalValue?.adapters || repositoryValue?.adapters ? {
|
|
610
|
+
[WHISPER_CPP_ENGINE]: mergeAdapter(globalValue?.adapters?.[WHISPER_CPP_ENGINE], repositoryValue?.adapters?.[WHISPER_CPP_ENGINE]),
|
|
611
|
+
[OPENAI_WHISPER_ENGINE]: mergeAdapter(globalValue?.adapters?.[OPENAI_WHISPER_ENGINE], repositoryValue?.adapters?.[OPENAI_WHISPER_ENGINE]),
|
|
612
|
+
[MLX_WHISPER_ENGINE]: mergeAdapter(globalValue?.adapters?.[MLX_WHISPER_ENGINE], repositoryValue?.adapters?.[MLX_WHISPER_ENGINE])
|
|
613
|
+
} : void 0,
|
|
614
|
+
...globalValue?.autoCapture ? { autoCapture: globalValue.autoCapture } : {}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Resolves voice settings, optionally shadowed by the active profile's override.
|
|
619
|
+
*
|
|
620
|
+
* The override is per field, so a persona that sets only `rate` keeps the voice
|
|
621
|
+
* from config.yaml. `engine` is deliberately not overridable: it selects the
|
|
622
|
+
* synthesiser binary, which is a machine-level fact rather than a persona's.
|
|
623
|
+
*
|
|
624
|
+
* The result is read once when autonomous voice activates, because
|
|
625
|
+
* `VoiceNarrationPlayback.config` is assigned in `activate()` and has no setter.
|
|
626
|
+
* That is sufficient today only because every profile switch reloads the
|
|
627
|
+
* session, which deactivates and reactivates voice. A future in-place profile
|
|
628
|
+
* switch would leave the spoken voice stale until the next activation.
|
|
629
|
+
*/
|
|
630
|
+
function resolveVoiceConfig(config, ttsOverride) {
|
|
631
|
+
return {
|
|
632
|
+
mode: config.mode ?? DEFAULT_VOICE_MODE,
|
|
633
|
+
engine: config.engine ?? DEFAULT_VOICE_ENGINE,
|
|
634
|
+
language: config.language ?? DEFAULT_VOICE_LANGUAGE,
|
|
635
|
+
recorder: {
|
|
636
|
+
...config.recorder,
|
|
637
|
+
device: config.recorder?.device ?? DEFAULT_RECORDER_DEVICE
|
|
638
|
+
},
|
|
639
|
+
adapters: config.adapters ?? {},
|
|
640
|
+
...config.autoCapture ? { autoCapture: {
|
|
641
|
+
...config.autoCapture,
|
|
642
|
+
startPhrases: [...config.autoCapture.startPhrases ?? []],
|
|
643
|
+
stopPhrases: [...config.autoCapture.stopPhrases ?? []],
|
|
644
|
+
composeOpenPhrases: [...config.autoCapture.composeOpenPhrases ?? DEFAULT_COMPOSE_OPEN_PHRASES],
|
|
645
|
+
composeSendPhrases: [...config.autoCapture.composeSendPhrases ?? DEFAULT_COMPOSE_SEND_PHRASES],
|
|
646
|
+
composeCancelPhrases: [...config.autoCapture.composeCancelPhrases ?? DEFAULT_COMPOSE_CANCEL_PHRASES],
|
|
647
|
+
utteranceIdleMs: config.autoCapture.utteranceIdleMs ?? DEFAULT_UTTERANCE_IDLE_MS,
|
|
648
|
+
composeUtteranceIdleMs: config.autoCapture.composeUtteranceIdleMs ?? DEFAULT_COMPOSE_UTTERANCE_IDLE_MS,
|
|
649
|
+
composeNudgeMs: config.autoCapture.composeNudgeMs ?? DEFAULT_COMPOSE_NUDGE_MS,
|
|
650
|
+
transcriptionTimeoutMs: config.autoCapture.transcriptionTimeoutMs ?? DEFAULT_TRANSCRIPTION_TIMEOUT_MS,
|
|
651
|
+
tts: {
|
|
652
|
+
...config.autoCapture.tts,
|
|
653
|
+
...ttsOverride?.voice === void 0 ? {} : { voice: ttsOverride.voice },
|
|
654
|
+
...ttsOverride?.rate === void 0 ? {} : { rate: ttsOverride.rate }
|
|
655
|
+
}
|
|
656
|
+
} } : {}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/** Root keys the merge reads from one side only. */
|
|
660
|
+
const GLOBAL_ONLY_ROOTS = ["computerUse", "editor"];
|
|
661
|
+
const REPOSITORY_ONLY_ROOTS = ["projectTrust"];
|
|
662
|
+
/** Nested exceptions; a repository declaration of either throws in the merge. */
|
|
663
|
+
const GLOBAL_ONLY_PATHS = [["voice", "mode"], ["voice", "autoCapture"]];
|
|
664
|
+
function startsWith(keyPath, prefix) {
|
|
665
|
+
return prefix.length <= keyPath.length && prefix.every((segment, index) => keyPath[index] === segment);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* The scopes a key path accepts. An unknown root answers `both` rather than
|
|
669
|
+
* throwing: the parser is what rejects unsupported keys, and it reports them
|
|
670
|
+
* far better than a scope lookup could.
|
|
671
|
+
*/
|
|
672
|
+
function configScopeOf(keyPath) {
|
|
673
|
+
if (keyPath.length === 0) return "both";
|
|
674
|
+
if (GLOBAL_ONLY_PATHS.some((prefix) => startsWith(keyPath, prefix))) return "global";
|
|
675
|
+
const root = keyPath[0];
|
|
676
|
+
if (GLOBAL_ONLY_ROOTS.includes(root)) return "global";
|
|
677
|
+
if (REPOSITORY_ONLY_ROOTS.includes(root)) return "repository";
|
|
678
|
+
return "both";
|
|
679
|
+
}
|
|
680
|
+
/** Every root key the config accepts, so a caller can enumerate without importing the parser's internals. */
|
|
681
|
+
function configRootKeys() {
|
|
682
|
+
return ROOT_KEYS;
|
|
683
|
+
}
|
|
684
|
+
const KEY_SEPARATOR = ".";
|
|
685
|
+
function isPlainRecord(value) {
|
|
686
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Every key path a parsed document actually sets, as dotted strings.
|
|
690
|
+
*
|
|
691
|
+
* A settings surface needs to tell "this file set this key" from "this file is
|
|
692
|
+
* silent about it", and the parsed `DoomConfig` cannot answer: an absent file
|
|
693
|
+
* reads as `{ projectTrust: 'ask' }` and `parseDoomConfig` always returns all
|
|
694
|
+
* known root keys, some undefined. So the question is asked of the raw document
|
|
695
|
+
* instead, before any defaulting has happened.
|
|
696
|
+
*
|
|
697
|
+
* Leaves only. A list is a leaf because it is replaced whole, and an empty
|
|
698
|
+
* record sets nothing.
|
|
699
|
+
*/
|
|
700
|
+
function configLeafKeys(value, prefix = []) {
|
|
701
|
+
if (!isPlainRecord(value)) return prefix.length > 0 ? [prefix.join(KEY_SEPARATOR)] : [];
|
|
702
|
+
const keys = Object.keys(value);
|
|
703
|
+
if (keys.length === 0) return prefix.length > 0 ? [prefix.join(KEY_SEPARATOR)] : [];
|
|
704
|
+
return keys.flatMap((key) => configLeafKeys(value[key], [...prefix, key]));
|
|
705
|
+
}
|
|
706
|
+
/** The value a key path points at, or undefined when any step is missing. */
|
|
707
|
+
function valueAtKeyPath(value, keyPath) {
|
|
708
|
+
let current = value;
|
|
709
|
+
for (const segment of keyPath) {
|
|
710
|
+
if (!isPlainRecord(current)) return void 0;
|
|
711
|
+
current = current[segment];
|
|
712
|
+
}
|
|
713
|
+
return current;
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
716
|
+
export { configLeafKeys as a, mergeDoomConfigs as c, parsePlanningModeConfig as d, resolveVoiceConfig as f, DOOM_VOICE_TTS_ENGINES as i, parseAutocompactModeConfig as l, DOOM_VOICE_ENGINES as n, configRootKeys as o, valueAtKeyPath as p, DOOM_VOICE_MODES as r, configScopeOf as s, DOOM_PLANNING_THINKING_LEVELS as t, parseDoomConfig as u };
|
|
717
|
+
|
|
718
|
+
//# sourceMappingURL=configPolicy--TNX7feH.mjs.map
|