@ikarmic/forge-core 5.0.0 → 5.1.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 (163) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent.d.ts +8 -0
  3. package/dist/agent/agent.js +148 -24
  4. package/dist/agent/background.d.ts +58 -0
  5. package/dist/agent/background.js +139 -0
  6. package/dist/agent/enhance-prompt.d.ts +3 -1
  7. package/dist/agent/enhance-prompt.js +6 -2
  8. package/dist/agent/goal.d.ts +54 -0
  9. package/dist/agent/goal.js +65 -0
  10. package/dist/agent/prompts.d.ts +25 -1
  11. package/dist/agent/prompts.js +61 -17
  12. package/dist/agents/agent-files.d.ts +43 -0
  13. package/dist/agents/agent-files.js +109 -0
  14. package/dist/checkpoints/store.js +5 -1
  15. package/dist/commands/custom-commands.d.ts +9 -4
  16. package/dist/commands/custom-commands.js +13 -2
  17. package/dist/config-file.d.ts +43 -0
  18. package/dist/config-file.js +249 -5
  19. package/dist/config-merge.d.ts +13 -0
  20. package/dist/config-merge.js +67 -0
  21. package/dist/config-schema.d.ts +340 -0
  22. package/dist/config-schema.js +126 -0
  23. package/dist/config.js +2 -1
  24. package/dist/context/compaction.js +4 -0
  25. package/dist/context/mask.js +1 -1
  26. package/dist/custom-providers.d.ts +95 -0
  27. package/dist/custom-providers.js +120 -0
  28. package/dist/git/auto-commit.d.ts +2 -2
  29. package/dist/git/auto-commit.js +5 -4
  30. package/dist/github/github-tools.d.ts +2 -2
  31. package/dist/hooks/runner.js +3 -2
  32. package/dist/import/claude-code.d.ts +39 -0
  33. package/dist/import/claude-code.js +254 -0
  34. package/dist/index.d.ts +34 -5
  35. package/dist/index.js +27 -4
  36. package/dist/journey/answers-block.d.ts +17 -0
  37. package/dist/journey/answers-block.js +15 -0
  38. package/dist/journey/index.d.ts +9 -0
  39. package/dist/journey/index.js +8 -0
  40. package/dist/journey/journey.d.ts +13 -0
  41. package/dist/journey/journey.js +40 -0
  42. package/dist/journey/offer.d.ts +25 -0
  43. package/dist/journey/offer.js +48 -0
  44. package/dist/journey/questions.d.ts +23 -0
  45. package/dist/journey/questions.js +49 -0
  46. package/dist/journey/stages.d.ts +13 -0
  47. package/dist/journey/stages.js +107 -0
  48. package/dist/journey/survey.d.ts +10 -0
  49. package/dist/journey/survey.js +203 -0
  50. package/dist/journey/trigger.d.ts +19 -0
  51. package/dist/journey/trigger.js +79 -0
  52. package/dist/journey/types.d.ts +72 -0
  53. package/dist/journey/types.js +1 -0
  54. package/dist/lsp/client.d.ts +5 -0
  55. package/dist/lsp/client.js +39 -3
  56. package/dist/mcp/index.d.ts +5 -1
  57. package/dist/mcp/index.js +3 -1
  58. package/dist/mcp/manager.d.ts +17 -3
  59. package/dist/mcp/manager.js +27 -2
  60. package/dist/mcp/marketplace.d.ts +2 -0
  61. package/dist/mcp/marketplace.js +10 -4
  62. package/dist/mcp/mcp-client.d.ts +12 -2
  63. package/dist/mcp/mcp-client.js +74 -17
  64. package/dist/mcp/mcp-config.d.ts +2 -0
  65. package/dist/mcp/mcp-config.js +1 -0
  66. package/dist/mcp/oauth.d.ts +55 -0
  67. package/dist/mcp/oauth.js +197 -0
  68. package/dist/mcp/resource-tools.d.ts +8 -0
  69. package/dist/mcp/resource-tools.js +56 -0
  70. package/dist/memory/session-digest.d.ts +12 -0
  71. package/dist/memory/session-digest.js +47 -0
  72. package/dist/memory/stale.js +2 -1
  73. package/dist/model-alias.d.ts +18 -2
  74. package/dist/model-alias.js +27 -3
  75. package/dist/model-catalog.js +0 -1
  76. package/dist/model-list.js +2 -1
  77. package/dist/model-overrides.d.ts +30 -0
  78. package/dist/model-overrides.js +48 -0
  79. package/dist/orchestrator/specialists-registry.js +2 -3
  80. package/dist/orchestrator/specialists.js +4 -4
  81. package/dist/policy/ca-certs.d.ts +8 -0
  82. package/dist/policy/ca-certs.js +30 -0
  83. package/dist/policy/proxy.d.ts +10 -3
  84. package/dist/policy/proxy.js +33 -6
  85. package/dist/pricing.js +4 -0
  86. package/dist/providers.d.ts +7 -0
  87. package/dist/providers.js +39 -5
  88. package/dist/semantic/embedder.d.ts +15 -0
  89. package/dist/semantic/embedder.js +25 -0
  90. package/dist/semantic/semantic-index.d.ts +10 -2
  91. package/dist/semantic/semantic-index.js +16 -7
  92. package/dist/sessions/session-store.js +2 -1
  93. package/dist/skills/builtin.d.ts +2 -0
  94. package/dist/skills/builtin.js +68 -0
  95. package/dist/skills/index.d.ts +2 -0
  96. package/dist/skills/index.js +2 -0
  97. package/dist/skills/loader.d.ts +10 -3
  98. package/dist/skills/loader.js +20 -7
  99. package/dist/skills/marketplace.js +2 -1
  100. package/dist/skills/remote.d.ts +14 -0
  101. package/dist/skills/remote.js +131 -0
  102. package/dist/skills/types.d.ts +10 -0
  103. package/dist/specs/generate.d.ts +18 -9
  104. package/dist/specs/generate.js +174 -14
  105. package/dist/specs/index.d.ts +2 -2
  106. package/dist/specs/index.js +1 -1
  107. package/dist/specs/store.d.ts +14 -2
  108. package/dist/specs/store.js +26 -1
  109. package/dist/specs/types.d.ts +11 -0
  110. package/dist/steering/extra-instructions.d.ts +8 -0
  111. package/dist/steering/extra-instructions.js +104 -0
  112. package/dist/steering/folder-instructions.d.ts +8 -0
  113. package/dist/steering/folder-instructions.js +28 -0
  114. package/dist/steering/select.js +2 -1
  115. package/dist/timed-fetch.d.ts +12 -0
  116. package/dist/timed-fetch.js +83 -0
  117. package/dist/tools/apply-patch.d.ts +52 -0
  118. package/dist/tools/apply-patch.js +197 -0
  119. package/dist/tools/ask-user.d.ts +27 -0
  120. package/dist/tools/ask-user.js +60 -0
  121. package/dist/tools/browser-tools.d.ts +6 -6
  122. package/dist/tools/browser-tools.js +3 -13
  123. package/dist/tools/codebase-search.d.ts +4 -2
  124. package/dist/tools/codebase-search.js +2 -2
  125. package/dist/tools/edit-file.js +5 -3
  126. package/dist/tools/fetch-url.d.ts +7 -0
  127. package/dist/tools/fetch-url.js +52 -19
  128. package/dist/tools/find-symbol.d.ts +2 -2
  129. package/dist/tools/ignore-rules.js +2 -1
  130. package/dist/tools/index.d.ts +5 -1
  131. package/dist/tools/index.js +10 -1
  132. package/dist/tools/memory.d.ts +6 -6
  133. package/dist/tools/notebook.d.ts +2 -2
  134. package/dist/tools/permission-rules.d.ts +32 -0
  135. package/dist/tools/permission-rules.js +182 -0
  136. package/dist/tools/read-file.d.ts +5 -0
  137. package/dist/tools/read-file.js +12 -2
  138. package/dist/tools/read-only.d.ts +1 -1
  139. package/dist/tools/read-only.js +2 -0
  140. package/dist/tools/run-command.d.ts +4 -2
  141. package/dist/tools/run-command.js +12 -8
  142. package/dist/tools/sandbox-loader.js +3 -13
  143. package/dist/tools/search-files.d.ts +8 -2
  144. package/dist/tools/search-files.js +43 -0
  145. package/dist/tools/shared.d.ts +5 -0
  146. package/dist/tools/shared.js +30 -2
  147. package/dist/tools/shell-env.d.ts +7 -0
  148. package/dist/tools/shell-env.js +9 -0
  149. package/dist/tools/sub-agent-wait.d.ts +29 -0
  150. package/dist/tools/sub-agent-wait.js +60 -0
  151. package/dist/tools/sub-agent.d.ts +50 -4
  152. package/dist/tools/sub-agent.js +213 -125
  153. package/dist/tools/text-encoding.d.ts +10 -0
  154. package/dist/tools/text-encoding.js +40 -0
  155. package/dist/tools/todo.d.ts +36 -0
  156. package/dist/tools/todo.js +42 -0
  157. package/dist/tools/write-file.js +15 -9
  158. package/dist/tools/write-tools.d.ts +12 -0
  159. package/dist/tools/write-tools.js +18 -0
  160. package/dist/types.d.ts +98 -0
  161. package/dist/worktree/worktree.d.ts +3 -0
  162. package/dist/worktree/worktree.js +22 -0
  163. package/package.json +1 -1
@@ -1,7 +1,178 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
+ import { customProviderProblems } from './custom-providers.js';
6
+ const PERMISSION_ACTIONS = new Set(['allow', 'ask', 'deny']);
7
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
8
+ const positiveNumber = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0;
9
+ const nonNegativeNumber = (n) => typeof n === 'number' && Number.isFinite(n) && n >= 0;
10
+ const stringList = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined);
11
+ const BLOCK_KINDS = {
12
+ reasoning: ['hidden', 'collapsed', 'expanded'],
13
+ terminal: ['collapsed', 'expanded'],
14
+ edit: ['collapsed', 'expanded'],
15
+ mcp: ['collapsed', 'expanded'],
16
+ };
17
+ /** `display.blocks`: keep the kinds and values the chat understands, warn once about each thing it would silently ignore. */
18
+ function cleanDisplayBlocks(config, filePath, warnings) {
19
+ const blocks = config.display?.blocks;
20
+ if (blocks === undefined)
21
+ return;
22
+ const display = config.display;
23
+ if (!isObject(blocks)) {
24
+ warnings.push(`${filePath}: display.blocks must be an object (reasoning, terminal, edit, mcp); ignored`);
25
+ delete display.blocks;
26
+ return;
27
+ }
28
+ const kept = {};
29
+ for (const [kind, value] of Object.entries(blocks)) {
30
+ const allowed = BLOCK_KINDS[kind];
31
+ if (!allowed)
32
+ warnings.push(`${filePath}: display.blocks.${kind} is not a block kind (${Object.keys(BLOCK_KINDS).join(', ')}); ignored`);
33
+ else if (typeof value !== 'string' || !allowed.includes(value))
34
+ warnings.push(`${filePath}: display.blocks.${kind} must be ${allowed.join(', ')}; ignored`);
35
+ else
36
+ kept[kind] = value;
37
+ }
38
+ if (Object.keys(kept).length)
39
+ display.blocks = kept;
40
+ else
41
+ delete display.blocks;
42
+ }
43
+ /** Validate `customProviders`, `modelOverrides`, `modelFilters` and `subagentModel`, dropping what cannot be used with one warning each. */
44
+ function cleanProviderKeys(config, filePath, warnings) {
45
+ if (config.customProviders !== undefined) {
46
+ const raw = config.customProviders;
47
+ if (!isObject(raw)) {
48
+ warnings.push(`${filePath}: "customProviders" must be an object of provider id → settings; ignored`);
49
+ delete config.customProviders;
50
+ }
51
+ else {
52
+ const kept = {};
53
+ const bad = new Set(customProviderProblems(raw).map((p) => p.id));
54
+ for (const p of customProviderProblems(raw))
55
+ warnings.push(`${filePath}: ${p.message} Ignored.`);
56
+ for (const [id, value] of Object.entries(raw)) {
57
+ if (bad.has(id) || !isObject(value))
58
+ continue;
59
+ const entry = { ...value };
60
+ if (entry.headers !== undefined && !(isObject(entry.headers) && Object.values(entry.headers).every((h) => typeof h === 'string'))) {
61
+ warnings.push(`${filePath}: customProviders.${id}.headers must be text values; headers ignored`);
62
+ delete entry.headers;
63
+ }
64
+ kept[id] = entry;
65
+ }
66
+ if (Object.keys(kept).length)
67
+ config.customProviders = kept;
68
+ else
69
+ delete config.customProviders;
70
+ }
71
+ }
72
+ if (config.modelOverrides !== undefined) {
73
+ const raw = config.modelOverrides;
74
+ if (!isObject(raw)) {
75
+ warnings.push(`${filePath}: "modelOverrides" must be an object of model → numbers; ignored`);
76
+ delete config.modelOverrides;
77
+ }
78
+ else {
79
+ const kept = {};
80
+ for (const [model, value] of Object.entries(raw)) {
81
+ if (!isObject(value)) {
82
+ warnings.push(`${filePath}: modelOverrides.${model} must be an object; ignored`);
83
+ continue;
84
+ }
85
+ const out = {};
86
+ const limit = isObject(value.limit) ? value.limit : undefined;
87
+ if (limit) {
88
+ const l = {};
89
+ for (const k of ['context', 'output']) {
90
+ if (limit[k] === undefined)
91
+ continue;
92
+ if (positiveNumber(limit[k]))
93
+ l[k] = limit[k];
94
+ else
95
+ warnings.push(`${filePath}: modelOverrides.${model}.limit.${k} must be a positive number; ignored`);
96
+ }
97
+ if (Object.keys(l).length)
98
+ out.limit = l;
99
+ }
100
+ const cost = isObject(value.cost) ? value.cost : undefined;
101
+ if (cost) {
102
+ const c = {};
103
+ for (const k of ['input', 'output']) {
104
+ if (cost[k] === undefined)
105
+ continue;
106
+ if (nonNegativeNumber(cost[k]))
107
+ c[k] = cost[k];
108
+ else
109
+ warnings.push(`${filePath}: modelOverrides.${model}.cost.${k} must be a number of 0 or more; ignored`);
110
+ }
111
+ if (Object.keys(c).length)
112
+ out.cost = c;
113
+ }
114
+ if (value.limit !== undefined && !limit)
115
+ warnings.push(`${filePath}: modelOverrides.${model}.limit must be an object; ignored`);
116
+ if (Object.keys(out).length)
117
+ kept[model] = out;
118
+ }
119
+ if (Object.keys(kept).length)
120
+ config.modelOverrides = kept;
121
+ else
122
+ delete config.modelOverrides;
123
+ }
124
+ }
125
+ if (config.modelFilters !== undefined) {
126
+ const raw = config.modelFilters;
127
+ if (!isObject(raw)) {
128
+ warnings.push(`${filePath}: "modelFilters" must be an object of provider → lists; ignored`);
129
+ delete config.modelFilters;
130
+ }
131
+ else {
132
+ const kept = {};
133
+ for (const [provider, value] of Object.entries(raw)) {
134
+ if (!isObject(value))
135
+ continue;
136
+ const whitelist = stringList(value.whitelist);
137
+ const blacklist = stringList(value.blacklist);
138
+ if (whitelist?.length || blacklist?.length)
139
+ kept[provider] = { ...(whitelist?.length ? { whitelist } : {}), ...(blacklist?.length ? { blacklist } : {}) };
140
+ }
141
+ if (Object.keys(kept).length)
142
+ config.modelFilters = kept;
143
+ else
144
+ delete config.modelFilters;
145
+ }
146
+ }
147
+ if (config.instructions !== undefined) {
148
+ const list = stringList(config.instructions)?.map((s) => s.trim()).filter(Boolean);
149
+ if (list?.length)
150
+ config.instructions = list;
151
+ else
152
+ delete config.instructions;
153
+ }
154
+ if (config.skillPaths !== undefined) {
155
+ const list = stringList(config.skillPaths)?.map((s) => s.trim()).filter(Boolean);
156
+ if (list?.length)
157
+ config.skillPaths = list;
158
+ else
159
+ delete config.skillPaths;
160
+ }
161
+ if (config.caCertificates !== undefined) {
162
+ const list = stringList(config.caCertificates)?.map((s) => s.trim()).filter(Boolean);
163
+ if (list?.length)
164
+ config.caCertificates = list;
165
+ else
166
+ delete config.caCertificates;
167
+ }
168
+ if (config.subagentModel !== undefined) {
169
+ const v = typeof config.subagentModel === 'string' ? config.subagentModel.trim() : '';
170
+ if (v)
171
+ config.subagentModel = v;
172
+ else
173
+ delete config.subagentModel;
174
+ }
175
+ }
5
176
  /** Returns `~/.forge/config.json` for the current user. */
6
177
  export function defaultForgeConfigPath() {
7
178
  return path.join(homedir(), '.forge', 'config.json');
@@ -40,6 +211,41 @@ export async function loadForgeConfigFile(configPath) {
40
211
  }
41
212
  }
42
213
  }
214
+ if (config.permission !== undefined) {
215
+ const raw = config.permission;
216
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
217
+ warnings.push(`${filePath}: "permission" must be an object of permission → action or pattern map; ignored`);
218
+ delete config.permission;
219
+ }
220
+ else {
221
+ const kept = {};
222
+ for (const [perm, value] of Object.entries(raw)) {
223
+ if (typeof value === 'string') {
224
+ if (PERMISSION_ACTIONS.has(value))
225
+ kept[perm] = value;
226
+ else
227
+ warnings.push(`${filePath}: permission.${perm} is "${value}", not allow, ask or deny; ignored`);
228
+ }
229
+ else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
230
+ const patterns = {};
231
+ for (const [pattern, action] of Object.entries(value)) {
232
+ if (typeof action === 'string' && PERMISSION_ACTIONS.has(action))
233
+ patterns[pattern] = action;
234
+ else
235
+ warnings.push(`${filePath}: permission.${perm}["${pattern}"] is "${String(action)}", not allow, ask or deny; ignored`);
236
+ }
237
+ if (Object.keys(patterns).length)
238
+ kept[perm] = patterns;
239
+ }
240
+ else {
241
+ warnings.push(`${filePath}: permission.${perm} must be allow, ask, deny or a pattern map; ignored`);
242
+ }
243
+ }
244
+ config.permission = kept;
245
+ }
246
+ }
247
+ cleanProviderKeys(config, filePath, warnings);
248
+ cleanDisplayBlocks(config, filePath, warnings);
43
249
  return { config, warnings };
44
250
  }
45
251
  catch (err) {
@@ -52,8 +258,46 @@ export async function loadForgeConfigFile(configPath) {
52
258
  */
53
259
  export async function saveForgeConfigFile(partial, configPath) {
54
260
  const filePath = configPath ?? defaultForgeConfigPath();
55
- const { config: existing } = await loadForgeConfigFile(filePath);
56
- const merged = { ...existing, ...partial };
57
- await mkdir(path.dirname(filePath), { recursive: true });
58
- await writeFile(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
261
+ await updateJsonFile(filePath, (existing) => ({ ...existing, ...partial }));
262
+ }
263
+ const fileQueues = new Map();
264
+ /**
265
+ * Read-modify-write of a JSON settings file, safely:
266
+ * - merged over the file as written, not the cleaned config, so keys this version drops or does not know survive;
267
+ * - a file that exists but is not valid JSON is never overwritten (that would replace every setting with one);
268
+ * - writes to one path from this process run one after another, so two saves at once both land;
269
+ * - written to a temporary file and renamed, so a crash cannot leave half a file.
270
+ */
271
+ export function updateJsonFile(filePath, update) {
272
+ const key = path.resolve(filePath);
273
+ const run = async () => {
274
+ let existing = {};
275
+ let text;
276
+ try {
277
+ text = await readFile(filePath, 'utf8');
278
+ }
279
+ catch (err) {
280
+ if (err.code !== 'ENOENT')
281
+ throw err;
282
+ }
283
+ if (text !== undefined && text.trim()) {
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(text);
287
+ }
288
+ catch (err) {
289
+ throw new Error(`${filePath} is not valid JSON (${err.message}); fix it before saving, so your other settings are not lost`);
290
+ }
291
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
292
+ throw new Error(`${filePath} must hold a JSON object; not saving over it`);
293
+ existing = parsed;
294
+ }
295
+ await mkdir(path.dirname(filePath), { recursive: true });
296
+ const tmp = `${filePath}.${process.pid}.tmp`;
297
+ await writeFile(tmp, JSON.stringify(update(existing), null, 2) + '\n', 'utf8');
298
+ await rename(tmp, filePath);
299
+ };
300
+ const next = (fileQueues.get(key) ?? Promise.resolve()).then(run, run);
301
+ fileQueues.set(key, next.catch(() => undefined));
302
+ return next;
59
303
  }
@@ -0,0 +1,13 @@
1
+ import { type ForgeConfigFile, type LoadForgeConfigFileResult } from './config-file.js';
2
+ /**
3
+ * The shared config as one object: `~/.forge/config.json`, then the workspace's `.forge/config.json` on top.
4
+ * A project file can pick the model and add stricter rules, but it can never loosen what the user decided:
5
+ * its `allow` permission rules are dropped, and the keys that hold approvals or delete data are only read
6
+ * from the user file. An untrusted workspace contributes nothing.
7
+ */
8
+ export declare function projectConfigPath(cwd: string): string;
9
+ export declare const USER_ONLY_CONFIG_KEYS: ReadonlyArray<keyof ForgeConfigFile>;
10
+ export declare function loadMergedForgeConfig(cwd: string, opts: {
11
+ trusted: boolean;
12
+ userPath?: string;
13
+ }): Promise<LoadForgeConfigFileResult>;
@@ -0,0 +1,67 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadForgeConfigFile } from './config-file.js';
4
+ /**
5
+ * The shared config as one object: `~/.forge/config.json`, then the workspace's `.forge/config.json` on top.
6
+ * A project file can pick the model and add stricter rules, but it can never loosen what the user decided:
7
+ * its `allow` permission rules are dropped, and the keys that hold approvals or delete data are only read
8
+ * from the user file. An untrusted workspace contributes nothing.
9
+ */
10
+ export function projectConfigPath(cwd) {
11
+ return path.join(cwd, '.forge', 'config.json');
12
+ }
13
+ // customProviders is user-only because an entry names a URL and the environment variable whose value is sent there:
14
+ // a repository must not be able to point a key at a host of its choosing.
15
+ export const USER_ONLY_CONFIG_KEYS = ['autoApproveTools', 'autoApproveCommands', 'sessionRetentionDays', 'customProviders', 'instructions', 'skillPaths', 'caCertificates'];
16
+ export async function loadMergedForgeConfig(cwd, opts) {
17
+ const user = await loadForgeConfigFile(opts.userPath);
18
+ const file = projectConfigPath(cwd);
19
+ if (!opts.trusted || !existsSync(file))
20
+ return user;
21
+ const project = await loadForgeConfigFile(file);
22
+ const warnings = [...user.warnings, ...project.warnings];
23
+ const p = project.config;
24
+ const merged = { ...user.config };
25
+ for (const key of USER_ONLY_CONFIG_KEYS) {
26
+ if (p[key] !== undefined)
27
+ warnings.push(`${file}: "${key}" is only read from the user config (~/.forge/config.json); ignored`);
28
+ }
29
+ for (const key of ['provider', 'model', 'completionModel', 'compactionModel', 'reasoningEffort', 'subagentModel']) {
30
+ if (p[key] !== undefined)
31
+ merged[key] = p[key];
32
+ }
33
+ if (p.modelOverrides)
34
+ merged.modelOverrides = { ...user.config.modelOverrides, ...p.modelOverrides };
35
+ if (p.aliases)
36
+ merged.aliases = { ...user.config.aliases, ...p.aliases };
37
+ if (p.modeModels)
38
+ merged.modeModels = { ...user.config.modeModels, ...p.modeModels };
39
+ if (p.display)
40
+ merged.display = { ...user.config.display, ...p.display };
41
+ if (p.disabledProviders)
42
+ merged.disabledProviders = [...new Set([...(user.config.disabledProviders ?? []), ...p.disabledProviders])];
43
+ if (p.permission) {
44
+ const permission = tighten(user.config.permission, p.permission);
45
+ if (permission)
46
+ merged.permission = permission;
47
+ }
48
+ return { config: merged, warnings };
49
+ }
50
+ /** User rules first, then the project's `ask` and `deny` rules (an `allow` is dropped), each after everything of the user's. */
51
+ function tighten(user, project) {
52
+ const out = { ...(user ?? {}) };
53
+ for (const [permission, value] of Object.entries(project)) {
54
+ const rules = typeof value === 'string' ? [['*', value]] : Object.entries(value);
55
+ const kept = rules.filter(([, action]) => action !== 'allow');
56
+ if (kept.length === 0)
57
+ continue;
58
+ const existing = out[permission];
59
+ const target = typeof existing === 'string' ? { '*': existing } : { ...(existing ?? {}) };
60
+ for (const [pattern, action] of kept) {
61
+ delete target[pattern];
62
+ target[pattern] = action;
63
+ }
64
+ out[permission] = target;
65
+ }
66
+ return Object.keys(out).length > 0 ? out : undefined;
67
+ }