@newsails/veil-cli 1.0.1

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 (199) hide show
  1. package/.veil/agents/analyst/AGENT.md +21 -0
  2. package/.veil/agents/analyst/agent.json +23 -0
  3. package/.veil/agents/assistant/AGENT.md +15 -0
  4. package/.veil/agents/assistant/agent.json +19 -0
  5. package/.veil/agents/coder/AGENT.md +18 -0
  6. package/.veil/agents/coder/agent.json +19 -0
  7. package/.veil/agents/hello/AGENT.md +5 -0
  8. package/.veil/agents/hello/agent.json +13 -0
  9. package/.veil/agents/writer/AGENT.md +12 -0
  10. package/.veil/agents/writer/agent.json +17 -0
  11. package/.veil/memory/MEMORY.md +343 -0
  12. package/.veil/memory/agents/analyst/MEMORY.md +55 -0
  13. package/.veil/memory/agents/hello/MEMORY.md +12 -0
  14. package/.veil/runtime.pid +1 -0
  15. package/.veil/settings.json +10 -0
  16. package/.veil-studio/studio.db +0 -0
  17. package/.veil-studio/studio.db-shm +0 -0
  18. package/.veil-studio/studio.db-wal +0 -0
  19. package/PLAN/01-vision.md +26 -0
  20. package/PLAN/02-tech-stack.md +94 -0
  21. package/PLAN/03-agents.md +232 -0
  22. package/PLAN/04-runtime.md +171 -0
  23. package/PLAN/05-tools.md +211 -0
  24. package/PLAN/06-communication.md +243 -0
  25. package/PLAN/07-storage.md +218 -0
  26. package/PLAN/08-api-cli.md +153 -0
  27. package/PLAN/09-permissions.md +108 -0
  28. package/PLAN/10-ably.md +105 -0
  29. package/PLAN/11-file-formats.md +442 -0
  30. package/PLAN/12-folder-structure.md +205 -0
  31. package/PLAN/13-operations.md +212 -0
  32. package/PLAN/README.md +23 -0
  33. package/README.md +128 -0
  34. package/REPORT.md +174 -0
  35. package/TODO.md +45 -0
  36. package/ai-tests/FRONTEND_PROMPT.md +220 -0
  37. package/ai-tests/Research & Planning.md +814 -0
  38. package/ai-tests/prompt-001-basic-api.md +230 -0
  39. package/ai-tests/prompt-002-basic-flows.md +230 -0
  40. package/ai-tests/prompt-003-agent-behaviors.md +220 -0
  41. package/api/middleware.js +60 -0
  42. package/api/routes/agents.js +193 -0
  43. package/api/routes/chat.js +93 -0
  44. package/api/routes/completions.js +122 -0
  45. package/api/routes/daemons.js +80 -0
  46. package/api/routes/memory.js +169 -0
  47. package/api/routes/models.js +40 -0
  48. package/api/routes/remote-methods.js +74 -0
  49. package/api/routes/sessions.js +208 -0
  50. package/api/routes/settings.js +108 -0
  51. package/api/routes/system.js +50 -0
  52. package/api/routes/tasks.js +270 -0
  53. package/api/server.js +120 -0
  54. package/cli/formatter.js +70 -0
  55. package/cli/index.js +443 -0
  56. package/cli/parser.js +113 -0
  57. package/config/config.json +10 -0
  58. package/config/models.json +6826 -0
  59. package/core/agent.js +329 -0
  60. package/core/cancel.js +38 -0
  61. package/core/compaction.js +176 -0
  62. package/core/events.js +13 -0
  63. package/core/loop.js +564 -0
  64. package/core/memory.js +51 -0
  65. package/core/prompt.js +185 -0
  66. package/core/queue.js +96 -0
  67. package/core/registry.js +291 -0
  68. package/core/remote-methods.js +124 -0
  69. package/core/router.js +386 -0
  70. package/core/running-sessions.js +18 -0
  71. package/docs/api/01-system.md +84 -0
  72. package/docs/api/02-agents.md +374 -0
  73. package/docs/api/03-chat.md +269 -0
  74. package/docs/api/04-tasks.md +470 -0
  75. package/docs/api/05-sessions.md +444 -0
  76. package/docs/api/06-daemons.md +142 -0
  77. package/docs/api/07-memory.md +186 -0
  78. package/docs/api/08-settings.md +133 -0
  79. package/docs/api/09-models.md +119 -0
  80. package/docs/api/09-websocket.md +350 -0
  81. package/docs/api/10-completions.md +134 -0
  82. package/docs/api/README.md +116 -0
  83. package/docs/guide/01-quickstart.md +220 -0
  84. package/docs/guide/02-folder-structure.md +185 -0
  85. package/docs/guide/03-configuration.md +252 -0
  86. package/docs/guide/04-agents.md +267 -0
  87. package/docs/guide/05-cli.md +290 -0
  88. package/docs/guide/06-tools.md +643 -0
  89. package/docs/guide/07-permissions.md +236 -0
  90. package/docs/guide/08-memory.md +139 -0
  91. package/docs/guide/09-multi-agent.md +271 -0
  92. package/docs/guide/10-daemons.md +226 -0
  93. package/docs/guide/README.md +53 -0
  94. package/docs/index.html +623 -0
  95. package/examples/README.md +151 -0
  96. package/examples/agents/assistant/AGENT.md +31 -0
  97. package/examples/agents/assistant/SOUL.md +9 -0
  98. package/examples/agents/assistant/agent.json +74 -0
  99. package/examples/agents/hello/AGENT.md +15 -0
  100. package/examples/agents/hello/agent.json +14 -0
  101. package/examples/agents/monitor/AGENT.md +51 -0
  102. package/examples/agents/monitor/agent.json +33 -0
  103. package/examples/agents/monitor/heartbeats/monitor.md +24 -0
  104. package/examples/agents/orchestrator/AGENT.md +70 -0
  105. package/examples/agents/orchestrator/agent.json +30 -0
  106. package/examples/agents/researcher/AGENT.md +52 -0
  107. package/examples/agents/researcher/agent.json +49 -0
  108. package/examples/agents/researcher/skills/web-research.md +28 -0
  109. package/examples/skills/code-review.md +72 -0
  110. package/examples/skills/summarise.md +59 -0
  111. package/examples/skills/web-research.md +42 -0
  112. package/examples/tools/word-count/index.js +27 -0
  113. package/examples/tools/word-count/tool.json +18 -0
  114. package/infrastructure/database.js +563 -0
  115. package/infrastructure/scheduler.js +122 -0
  116. package/llm/client.js +206 -0
  117. package/migrations/001-initial.sql +121 -0
  118. package/migrations/002-debuggability.sql +13 -0
  119. package/migrations/003-drop-orphaned-columns.sql +72 -0
  120. package/migrations/004-session-message-token-fields.sql +78 -0
  121. package/migrations/005-session-thinking.sql +5 -0
  122. package/package.json +30 -0
  123. package/schemas/agent.json +143 -0
  124. package/schemas/settings.json +111 -0
  125. package/scripts/fetch-models.js +93 -0
  126. package/session-debug-scenario.md +248 -0
  127. package/settings/fields.js +52 -0
  128. package/system-prompts/base-core.md +7 -0
  129. package/system-prompts/environment.md +13 -0
  130. package/system-prompts/reminders/anti-drift.md +6 -0
  131. package/system-prompts/reminders/stall-recovery.md +10 -0
  132. package/system-prompts/safety-rules.md +25 -0
  133. package/system-prompts/task-heuristics.md +27 -0
  134. package/test/client.js +71 -0
  135. package/test/integration/01-health.test.js +25 -0
  136. package/test/integration/02-agents.test.js +80 -0
  137. package/test/integration/03-chat-hello.test.js +48 -0
  138. package/test/integration/04-chat-multiturn.test.js +61 -0
  139. package/test/integration/05-chat-writer.test.js +48 -0
  140. package/test/integration/06-task-basic.test.js +68 -0
  141. package/test/integration/07-task-tools.test.js +74 -0
  142. package/test/integration/08-task-code-analysis.test.js +69 -0
  143. package/test/integration/09-memory-analyst.test.js +63 -0
  144. package/test/integration/10-task-advanced.test.js +85 -0
  145. package/test/integration/11-sessions-advanced.test.js +84 -0
  146. package/test/integration/12-assistant-chat-tools.test.js +75 -0
  147. package/test/integration/13-edge-cases.test.js +99 -0
  148. package/test/integration/14-cancel.test.js +62 -0
  149. package/test/integration/15-debug.test.js +106 -0
  150. package/test/integration/16-memory-api.test.js +83 -0
  151. package/test/integration/17-settings-api.test.js +41 -0
  152. package/test/integration/18-tool-search-activation.test.js +119 -0
  153. package/test/results/.gitkeep +0 -0
  154. package/test/runner.js +206 -0
  155. package/test/smoke.js +216 -0
  156. package/tools/agent_message.js +85 -0
  157. package/tools/agent_send.js +80 -0
  158. package/tools/agent_spawn.js +44 -0
  159. package/tools/bash.js +49 -0
  160. package/tools/edit_file.js +41 -0
  161. package/tools/glob.js +64 -0
  162. package/tools/grep.js +82 -0
  163. package/tools/list_dir.js +63 -0
  164. package/tools/log_write.js +31 -0
  165. package/tools/memory_read.js +38 -0
  166. package/tools/memory_search.js +65 -0
  167. package/tools/memory_write.js +42 -0
  168. package/tools/read_file.js +48 -0
  169. package/tools/sleep.js +22 -0
  170. package/tools/task_create.js +41 -0
  171. package/tools/task_respond.js +37 -0
  172. package/tools/task_spawn.js +64 -0
  173. package/tools/task_status.js +39 -0
  174. package/tools/task_subscribe.js +37 -0
  175. package/tools/todo_read.js +26 -0
  176. package/tools/todo_write.js +38 -0
  177. package/tools/tool_activate.js +24 -0
  178. package/tools/tool_search.js +24 -0
  179. package/tools/web_fetch.js +50 -0
  180. package/tools/web_search.js +52 -0
  181. package/tools/write_file.js +28 -0
  182. package/ui/api.js +190 -0
  183. package/ui/app.js +281 -0
  184. package/ui/index.html +382 -0
  185. package/ui/views/agents.js +377 -0
  186. package/ui/views/chat.js +610 -0
  187. package/ui/views/connection.js +96 -0
  188. package/ui/views/daemons.js +129 -0
  189. package/ui/views/feed.js +194 -0
  190. package/ui/views/memory.js +263 -0
  191. package/ui/views/models.js +146 -0
  192. package/ui/views/sessions.js +314 -0
  193. package/ui/views/settings.js +142 -0
  194. package/ui/views/tasks.js +415 -0
  195. package/utils/context.js +49 -0
  196. package/utils/id.js +16 -0
  197. package/utils/models.js +88 -0
  198. package/utils/paths.js +213 -0
  199. package/utils/settings.js +172 -0
package/utils/paths.js ADDED
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const CONFIG_DIR_NAME = '.veil';
7
+
8
+ /**
9
+ * Returns the .veil/ config directory for a project.
10
+ * @param {string} cwd - Project root directory
11
+ * @returns {string}
12
+ */
13
+ function getProjectConfigDir(cwd) {
14
+ return path.join(cwd, CONFIG_DIR_NAME);
15
+ }
16
+
17
+ /**
18
+ * Returns the agents directory for a project.
19
+ * @param {string} cwd - Project root directory
20
+ * @returns {string}
21
+ */
22
+ function getProjectAgentsDir(cwd) {
23
+ return path.join(getProjectConfigDir(cwd), 'agents');
24
+ }
25
+
26
+ /**
27
+ * Returns the path to a specific agent's folder in a project.
28
+ * @param {string} cwd - Project root directory
29
+ * @param {string} agentName - Agent name
30
+ * @returns {string}
31
+ */
32
+ function getAgentDir(cwd, agentName) {
33
+ return path.join(getProjectAgentsDir(cwd), agentName);
34
+ }
35
+
36
+ /**
37
+ * Returns the project-level settings.json path.
38
+ * @param {string} cwd - Project root directory
39
+ * @returns {string}
40
+ */
41
+ function getProjectSettingsPath(cwd) {
42
+ return path.join(getProjectConfigDir(cwd), 'settings.json');
43
+ }
44
+
45
+ /**
46
+ * Returns the project-level settings.local.json path.
47
+ * @param {string} cwd - Project root directory
48
+ * @returns {string}
49
+ */
50
+ function getProjectSettingsLocalPath(cwd) {
51
+ return path.join(getProjectConfigDir(cwd), 'settings.local.json');
52
+ }
53
+
54
+ /**
55
+ * Returns the project-level auth.json path.
56
+ * @param {string} cwd - Project root directory
57
+ * @returns {string}
58
+ */
59
+ function getProjectAuthPath(cwd) {
60
+ return path.join(getProjectConfigDir(cwd), 'auth.json');
61
+ }
62
+
63
+ /**
64
+ * Returns the project-level AGENT.md path (global instructions for all agents).
65
+ * @param {string} cwd - Project root directory
66
+ * @returns {string}
67
+ */
68
+ function getProjectAgentMdPath(cwd) {
69
+ return path.join(getProjectConfigDir(cwd), 'AGENT.md');
70
+ }
71
+
72
+ /**
73
+ * Returns the project-level memory directory.
74
+ * @param {string} cwd - Project root directory
75
+ * @returns {string}
76
+ */
77
+ function getProjectMemoryDir(cwd) {
78
+ return path.join(getProjectConfigDir(cwd), 'memory');
79
+ }
80
+
81
+ /**
82
+ * Returns the memory dir for a specific agent.
83
+ * @param {string} cwd - Project root directory
84
+ * @param {string} agentName - Agent name
85
+ * @returns {string}
86
+ */
87
+ function getAgentMemoryDir(cwd, agentName) {
88
+ return path.join(getProjectMemoryDir(cwd), 'agents', agentName);
89
+ }
90
+
91
+ /**
92
+ * Returns the heartbeats directory.
93
+ * @param {string} cwd - Project root directory
94
+ * @returns {string}
95
+ */
96
+ function getHeartbeatsDir(cwd) {
97
+ return path.join(getProjectConfigDir(cwd), 'heartbeats');
98
+ }
99
+
100
+ /**
101
+ * Returns the project-level tools directory.
102
+ * @param {string} cwd - Project root directory
103
+ * @returns {string}
104
+ */
105
+ function getProjectToolsDir(cwd) {
106
+ return path.join(getProjectConfigDir(cwd), 'tools');
107
+ }
108
+
109
+ /**
110
+ * Returns the project-level skills directory.
111
+ * @param {string} cwd - Project root directory
112
+ * @returns {string}
113
+ */
114
+ function getProjectSkillsDir(cwd) {
115
+ return path.join(getProjectConfigDir(cwd), 'skills');
116
+ }
117
+
118
+ /**
119
+ * Returns the PID file path.
120
+ * @param {string} cwd - Project root directory
121
+ * @returns {string}
122
+ */
123
+ function getPidFilePath(cwd) {
124
+ return path.join(getProjectConfigDir(cwd), 'runtime.pid');
125
+ }
126
+
127
+ // ── Global paths (~/.veil/) ─────────────────────────────────────────────
128
+
129
+ /**
130
+ * Returns the global ~/.veil/ directory.
131
+ * @returns {string}
132
+ */
133
+ function getGlobalConfigDir() {
134
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
135
+ }
136
+
137
+ /**
138
+ * Returns the global agents directory.
139
+ * @returns {string}
140
+ */
141
+ function getGlobalAgentsDir() {
142
+ return path.join(getGlobalConfigDir(), 'agents');
143
+ }
144
+
145
+ /**
146
+ * Returns the global tools directory.
147
+ * @returns {string}
148
+ */
149
+ function getGlobalToolsDir() {
150
+ return path.join(getGlobalConfigDir(), 'tools');
151
+ }
152
+
153
+ /**
154
+ * Returns the global skills directory.
155
+ * @returns {string}
156
+ */
157
+ function getGlobalSkillsDir() {
158
+ return path.join(getGlobalConfigDir(), 'skills');
159
+ }
160
+
161
+ /**
162
+ * Returns the global settings.json path.
163
+ * @returns {string}
164
+ */
165
+ function getGlobalSettingsPath() {
166
+ return path.join(getGlobalConfigDir(), 'settings.json');
167
+ }
168
+
169
+ /**
170
+ * Returns the global auth.json path.
171
+ * @returns {string}
172
+ */
173
+ function getGlobalAuthPath() {
174
+ return path.join(getGlobalConfigDir(), 'auth.json');
175
+ }
176
+
177
+ /**
178
+ * Returns the global SQLite database path.
179
+ * @returns {string}
180
+ */
181
+ function getDbPath() {
182
+ return path.join(getGlobalConfigDir(), 'data.db');
183
+ }
184
+
185
+ /**
186
+ * Returns the global backups directory.
187
+ * @returns {string}
188
+ */
189
+ function getBackupsDir() {
190
+ return path.join(getGlobalConfigDir(), 'backups');
191
+ }
192
+
193
+ module.exports = {
194
+ getProjectConfigDir,
195
+ getProjectAgentsDir,
196
+ getAgentDir,
197
+ getProjectSettingsPath,
198
+ getProjectSettingsLocalPath,
199
+ getProjectAuthPath,
200
+ getProjectAgentMdPath,
201
+ getProjectMemoryDir,
202
+ getAgentMemoryDir,
203
+ getHeartbeatsDir,
204
+ getProjectToolsDir,
205
+ getProjectSkillsDir,
206
+ getPidFilePath,
207
+ getGlobalConfigDir,
208
+ getGlobalAgentsDir,
209
+ getGlobalToolsDir,
210
+ getGlobalSettingsPath,
211
+ getGlobalAuthPath,
212
+ getDbPath,
213
+ };
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const paths = require('./paths');
6
+ const F = require('../settings/fields');
7
+
8
+ /**
9
+ * Deep merge: target is mutated with source values (source wins).
10
+ * @param {Object} target
11
+ * @param {Object} source
12
+ * @returns {Object}
13
+ */
14
+ function deepMerge(target, source) {
15
+ for (const key of Object.keys(source)) {
16
+ if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
17
+ if (!target[key] || typeof target[key] !== 'object') target[key] = {};
18
+ deepMerge(target[key], source[key]);
19
+ } else {
20
+ target[key] = source[key];
21
+ }
22
+ }
23
+ return target;
24
+ }
25
+
26
+ /**
27
+ * Safely read + parse a JSON file. Returns null if file doesn't exist.
28
+ * @param {string} filePath
29
+ * @returns {Object|null}
30
+ */
31
+ function readJsonFile(filePath) {
32
+ try {
33
+ const raw = fs.readFileSync(filePath, 'utf8');
34
+ return JSON.parse(raw);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Build default settings object.
42
+ * @returns {Object}
43
+ */
44
+ function getDefaults() {
45
+ return {
46
+ port: F.DEFAULT_PORT,
47
+ secret: null,
48
+ models: {
49
+ main: { [F.MODEL_BASE_URL]: 'https://openrouter.ai/api/v1', [F.MODEL_API_KEY]: '', [F.MODEL_NAME]: '' },
50
+ compact: null,
51
+ title: null,
52
+ },
53
+ permissions: {
54
+ [F.PERMISSIONS_ALLOW]: [],
55
+ [F.PERMISSIONS_DENY]: [],
56
+ [F.PERMISSIONS_ASK]: [],
57
+ },
58
+ hooks: {
59
+ [F.HOOKS_PRE_TOOL_USE]: null,
60
+ [F.HOOKS_POST_TOOL_USE]: null,
61
+ },
62
+ compaction: {
63
+ [F.COMPACTION_THRESHOLD]: F.DEFAULT_COMPACTION_THRESHOLD,
64
+ [F.COMPACTION_OBSERVATION_MASKING_TURNS]: F.DEFAULT_OBSERVATION_MASKING_TURNS,
65
+ },
66
+ memory: {
67
+ enabled: true,
68
+ maxLines: F.DEFAULT_MEMORY_MAX_LINES,
69
+ },
70
+ storage: {
71
+ retention: {
72
+ sessions: { maxAgeDays: 90, maxCount: 10000 },
73
+ tasks: { maxAgeDays: 30, maxCount: 5000 },
74
+ },
75
+ },
76
+ maxIterations: F.DEFAULT_MAX_ITERATIONS,
77
+ maxDurationSeconds: F.DEFAULT_MAX_DURATION_SECONDS,
78
+ maxConcurrentTasks: F.DEFAULT_MAX_CONCURRENT_TASKS,
79
+ maxSubAgentDepth: F.DEFAULT_MAX_SUBAGENT_DEPTH,
80
+ mcpServers: {},
81
+ ably: { enabled: false, key: null },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Load and merge settings from all layers:
87
+ * defaults → ~/.veil/settings.json → .veil/settings.json → .veil/settings.local.json → cliOverrides
88
+ *
89
+ * @param {string} cwd - Project root directory
90
+ * @param {Object} [cliOverrides={}] - CLI flag overrides
91
+ * @returns {Object} Merged settings
92
+ */
93
+ function loadSettings({ cwd, cliOverrides = {} }) {
94
+ const result = getDefaults();
95
+
96
+ const globalSettings = readJsonFile(paths.getGlobalSettingsPath());
97
+ if (globalSettings) deepMerge(result, globalSettings);
98
+
99
+ const globalAuth = readJsonFile(paths.getGlobalAuthPath());
100
+ if (globalAuth) {
101
+ if (globalAuth.models) deepMerge(result.models, globalAuth.models);
102
+ }
103
+
104
+ const projectSettings = readJsonFile(paths.getProjectSettingsPath(cwd));
105
+ if (projectSettings) deepMerge(result, projectSettings);
106
+
107
+ const projectAuth = readJsonFile(paths.getProjectAuthPath(cwd));
108
+ if (projectAuth) {
109
+ if (projectAuth.models) deepMerge(result.models, projectAuth.models);
110
+ }
111
+
112
+ const localSettings = readJsonFile(paths.getProjectSettingsLocalPath(cwd));
113
+ if (localSettings) deepMerge(result, localSettings);
114
+
115
+ if (cliOverrides && Object.keys(cliOverrides).length > 0) {
116
+ deepMerge(result, cliOverrides);
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Get the model config for a given role. Falls back to 'main' if role not configured.
124
+ * @param {Object} settings
125
+ * @param {'main'|'compact'|'title'} role
126
+ * @returns {{ base_url: string, api_key: string, model: string }}
127
+ */
128
+ function getModelConfig(settings, role = F.MODEL_MAIN) {
129
+ const roleConfig = settings.models && settings.models[role];
130
+ if (roleConfig && roleConfig[F.MODEL_NAME]) return roleConfig;
131
+ return settings.models[F.MODEL_MAIN];
132
+ }
133
+
134
+ const VALID_LEVELS = ['merged', 'project', 'global', 'local'];
135
+
136
+ /**
137
+ * Resolve the settings file path for a given level.
138
+ * @param {'project'|'global'|'local'} level
139
+ * @param {string} cwd
140
+ * @returns {string}
141
+ */
142
+ function settingsPathForLevel(level, cwd) {
143
+ if (level === 'global') return paths.getGlobalSettingsPath();
144
+ if (level === 'local') return paths.getProjectSettingsLocalPath(cwd);
145
+ return paths.getProjectSettingsPath(cwd);
146
+ }
147
+
148
+ /**
149
+ * Read raw settings for a single level without merging.
150
+ * - 'merged' → full merged loadSettings result
151
+ * - 'project' → .veil/settings.json (raw, or {} if absent)
152
+ * - 'global' → ~/.veil/settings.json (raw, or {} if absent)
153
+ * - 'local' → .veil/settings.local.json (raw, or {} if absent)
154
+ *
155
+ * @param {{ cwd: string, level?: string }} opts
156
+ * @returns {{ settings: Object, exists: boolean, path: string|null }}
157
+ */
158
+ function readSettingsForLevel({ cwd, level = 'merged' }) {
159
+ if (!VALID_LEVELS.includes(level)) {
160
+ throw Object.assign(new Error(`Invalid level "${level}". Must be one of: ${VALID_LEVELS.join(', ')}`), { code: 'INVALID_LEVEL' });
161
+ }
162
+
163
+ if (level === 'merged') {
164
+ return { settings: loadSettings({ cwd }), exists: true, path: null };
165
+ }
166
+
167
+ const filePath = settingsPathForLevel(level, cwd);
168
+ const raw = readJsonFile(filePath);
169
+ return { settings: raw || {}, exists: raw !== null, path: filePath };
170
+ }
171
+
172
+ module.exports = { loadSettings, getModelConfig, readSettingsForLevel, settingsPathForLevel, VALID_LEVELS };