@wirecat/cli-core 0.421.0 → 0.423.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/analysis/settings.d.ts +0 -6
  3. package/dist/analysis/settings.js +2 -5
  4. package/dist/cli/analysis-choice.d.ts +0 -1
  5. package/dist/cli/analysis-choice.js +9 -22
  6. package/dist/cli/bot/context.js +1 -1
  7. package/dist/cli/commands-command.js +1 -1
  8. package/dist/cli/config-command.d.ts +0 -4
  9. package/dist/cli/config-command.js +36 -26
  10. package/dist/cli/config-issues.d.ts +52 -0
  11. package/dist/cli/config-issues.js +187 -0
  12. package/dist/cli/config-keys.js +4 -28
  13. package/dist/cli/embedding-choice.d.ts +1 -2
  14. package/dist/cli/embedding-choice.js +2 -4
  15. package/dist/cli/first-run.d.ts +0 -16
  16. package/dist/cli/first-run.js +1 -6
  17. package/dist/cli/index.d.ts +3 -1
  18. package/dist/cli/index.js +2 -1
  19. package/dist/cli/messenger/context.js +1 -1
  20. package/dist/cli/messenger/conversations-command.js +5 -9
  21. package/dist/cli/messenger/doctor-command.js +38 -4
  22. package/dist/cli/messenger/inbox.js +2 -6
  23. package/dist/cli/messenger/private-people-command.js +1 -2
  24. package/dist/cli/messenger/review.js +2 -5
  25. package/dist/cli/permission-keys.d.ts +9 -2
  26. package/dist/cli/permission-keys.js +21 -6
  27. package/dist/cli/preview.d.ts +3 -2
  28. package/dist/cli/preview.js +4 -3
  29. package/dist/cli/profile.d.ts +2 -0
  30. package/dist/cli/profile.js +10 -0
  31. package/dist/cli/program.d.ts +1 -1
  32. package/dist/cli/program.js +18 -10
  33. package/dist/cli/settings.d.ts +21 -117
  34. package/dist/cli/settings.js +107 -114
  35. package/dist/cli/shared-command-exports.d.ts +4 -232
  36. package/dist/mcp/surface.js +9 -3
  37. package/dist/meetings/mcp/index.d.ts +15 -15
  38. package/dist/models/settings.d.ts +1 -7
  39. package/dist/models/settings.js +1 -18
  40. package/dist/ports/app.d.ts +6 -0
  41. package/dist/search/account-list.js +8 -7
  42. package/dist/search/dictionary.d.ts +3 -6
  43. package/dist/search/dictionary.js +2 -1
  44. package/dist/search/languages.d.ts +28 -0
  45. package/dist/search/languages.js +7 -0
  46. package/dist/search/stem.d.ts +5 -4
  47. package/dist/search/stem.js +3 -2
  48. package/package.json +1 -1
@@ -2,6 +2,7 @@ import * as v from "valibot";
2
2
  import { type AISettings } from "../analysis/settings.js";
3
3
  import { type AppIdentity } from "../ports/app.js";
4
4
  import { type Level, type PermissionKey } from "../sends/permissions.js";
5
+ import { type ConfigIssue } from "./config-issues.js";
5
6
  export declare const plain: (rule: string) => (issue: v.BaseIssue<unknown>) => string;
6
7
  export declare const count: v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>;
7
8
  export declare const flag: v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>;
@@ -41,7 +42,6 @@ interface KindSection {
41
42
  profiles?: Record<string, Scope>;
42
43
  }
43
44
  export interface Config {
44
- default_profile?: string;
45
45
  defaults?: Scope;
46
46
  profiles: Record<string, Scope>;
47
47
  personal?: KindSection;
@@ -127,6 +127,19 @@ export interface Settings extends AISettings {
127
127
  /** This profile's entries and the shared defaults, for an adapter resolving its own fields with `first`. */
128
128
  configured: Scope;
129
129
  shared: Scope;
130
+ /**
131
+ * What is wrong in the file; empty unless resolved `withIssues`, which leaves those settings out. Optional for an
132
+ * app that builds `Settings` from a file of its own.
133
+ */
134
+ configIssues?: ConfigIssue[];
135
+ /**
136
+ * Every permission key saved anywhere in the file, for the program to check against its commands. An app that
137
+ * builds `Settings` itself leaves it out, and only the keys in force are checked.
138
+ */
139
+ savedPermissions?: {
140
+ key: PermissionKey;
141
+ at: string[];
142
+ }[];
130
143
  }
131
144
  export interface ResolveOptions {
132
145
  env?: NodeJS.ProcessEnv;
@@ -140,6 +153,11 @@ export interface ResolveOptions {
140
153
  needsProfile?: boolean;
141
154
  /** Read the config file without writing it: no default file. For a dry run. */
142
155
  readOnly?: boolean;
156
+ /**
157
+ * Load what is valid and report the rest in `configIssues`, rather than refuse: for `doctor`, which reports,
158
+ * and `config`, which repairs. Invalid JSON loads as an empty file.
159
+ */
160
+ withIssues?: boolean;
143
161
  }
144
162
  /** Where `profile` came from when none was named and the command needs none. */
145
163
  export declare const NONE_NAMED = "none named";
@@ -161,25 +179,22 @@ export declare const fromFile: <T>(settings: Pick<Settings, "configured" | "shar
161
179
  export declare const settingsFor: <T extends object = Record<never, never>>(app: AppIdentity, extension?: SettingsExtension<T>) => {
162
180
  resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Settings & T;
163
181
  configuredProfiles: (options?: ResolveOptions) => string[];
164
- changeSetting: (path: string, { profile, setting, value, kind, }: {
182
+ changeSetting: (path: string, { profile, setting, value, kind, root, }: {
165
183
  profile: string | undefined;
166
184
  setting: string;
167
185
  value: string | undefined;
168
186
  kind?: ProfileKind | undefined;
187
+ root?: boolean;
169
188
  }) => unknown;
170
189
  allSettings: string[];
171
190
  defaultsOnlyKeys: string[];
172
191
  schema: v.StrictObjectSchema<{
173
- default_profile: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
174
192
  defaults: v.OptionalSchema<v.StrictObjectSchema<{
175
193
  embedding_provider: ({
176
194
  embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
177
195
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
178
196
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
179
197
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
180
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
181
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
182
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
183
198
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
184
199
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
185
200
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -191,9 +206,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
191
206
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
192
207
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
193
208
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
194
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
195
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
196
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
197
209
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
198
210
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
199
211
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -205,9 +217,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
205
217
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
206
218
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
207
219
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
208
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
209
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
210
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
211
220
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
212
221
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
213
222
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -219,65 +228,17 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
219
228
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
220
229
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
221
230
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
222
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
223
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
224
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
225
231
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
226
232
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
227
233
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
228
234
  readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
229
235
  }, undefined>, undefined>, undefined>;
230
236
  })["embeddingDims"];
231
- analysis_provider: ({
232
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
233
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
234
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
235
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
236
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
237
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
238
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
239
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
240
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
241
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
242
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
243
- }, undefined>, undefined>, undefined>;
244
- })["analysisProvider"];
245
- analysis_model: ({
246
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
247
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
248
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
249
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
250
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
251
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
252
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
253
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
254
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
255
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
256
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
257
- }, undefined>, undefined>, undefined>;
258
- })["analysisModel"];
259
- analysis_base_url: ({
260
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
261
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
262
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
263
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
264
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
265
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
266
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
267
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
268
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
269
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
270
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
271
- }, undefined>, undefined>, undefined>;
272
- })["analysisBaseUrl"];
273
237
  models: ({
274
238
  embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
275
239
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
276
240
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
277
241
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
278
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
279
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
280
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
281
242
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
282
243
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
283
244
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -306,9 +267,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
306
267
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
307
268
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
308
269
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
309
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
310
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
311
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
312
270
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
313
271
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
314
272
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -320,9 +278,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
320
278
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
321
279
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
322
280
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
323
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
324
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
325
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
326
281
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
327
282
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
328
283
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -334,9 +289,6 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
334
289
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
335
290
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
336
291
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
337
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
338
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
339
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
340
292
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
341
293
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
342
294
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -348,65 +300,17 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
348
300
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
349
301
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
350
302
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
351
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
352
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
353
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
354
303
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
355
304
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
356
305
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
357
306
  readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
358
307
  }, undefined>, undefined>, undefined>;
359
308
  })["embeddingDims"];
360
- analysis_provider: ({
361
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
362
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
363
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
364
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
365
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
366
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
367
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
368
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
369
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
370
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
371
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
372
- }, undefined>, undefined>, undefined>;
373
- })["analysisProvider"];
374
- analysis_model: ({
375
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
376
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
377
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
378
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
379
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
380
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
381
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
382
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
383
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
384
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
385
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
386
- }, undefined>, undefined>, undefined>;
387
- })["analysisModel"];
388
- analysis_base_url: ({
389
- embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
390
- embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
391
- embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
392
- embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
393
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
394
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
395
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
396
- models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
397
- readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
398
- readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
399
- readonly base_url: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
400
- }, undefined>, undefined>, undefined>;
401
- })["analysisBaseUrl"];
402
309
  models: ({
403
310
  embeddingProvider: v.OptionalSchema<v.PicklistSchema<["local", "openai"], undefined>, undefined>;
404
311
  embeddingModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
405
312
  embeddingBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
406
313
  embeddingDims: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 65536, undefined>]>, undefined>;
407
- analysisProvider: v.OptionalSchema<v.PicklistSchema<["agent", "openai", "anthropic"], undefined>, undefined>;
408
- analysisModel: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
409
- analysisBaseUrl: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "use an HTTP/S endpoint without credentials, query or fragment">]>, undefined>;
410
314
  models: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, v.StrictObjectSchema<{
411
315
  readonly provider: v.OptionalSchema<v.PicklistSchema<["off", "openai", "anthropic"], undefined>, undefined>;
412
316
  readonly model: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { CliError, configFilePath, resolvePaths, saveConfigFile } from "@wirecat/cli-base";
3
3
  import * as v from "valibot";
4
4
  import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
@@ -7,7 +7,7 @@ import { envName, readOnlyLockOf } from "../ports/app.js";
7
7
  import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
8
8
  import { storePath } from "../store/path.js";
9
9
  import { sharedConfigPath } from "../store/shared-config.js";
10
- import { loadSnakeCaseConfig } from "./config-keys.js";
10
+ import { issueMessage, parseConfigText, publicIssue, savedPermissionKeys, schemaIssues, withoutIssues, } from "./config-issues.js";
11
11
  import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
12
12
  import { profilesHere } from "./messenger/accounts.js";
13
13
  import { usableProfileName } from "./profile.js";
@@ -108,7 +108,6 @@ export const settingsFor = (app, extension = {}) => {
108
108
  profiles: v.optional(v.record(v.string(), strictOf(entries), "has to be an object of profiles, by name")),
109
109
  }, objectMessage);
110
110
  const configEntries = {
111
- default_profile: v.optional(v.string(plain("has to be a profile name, in quotes"))),
112
111
  defaults: v.optional(strictOf(defaultsEntries)),
113
112
  profiles: v.optional(v.record(v.string(), strictOf(profileEntries), "has to be an object of profiles, by name"), {}),
114
113
  personal: v.optional(kindSection(profileEntries)),
@@ -117,22 +116,35 @@ export const settingsFor = (app, extension = {}) => {
117
116
  const schema = strictOf(configEntries);
118
117
  const defaultsOnlyKeys = Object.keys(defaultsOnly);
119
118
  const botOnlyKeys = Object.keys(BOT_ONLY_ENTRIES);
120
- const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys, "default_profile"];
119
+ const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys];
121
120
  const PROFILE = envName(app, "PROFILE");
122
121
  const PROFILE_LOCK = envName(app, "PROFILE_LOCK");
123
122
  const TIMEOUT = envName(app, "TIMEOUT");
124
123
  const REQUESTS_PER_MINUTE = envName(app, "REQUESTS_PER_MINUTE");
125
124
  const configPathFor = ({ env = process.env, configDir }) => configFilePath(configDir ?? resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config);
126
- const readConfig = (path) => {
125
+ const readSchema = extension.schema ?? schema;
126
+ const context = (file) => ({ file, tool: app.command });
127
+ /** The file as written, `undefined` when there is none, or the issue that says it is not JSON. */
128
+ const readRaw = (path) => {
129
+ let text;
127
130
  try {
128
- return loadSnakeCaseConfig(path, extension.schema ?? schema, () => ({ profiles: {} }));
131
+ text = readFileSync(path, "utf8");
129
132
  }
130
- catch (error) {
131
- const message = error instanceof Error ? error.message : String(error);
132
- throw new CliError("configuration_error", message.includes("catch_up_marks_read")
133
- ? `${message}; remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`
134
- : message);
133
+ catch {
134
+ return { raw: undefined };
135
135
  }
136
+ return parseConfigText(text, path);
137
+ };
138
+ /** The file with every problem found; what loads is the file without the settings they name. */
139
+ const loadConfig = (path) => {
140
+ const read = readRaw(path);
141
+ if ("issue" in read)
142
+ return { config: { profiles: {} }, issues: [read.issue], raw: undefined };
143
+ if (read.raw === undefined)
144
+ return { config: { profiles: {} }, issues: [], raw: undefined };
145
+ const issues = schemaIssues(read.raw, readSchema, context(path));
146
+ const parsed = v.safeParse(readSchema, withoutIssues(read.raw, issues));
147
+ return { config: (parsed.success ? parsed.output : { profiles: {} }), issues, raw: read.raw };
136
148
  };
137
149
  /**
138
150
  * A lock pins a process to one profile: the owner sets it where an agent runs, and a first word
@@ -157,10 +169,6 @@ export const settingsFor = (app, extension = {}) => {
157
169
  const unnamed = (config, env, needed) => {
158
170
  if (!needed)
159
171
  return { value: NO_PROFILE, from: NONE_NAMED };
160
- if (config.default_profile !== undefined) {
161
- throw new CliError("validation_error", `default_profile is no longer read — name the profile: \`${app.command} ${config.default_profile} …\`; ` +
162
- `\`${app.command} config unset default_profile\` removes it from the config file`, { profiles: profilesOf(config, env) });
163
- }
164
172
  const here = profilesOf(config, env);
165
173
  throw new CliError("validation_error", here.length === 0
166
174
  ? `name a profile: there is none on this computer yet — create one with \`${app.command} personal setup\` ` +
@@ -178,7 +186,10 @@ export const settingsFor = (app, extension = {}) => {
178
186
  catch (error) {
179
187
  throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
180
188
  }
181
- const config = readConfig(configPath);
189
+ const loaded = loadConfig(configPath);
190
+ if (loaded.issues.length > 0 && !options.withIssues)
191
+ throw new CliError("configuration_error", issueMessage(loaded.issues.map(publicIssue)));
192
+ const { config } = loaded;
182
193
  const lock = given(env[PROFILE_LOCK]);
183
194
  const named = first([
184
195
  ["first word", flags.profile],
@@ -202,9 +213,6 @@ export const settingsFor = (app, extension = {}) => {
202
213
  [extension.sourcePaths ? "config file: defaults" : "config defaults", shared],
203
214
  ];
204
215
  const fromLayers = (key, fallback, only = layers) => first(only.map(([from, scope]) => [from, scope?.[key]]), fallback);
205
- if (fromLayers("catch_up_marks_read", undefined).value !== undefined) {
206
- throw new CliError("configuration_error", `remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`);
207
- }
208
216
  const kindLayers = layers.filter(([from]) => from.startsWith(`config file: ${kind}.`));
209
217
  const limit = flags.limit === undefined ? fromLayers("limit", DEFAULT_LIMIT) : { value: flags.limit, from: "flag" };
210
218
  const record = flags.record === undefined ? fromLayers("record", false) : { value: flags.record, from: "flag" };
@@ -303,6 +311,8 @@ export const settingsFor = (app, extension = {}) => {
303
311
  },
304
312
  configured,
305
313
  shared,
314
+ configIssues: loaded.issues.map(publicIssue),
315
+ savedPermissions: savedPermissionKeys(config),
306
316
  };
307
317
  // The file was checked by the schema; a flag was not, and `--limit abc` is `NaN` by the time it
308
318
  // gets here, which slices an array to nothing without complaining.
@@ -319,16 +329,52 @@ export const settingsFor = (app, extension = {}) => {
319
329
  const extra = extension.resolve?.({ flags, settings, layers, fromLayers });
320
330
  return { ...settings, ...extra?.values, sources: { ...settings.sources, ...extra?.sources } };
321
331
  };
322
- const configuredProfiles = (options = {}) => namedProfiles(readConfig(configPathFor(options)));
323
- /** `config set` and `config unset`: checked by the same schema the reader uses, so it never writes a file the reader refuses. */
324
- const changeSetting = (path, { profile, setting, value, kind, }) => {
332
+ const configuredProfiles = (options = {}) => {
333
+ const { config, issues } = loadConfig(configPathFor(options));
334
+ if (issues.length > 0 && !options.withIssues)
335
+ throw new CliError("configuration_error", issueMessage(issues.map(publicIssue)));
336
+ return namedProfiles(config);
337
+ };
338
+ /**
339
+ * `config set` and `config unset`, on the file as written, so they also run on a file with problems. A set is
340
+ * refused only for a problem it would add; an unset removes the setting whether or not the schema knows it.
341
+ * `root` reaches a path outside any profile or `defaults`, and only one that has a problem.
342
+ */
343
+ const changeSetting = (path, { profile, setting, value, kind, root = false, }) => {
325
344
  const [name = setting, ...rest] = setting.split(".");
326
345
  const level = name === "permissions" && rest.length > 0 ? rest.join(".") : undefined;
327
346
  const modelField = /^models\.([a-z][a-z0-9-]*)\.(provider|model|base_url)$/.exec(setting);
328
- if (!allSettings.includes(setting) && level === undefined && !modelField) {
329
- const offered = value === undefined ? allSettings : allSettings.filter((one) => one !== "default_profile");
330
- throw new CliError("validation_error", `no setting called "${setting}" — one of: ${offered.join(", ")}`);
347
+ const known = allSettings.includes(setting) || level !== undefined || modelField !== null;
348
+ const read = readRaw(path);
349
+ if ("issue" in read)
350
+ throw new CliError("configuration_error", issueMessage([publicIssue(read.issue)]));
351
+ const raw = read.raw === undefined ? {} : read.raw;
352
+ if (!isScope(raw))
353
+ throw new CliError("configuration_error", `${path}: the file has to be an object, in braces`);
354
+ const scopePath = root
355
+ ? []
356
+ : [...(kind === undefined ? [] : [kind]), ...(profile === undefined ? ["defaults"] : ["profiles", profile])];
357
+ const keyPath = root
358
+ ? setting.split(".")
359
+ : level !== undefined
360
+ ? ["permissions", level]
361
+ : modelField
362
+ ? ["models", modelField[1], modelField[2]]
363
+ : [setting];
364
+ const at = [...scopePath, ...keyPath];
365
+ const before = schemaIssues(raw, readSchema, context(path));
366
+ if (value === undefined) {
367
+ if (root && !before.some((issue) => issue.at.join(".") === at.join(".")))
368
+ throw new CliError("validation_error", `${setting} has no problem to remove — name the profile, or add --defaults, to unset a setting`);
369
+ if (!known && valueAt(raw, at) === undefined)
370
+ throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
371
+ const changed = structuredClone(raw);
372
+ removeAt(changed, at);
373
+ saveConfigFile(path, changed);
374
+ return null;
331
375
  }
376
+ if (!known)
377
+ throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
332
378
  if (profile !== undefined && defaultsOnlyKeys.includes(setting)) {
333
379
  throw new CliError("validation_error", `${setting} is one setting for the whole program, not per profile — add --defaults`);
334
380
  }
@@ -338,98 +384,45 @@ export const settingsFor = (app, extension = {}) => {
338
384
  if (kind !== "bot" && botOnlyKeys.includes(setting)) {
339
385
  throw new CliError("validation_error", `${setting} is a bot's setting — add --bot`);
340
386
  }
341
- const config = readConfig(path);
342
- if (setting === "default_profile") {
343
- if (kind !== undefined) {
344
- throw new CliError("validation_error", "default_profile is one for the whole file — drop --personal and --bot");
345
- }
346
- if (value !== undefined) {
347
- throw new CliError("validation_error", `default_profile is gone: a command names its profile every time — \`${app.command} <profile> …\` or ${PROFILE}`);
348
- }
349
- const { default_profile: _, ...rest } = config;
350
- saveConfigFile(path, rest);
351
- return null;
352
- }
353
- const section = kind === undefined ? undefined : (config[kind] ?? {});
354
- const current = section === undefined
355
- ? profile === undefined
356
- ? config.defaults
357
- : config.profiles[profile]
358
- : profile === undefined
359
- ? section.defaults
360
- : section.profiles?.[profile];
361
- const scope = { ...current };
362
- if (modelField) {
363
- const purpose = modelField[1];
364
- const field = modelField[2];
365
- const models = { ...scope.models };
366
- const target = { ...models[purpose] };
367
- if (value === undefined)
368
- delete target[field];
369
- else
370
- target[field] = parseValue(value);
371
- if (Object.keys(target).length > 0)
372
- models[purpose] = target;
373
- else
374
- delete models[purpose];
375
- if (Object.keys(models).length > 0)
376
- scope.models = models;
377
- else
378
- delete scope.models;
379
- }
380
- else if (level !== undefined) {
381
- const levels = { ...scope.permissions };
382
- if (value === undefined)
383
- delete levels[level];
384
- else
385
- levels[level] = value.trim();
386
- if (Object.keys(levels).length > 0)
387
- scope.permissions = levels;
388
- else
389
- delete scope.permissions;
390
- }
391
- else if (value === undefined)
392
- delete scope[setting];
393
- else
394
- scope[setting] = parseValue(value);
395
- const empty = Object.keys(scope).length === 0;
396
- let changed;
397
- if (kind === undefined || section === undefined) {
398
- changed =
399
- profile === undefined
400
- ? { ...config, defaults: scope }
401
- : { ...config, profiles: { ...config.profiles, [profile]: scope } };
402
- if (profile === undefined && empty)
403
- delete changed.defaults;
404
- if (profile !== undefined && empty)
405
- delete changed.profiles[profile];
406
- }
407
- else {
408
- const next = profile === undefined
409
- ? { ...section, defaults: scope }
410
- : { ...section, profiles: { ...section.profiles, [profile]: scope } };
411
- if (profile === undefined && empty)
412
- delete next.defaults;
413
- if (profile !== undefined && empty && next.profiles)
414
- delete next.profiles[profile];
415
- if (next.profiles && Object.keys(next.profiles).length === 0)
416
- delete next.profiles;
417
- changed = { ...config, [kind]: next };
418
- if (Object.keys(next).length === 0)
419
- delete changed[kind];
420
- }
421
- const checked = v.safeParse(extension.schema ?? schema, changed);
422
- if (!checked.success) {
423
- throw new CliError("validation_error", `${setting} cannot be "${value}": ${checked.issues[0]?.message ?? "invalid"}`);
424
- }
425
- saveConfigFile(path, checked.output);
426
- if (modelField)
427
- return (scope.models?.[modelField[1]]?.[modelField[2]] ??
428
- null);
429
- return level === undefined ? (scope[setting] ?? null) : (scope.permissions?.[level] ?? null);
387
+ const changed = structuredClone(raw);
388
+ setAt(changed, at, level === undefined ? parseValue(value) : value.trim());
389
+ const had = new Set(before.map((issue) => issue.at.join(".")));
390
+ const added = schemaIssues(changed, readSchema, context(path)).find((issue) => !had.has(issue.at.join(".")) || issue.at.join(".") === at.join("."));
391
+ if (added)
392
+ throw new CliError("validation_error", `${setting} cannot be "${value}": ${added.what}`);
393
+ const checked = v.safeParse(readSchema, changed);
394
+ saveConfigFile(path, checked.success ? checked.output : changed);
395
+ return valueAt(checked.success ? checked.output : changed, at) ?? null;
430
396
  };
431
397
  return { resolveSettings, configuredProfiles, changeSetting, allSettings, defaultsOnlyKeys, schema };
432
398
  };
399
+ const isScope = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
400
+ const valueAt = (node, path) => path.reduce((at, key) => (isScope(at) ? at[key] : undefined), node);
401
+ const setAt = (node, path, value) => {
402
+ let at = node;
403
+ for (const key of path.slice(0, -1)) {
404
+ if (!isScope(at[key]))
405
+ at[key] = {};
406
+ at = at[key];
407
+ }
408
+ at[path.at(-1)] = value;
409
+ };
410
+ /** Removes the value and every section it leaves empty, so an unset leaves no `{}` behind. */
411
+ const removeAt = (node, path) => {
412
+ const [key, ...rest] = path;
413
+ if (key === undefined)
414
+ return;
415
+ if (rest.length === 0) {
416
+ delete node[key];
417
+ return;
418
+ }
419
+ const child = node[key];
420
+ if (!isScope(child))
421
+ return;
422
+ removeAt(child, rest);
423
+ if (Object.keys(child).length === 0)
424
+ delete node[key];
425
+ };
433
426
  /** Every profile the file names, in any section. */
434
427
  const namedProfiles = (config) => [
435
428
  ...new Set([