broapp 0.4.6 → 0.4.7

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.
@@ -1,23 +1,45 @@
1
1
  /**
2
2
  * Where the AI layer's non-secret settings live.
3
3
  *
4
- * `<dataDir>/ai/settings.json` holds which provider and model the user chose
5
- * and where to reach them. It never holds the API key that is
6
- * `secrets.ts` and a test asserts the string does not appear in the file,
7
- * because "we do not write it there" is the kind of promise that quietly
8
- * stops being true.
4
+ * `<dataDir>/ai/settings.json` holds which provider is in use, and for every
5
+ * provider the person has set up, where to reach it, which model it runs and
6
+ * whether it is turned on. It never holds an API key that is `secrets.ts` —
7
+ * and a test asserts the string does not appear in the file, because "we do
8
+ * not write it there" is the kind of promise that quietly stops being true.
9
9
  */
10
10
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
 
13
- /** The settings file's shape. `version` exists so a later format can migrate. */
14
- export interface StoredSettings {
15
- version: 1;
16
- provider: string | null;
17
- modelId: string | null;
13
+ /** One provider's own settings, kept whether or not it is the one in use. */
14
+ export interface ProviderEntry {
18
15
  baseUrl: string | null;
19
- /** False means the key is held in memory only and forgotten on exit. */
16
+ modelId: string | null;
17
+ /**
18
+ * Whether this provider may be sent anything. The provider in use always
19
+ * is; any other only when a person turned it on, so a model reference in a
20
+ * file cannot send their work somewhere they never chose.
21
+ */
22
+ enabled: boolean;
23
+ }
24
+
25
+ /**
26
+ * The settings file's shape.
27
+ *
28
+ * Version 1 held one `provider`, `modelId` and `baseUrl`, so changing provider
29
+ * lost the other's address and model. Version 2 keeps an entry per provider;
30
+ * a version-1 file is read as version 2 and left as it is until the next write.
31
+ */
32
+ export interface StoredSettings {
33
+ version: 2;
34
+ /** The provider a turn runs on when nothing names another. */
35
+ active: string | null;
36
+ /** False means every key is held in memory only and forgotten on exit. */
20
37
  remember: boolean;
38
+ /**
39
+ * Keyed by provider id. An entry for an id this build has no adapter for is
40
+ * kept on write and ignored on read: another build may own it.
41
+ */
42
+ providers: Record<string, ProviderEntry>;
21
43
  }
22
44
 
23
45
  /** Reads and writes {@link StoredSettings}. */
@@ -28,22 +50,49 @@ export interface SettingsStore {
28
50
 
29
51
  /** What a fresh installation has. No provider, so the layer is off. */
30
52
  export function defaultSettings(): StoredSettings {
31
- return { version: 1, provider: null, modelId: null, baseUrl: null, remember: true };
53
+ return { version: 2, active: null, remember: true, providers: {} };
32
54
  }
33
55
 
34
- function coerce(value: unknown): StoredSettings | null {
35
- if (typeof value !== 'object' || value === null) return null;
36
- const raw = value as Record<string, unknown>;
37
- const text = (key: string): string | null => (typeof raw[key] === 'string' ? (raw[key] as string) : null);
56
+ function text(raw: Record<string, unknown>, key: string): string | null {
57
+ const value = raw[key];
58
+ return typeof value === 'string' ? value : null;
59
+ }
60
+
61
+ /** A version-1 file, as version 2: the one provider it named, turned on. */
62
+ function fromVersion1(raw: Record<string, unknown>): StoredSettings {
63
+ const active = text(raw, 'provider');
38
64
  return {
39
- version: 1,
40
- provider: text('provider'),
41
- modelId: text('modelId'),
42
- baseUrl: text('baseUrl'),
65
+ version: 2,
66
+ active,
43
67
  remember: raw['remember'] !== false,
68
+ providers:
69
+ active === null
70
+ ? {}
71
+ : { [active]: { baseUrl: text(raw, 'baseUrl'), modelId: text(raw, 'modelId'), enabled: true } },
44
72
  };
45
73
  }
46
74
 
75
+ function coerce(value: unknown): StoredSettings | null {
76
+ if (typeof value !== 'object' || value === null) return null;
77
+ const raw = value as Record<string, unknown>;
78
+ // A file written before `version` existed, or by version 1, has the old shape.
79
+ if (raw['version'] === undefined || raw['version'] === 1) return fromVersion1(raw);
80
+ if (raw['version'] !== 2) return null;
81
+ const listed = raw['providers'];
82
+ if (typeof listed !== 'object' || listed === null || Array.isArray(listed)) return null;
83
+ const providers: Record<string, ProviderEntry> = {};
84
+ for (const [id, entry] of Object.entries(listed as Record<string, unknown>)) {
85
+ if (typeof entry !== 'object' || entry === null) continue;
86
+ const fields = entry as Record<string, unknown>;
87
+ providers[id] = {
88
+ baseUrl: text(fields, 'baseUrl'),
89
+ modelId: text(fields, 'modelId'),
90
+ enabled: fields['enabled'] === true,
91
+ };
92
+ }
93
+ return { version: 2, active: text(raw, 'active'), remember: raw['remember'] !== false, providers };
94
+ }
95
+
47
96
  /** Open the settings store for one data directory. */
48
97
  export function createSettingsStore(dataDir: string): SettingsStore {
49
98
  const directory = join(dataDir, 'ai');