@aragon-agent/cli 0.6.34 → 0.6.35

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 (64) hide show
  1. package/README.md +3071 -3007
  2. package/dist/agent/controller.d.ts +29 -0
  3. package/dist/agent/controller.js +42 -1
  4. package/dist/agent/kit-apply.d.ts +56 -0
  5. package/dist/agent/kit-apply.js +209 -0
  6. package/dist/agent/model-profile-settings.d.ts +28 -0
  7. package/dist/agent/model-profile-settings.js +142 -29
  8. package/dist/agent/reducer.d.ts +1 -1
  9. package/dist/commands/builtins.js +4 -0
  10. package/dist/commands/kit-command.d.ts +20 -0
  11. package/dist/commands/kit-command.js +131 -0
  12. package/dist/config/kit-resolution.d.ts +93 -0
  13. package/dist/config/kit-resolution.js +226 -0
  14. package/dist/config/model-kits.d.ts +224 -0
  15. package/dist/config/model-kits.js +302 -0
  16. package/dist/config/model-profile-store.d.ts +25 -0
  17. package/dist/config/model-profiles.d.ts +41 -3
  18. package/dist/config/model-profiles.js +46 -5
  19. package/dist/config/schema.d.ts +16 -0
  20. package/dist/input/keymap.js +18 -4
  21. package/dist/ui/App.js +108 -12
  22. package/dist/ui/Header.d.ts +9 -1
  23. package/dist/ui/Header.js +8 -2
  24. package/dist/ui/PromptInput.js +1 -1
  25. package/dist/ui/ScrollbackShell.d.ts +18 -3
  26. package/dist/ui/ScrollbackShell.js +1 -1
  27. package/dist/ui/StatusBar.d.ts +13 -0
  28. package/dist/ui/StatusBar.js +10 -3
  29. package/dist/ui/TeamPanel.d.ts +12 -0
  30. package/dist/ui/TeamPanel.js +10 -31
  31. package/dist/ui/Transcript.js +22 -2
  32. package/dist/ui/entries/TeamCard.d.ts +18 -2
  33. package/dist/ui/entries/TeamCard.js +47 -13
  34. package/dist/ui/layout/header-hints.d.ts +11 -1
  35. package/dist/ui/layout/header-hints.js +35 -3
  36. package/dist/ui/layout/kit-chip.d.ts +50 -0
  37. package/dist/ui/layout/kit-chip.js +81 -0
  38. package/dist/ui/layout/virtual-window.js +9 -0
  39. package/dist/ui/overlays/HelpOverlay.js +7 -0
  40. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  41. package/dist/ui/overlays/KitEditor.js +101 -0
  42. package/dist/ui/overlays/KitManager.d.ts +23 -0
  43. package/dist/ui/overlays/KitManager.js +104 -0
  44. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  45. package/dist/ui/overlays/KitPicker.js +58 -0
  46. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  47. package/dist/ui/overlays/SettingsScreen.js +139 -16
  48. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  49. package/dist/ui/overlays/kit-picker-state.js +136 -0
  50. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  51. package/dist/ui/overlays/kit-settings-state.js +267 -0
  52. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  53. package/dist/ui/overlays/model-profile-state.js +59 -6
  54. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  55. package/dist/ui/scrollback/LiveTail.js +7 -2
  56. package/dist/ui/scrollback/live-preview.js +10 -3
  57. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  58. package/dist/ui/scrollback/tail-window.js +17 -2
  59. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  60. package/dist/ui/team-status.d.ts +141 -0
  61. package/dist/ui/team-status.js +224 -0
  62. package/dist/ui/transcript-text.js +13 -4
  63. package/dist/ui/use-wheel-routing.js +2 -2
  64. package/package.json +2 -2
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Agent model kits: a named preset that pins the THREE model roles
3
+ * (main / fast / subagent) to profiles that already live in the library
4
+ * (agent-model-kits D-1 / D-2).
5
+ *
6
+ * ASCII ONLY - `src/config/**` is inside `glyphs.test.ts::inScope`.
7
+ *
8
+ * A KIT STORES REFERENCES, NEVER COPIES (D-1): every slot is a profile id, so
9
+ * editing the profile changes the meaning of every kit that names it, which is
10
+ * exactly the promise a "preset" makes. That is also why deleting a profile has
11
+ * to delete the kits that reference it (D-9) instead of leaving a preset nobody
12
+ * can repair from the UI.
13
+ *
14
+ * EVERY CROSS-LAYER IMPORT HERE IS `import type` (RV-P2-5): `schema.ts` names
15
+ * `model-profiles.ts` through an inline `import('./model-profiles.js')` type
16
+ * query, so `model-profiles -> model-kits -> schema` is already a TYPE cycle. A
17
+ * value import (`DEFAULT_FAST_CONFIG`, `clampTeamConfig`) would turn it into a
18
+ * runtime cycle, and vitest surfaces that as an empty module at import time.
19
+ */
20
+ import type { CliConfig, FastConfig, TeamConfig } from './schema.js';
21
+ import type { ModelProfilesConfig, ProfileValidationIssue } from './model-profiles.js';
22
+ import type { ModelSettingsSaveResult } from './model-profile-store.js';
23
+ export type KitSlot = 'main' | 'fast' | 'subagent';
24
+ /**
25
+ * The session kit's runtime shape (D-5): the kit plus the slots it still owns.
26
+ *
27
+ * `held` is the ONLY mutable part. It shrinks when a save explicitly decides a
28
+ * slot (§4.3 / D-6), so "the user changed subagent, so subagent is theirs now"
29
+ * needs no second bookkeeping structure.
30
+ */
31
+ export interface SessionKitState {
32
+ id: string;
33
+ held: KitSlot[];
34
+ }
35
+ /**
36
+ * The kit's subagent slot.
37
+ *
38
+ * `model` is typed with `TeamConfig['model']` rather than by importing
39
+ * `team/model.ts`: `team/model.ts` already imports `config/`, so the reverse
40
+ * edge would be a cycle. `import type` keeps even this one erased.
41
+ */
42
+ export interface AgentKitSubagent {
43
+ model: TeamConfig['model'];
44
+ /** Non-null IF AND ONLY IF `model === 'profile'` (I-2, guaranteed by normalize). */
45
+ modelProfileId: string | null;
46
+ }
47
+ export interface AgentKit {
48
+ id: string;
49
+ /** 1..48 code points, no control characters. Names MAY repeat (§6.6). */
50
+ name: string;
51
+ /** Required (D-8). A dangling reference is NOT a validation error - see I-8. */
52
+ mainProfileId: string;
53
+ /** `null` = this kit turns the fast tier OFF for its session (D-8). */
54
+ fastProfileId: string | null;
55
+ subagent: AgentKitSubagent;
56
+ }
57
+ export declare const KIT_LIMITS: {
58
+ /**
59
+ * Structural ceiling. The EFFECTIVE ceiling is lower and implicit: every kit
60
+ * references at least one profile, so 200 entries cap the reachable kits
61
+ * (RV-P2-7) - recorded, not cross-validated, because the two numbers answer
62
+ * different questions.
63
+ */
64
+ readonly maxKits: 50;
65
+ readonly nameMaxChars: 48;
66
+ readonly maxSlots: 3;
67
+ };
68
+ /**
69
+ * STRUCTURAL ONLY (RV-P1-1). Reference existence is a RESOLUTION-time concern,
70
+ * exactly as it is for `team.modelProfileId` (`schema.ts` clampTeamConfig):
71
+ * `createModelProfileState` blanks the whole settings page when validation
72
+ * fails, so promoting "a hand-edited file left one dangling id" into "the page
73
+ * and `/team model` are both dead" is a wildly disproportionate blast radius.
74
+ *
75
+ * `duplicate_name` is deliberately NOT a code: two kits may share a name
76
+ * (§6.6), and `/kit use` disambiguates with an `#id` prefix.
77
+ */
78
+ export interface KitIssue {
79
+ path: string;
80
+ code: 'invalid' | 'duplicate_id';
81
+ message: string;
82
+ }
83
+ /** One resolved slot; `null` on the slot itself means the entry is gone. */
84
+ export interface KitConnection {
85
+ provider: string;
86
+ model: string;
87
+ baseUrl: string | null;
88
+ name: string;
89
+ }
90
+ export interface KitSlotConnections {
91
+ /** `null` = the referenced entry is gone; render `Profile missing`, never throw. */
92
+ main: KitConnection | null;
93
+ fast: KitConnection | null;
94
+ subagent: {
95
+ /**
96
+ * The SLOT MODE (`main` / `fast` / `profile`). Named `mode` rather than
97
+ * `model` because the resolved connection below carries a model ID of its
98
+ * own, and two `model` keys in one object literal is a TypeScript error
99
+ * (recorded as an implementation-time fix in the spec's issues section).
100
+ */
101
+ mode: TeamConfig['model'];
102
+ provider: string | null;
103
+ model: string | null;
104
+ baseUrl: string | null;
105
+ name: string | null;
106
+ };
107
+ }
108
+ /**
109
+ * A consequence of applying a kit that the user MUST be told about (§4.2).
110
+ *
111
+ * Not one of these is a failure: applying a kit is allowed to turn the fast tier
112
+ * on (which is real money - `DEFAULT_FAST_CONFIG.review` is `true`), to leave a
113
+ * profile without a usable key, or to name a tier this kit just switched off.
114
+ * The rule is the one every other subsystem in this package follows: no silent
115
+ * decisions, especially about spend.
116
+ */
117
+ export type KitNote = 'fast_tier_enabled' | 'fast_review_enabled' | 'fast_tier_off' | 'restart_required' | 'main_key_missing' | 'subagent_key_missing' | 'subagent_fast_unavailable';
118
+ /**
119
+ * What a session-kit action did, or why it did nothing.
120
+ *
121
+ * THE CODE SET IS THE SAVE SET PLUS TWO (RV-P1-3): every code the reused
122
+ * prepare/commit chain can produce is spelled here, so `kitApplyMessage` has no
123
+ * gap to fall through. A missing message would be rendered by a non-null
124
+ * assertion as the literal string `undefined`.
125
+ */
126
+ export interface KitApplyResult {
127
+ ok: boolean;
128
+ code?: ModelSettingsSaveResult['code'] | 'kit_missing' | 'kit_references_missing_profile';
129
+ /** `clearSessionKit` only: the session is back on the config file. */
130
+ released?: boolean;
131
+ notes: KitNote[];
132
+ /** Set when `kit_references_missing_profile` refused the apply (I-8). */
133
+ issues?: ProfileValidationIssue[];
134
+ }
135
+ export interface KitsNormalization {
136
+ kits: AgentKit[];
137
+ /** Self-healed: a pointer at a kit that did not survive becomes `null`. */
138
+ defaultKitId: string | null;
139
+ /** Forward migration marker (D-3). Never anything but 1. */
140
+ kitsVersion: 1;
141
+ /** Entries thrown away because they were structurally unsalvageable. */
142
+ dropped: number;
143
+ }
144
+ export declare const SUBAGENT_MODES: readonly ["main", "fast", "profile"];
145
+ /** The one mapping from a kit to a persisted-config patch (§4.2). */
146
+ export interface KitCandidate {
147
+ modelProfiles: ModelProfilesConfig;
148
+ team: Partial<TeamConfig>;
149
+ fast: Partial<FastConfig>;
150
+ }
151
+ /**
152
+ * Validate the `kits` array STRUCTURALLY (RV-P1-1).
153
+ *
154
+ * Returns `[]` for `undefined`/`null`/`[]` so a library written by a pre-KIT
155
+ * build keeps validating - the whole point of D-3's additive migration.
156
+ */
157
+ export declare function validateKits(value: unknown): KitIssue[];
158
+ /**
159
+ * White-list normalization of `kits` + `defaultKitId` (F-3 / R-2).
160
+ *
161
+ * TOTAL: no input throws. `normalizeModelProfiles` calls this on whatever the
162
+ * file holds, and a throw there would take the whole library - including the
163
+ * user's credentials - down with one malformed kit.
164
+ *
165
+ * `defaultKitRef` is a SEPARATE argument because `defaultKitId` is a sibling of
166
+ * `kits` in the library, not a member of the array; keeping it here is what
167
+ * lets the pointer self-heal against the kits that actually survived.
168
+ */
169
+ export declare function normalizeKits(kitsValue: unknown, defaultKitRef?: unknown): KitsNormalization;
170
+ /** Create a kit with a stable UUID, normalized like `createModelProfile`. */
171
+ export declare function createAgentKit(input: Omit<AgentKit, 'id'>): AgentKit;
172
+ /** Copy a kit under a free ` (copy)` name - the manager's `Duplicate` action. */
173
+ export declare function duplicateAgentKit(kit: AgentKit, existing: readonly AgentKit[]): AgentKit;
174
+ /**
175
+ * Every kit a reference text may mean.
176
+ *
177
+ * ONE parser for `ctrl+K`, `/kit use` and the settings manager, so "what did I
178
+ * just type" has a single answer. `ref` is an exact id, an `#id` prefix, or an
179
+ * exact name; an ambiguous reference returns more than one hit and the caller
180
+ * reports it rather than silently taking the first (§4.9).
181
+ */
182
+ export declare function kitMatches(library: ModelProfilesConfig | undefined, ref: string | null | undefined): AgentKit[];
183
+ /** Resolve one kit, or `undefined` when the reference is missing or ambiguous. */
184
+ export declare function findKit(library: ModelProfilesConfig | undefined, ref: string | null | undefined): AgentKit | undefined;
185
+ /**
186
+ * THE ONE mapping from a kit to a persisted-config patch (§4.2).
187
+ *
188
+ * Both consumption paths - "set as default" (writes the result) and `ctrl+K`
189
+ * (never writes it) - call this, so a default kit and a session kit produce
190
+ * structurally identical configurations. There is no second interpretation.
191
+ *
192
+ * `defaultKitId` is only touched when the caller PASSES it (it is
193
+ * `undefined` for a session kit), which is what keeps `ctrl+K` from moving the
194
+ * persisted pointer.
195
+ */
196
+ export declare function kitCandidate(input: {
197
+ library: ModelProfilesConfig;
198
+ kit: AgentKit;
199
+ defaultKitId?: string | null;
200
+ }): KitCandidate;
201
+ /**
202
+ * Dereference the three slots for every surface that prints them (picker,
203
+ * management page, `/kit list`).
204
+ *
205
+ * A MISSING ENTRY RETURNS `null` INSTEAD OF THROWING (RV-P1-1 / I-8): the UI
206
+ * renders `Profile missing`, the library stays readable, and only APPLYING the
207
+ * kit is refused, with a stable code.
208
+ */
209
+ export declare function kitSlotConnections(library: ModelProfilesConfig | undefined, kit: AgentKit): KitSlotConnections;
210
+ /**
211
+ * Pre-fill the first kit from what this machine already runs (§4.10 / D-13).
212
+ *
213
+ * `mainProfileId` comes from the caller because CREATING a profile is UI work:
214
+ * `createCustomProfile` lives in `ui/overlays/model-profile-state.ts`, and a
215
+ * `config/` module importing `ui/` would invert the dependency direction this
216
+ * package keeps everywhere else. The caller creates (or reuses) the entry and
217
+ * hands its id in; when it is absent the kit is still built, because a
218
+ * half-filled editor is a better first form than an empty one.
219
+ */
220
+ export declare function createKitFromCurrent(input: {
221
+ config: CliConfig;
222
+ mainProfileId?: string | null;
223
+ name?: string;
224
+ }): AgentKit;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Agent model kits: a named preset that pins the THREE model roles
3
+ * (main / fast / subagent) to profiles that already live in the library
4
+ * (agent-model-kits D-1 / D-2).
5
+ *
6
+ * ASCII ONLY - `src/config/**` is inside `glyphs.test.ts::inScope`.
7
+ *
8
+ * A KIT STORES REFERENCES, NEVER COPIES (D-1): every slot is a profile id, so
9
+ * editing the profile changes the meaning of every kit that names it, which is
10
+ * exactly the promise a "preset" makes. That is also why deleting a profile has
11
+ * to delete the kits that reference it (D-9) instead of leaving a preset nobody
12
+ * can repair from the UI.
13
+ *
14
+ * EVERY CROSS-LAYER IMPORT HERE IS `import type` (RV-P2-5): `schema.ts` names
15
+ * `model-profiles.ts` through an inline `import('./model-profiles.js')` type
16
+ * query, so `model-profiles -> model-kits -> schema` is already a TYPE cycle. A
17
+ * value import (`DEFAULT_FAST_CONFIG`, `clampTeamConfig`) would turn it into a
18
+ * runtime cycle, and vitest surfaces that as an empty module at import time.
19
+ */
20
+ import { randomUUID } from 'node:crypto';
21
+ export const KIT_LIMITS = {
22
+ /**
23
+ * Structural ceiling. The EFFECTIVE ceiling is lower and implicit: every kit
24
+ * references at least one profile, so 200 entries cap the reachable kits
25
+ * (RV-P2-7) - recorded, not cross-validated, because the two numbers answer
26
+ * different questions.
27
+ */
28
+ maxKits: 50,
29
+ nameMaxChars: 48,
30
+ maxSlots: 3,
31
+ };
32
+ export const SUBAGENT_MODES = ['main', 'fast', 'profile'];
33
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
34
+ function isRecord(value) {
35
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
36
+ }
37
+ /** Non-empty, control-free text of at most `max` code points. */
38
+ function validText(value, max) {
39
+ if (typeof value !== 'string' || CONTROL.test(value))
40
+ return false;
41
+ const points = [...value.trim()].length;
42
+ return points > 0 && points <= max;
43
+ }
44
+ function invalidKit(path, message) {
45
+ return { path, code: 'invalid', message };
46
+ }
47
+ function validateKitSubagent(value, path) {
48
+ if (!isRecord(value))
49
+ return [invalidKit(`${path}.subagent`, 'Subagent must be an object.')];
50
+ const issues = [];
51
+ if (!SUBAGENT_MODES.includes(value.model)) {
52
+ issues.push(invalidKit(`${path}.subagent.model`, 'Subagent model must be main, fast or profile.'));
53
+ }
54
+ const id = value.modelProfileId;
55
+ if (id !== null && !validText(id, 128)) {
56
+ issues.push(invalidKit(`${path}.subagent.modelProfileId`, 'Subagent profile must be empty or 1-128 safe characters.'));
57
+ }
58
+ return issues;
59
+ }
60
+ function validateKit(value, index) {
61
+ const path = `modelProfiles.kits[${index}]`;
62
+ if (!isRecord(value))
63
+ return [invalidKit(path, 'Kit must be an object.')];
64
+ const issues = [];
65
+ if (!validText(value.id, 128)) {
66
+ issues.push(invalidKit(`${path}.id`, 'Kit ID must have 1-128 safe characters.'));
67
+ }
68
+ if (!validText(value.name, KIT_LIMITS.nameMaxChars)) {
69
+ issues.push(invalidKit(`${path}.name`, `Kit name must have 1-${KIT_LIMITS.nameMaxChars} safe characters.`));
70
+ }
71
+ if (!validText(value.mainProfileId, 128)) {
72
+ issues.push(invalidKit(`${path}.mainProfileId`, 'Choose a main profile for this kit.'));
73
+ }
74
+ const fast = value.fastProfileId;
75
+ if (fast !== null && !validText(fast, 128)) {
76
+ issues.push(invalidKit(`${path}.fastProfileId`, 'Fast profile must be a profile or off.'));
77
+ }
78
+ issues.push(...validateKitSubagent(value.subagent, path));
79
+ return issues;
80
+ }
81
+ /**
82
+ * Validate the `kits` array STRUCTURALLY (RV-P1-1).
83
+ *
84
+ * Returns `[]` for `undefined`/`null`/`[]` so a library written by a pre-KIT
85
+ * build keeps validating - the whole point of D-3's additive migration.
86
+ */
87
+ export function validateKits(value) {
88
+ if (value === undefined || value === null)
89
+ return [];
90
+ if (!Array.isArray(value)) {
91
+ return [invalidKit('modelProfiles.kits', 'Kits must be an array.')];
92
+ }
93
+ if (value.length > KIT_LIMITS.maxKits) {
94
+ return [invalidKit('modelProfiles.kits', `Use at most ${KIT_LIMITS.maxKits} kits; delete unused kits to add more.`)];
95
+ }
96
+ const issues = value.flatMap(validateKit);
97
+ const ids = new Set();
98
+ value.forEach((kit, index) => {
99
+ if (!isRecord(kit) || typeof kit.id !== 'string')
100
+ return;
101
+ if (ids.has(kit.id)) {
102
+ issues.push({ path: `modelProfiles.kits[${index}].id`, code: 'duplicate_id',
103
+ message: 'Kit IDs must be unique.' });
104
+ }
105
+ ids.add(kit.id);
106
+ });
107
+ return issues;
108
+ }
109
+ function normalizeSubagent(value) {
110
+ const source = isRecord(value) ? value : {};
111
+ const model = SUBAGENT_MODES.includes(source.model)
112
+ ? source.model : 'main';
113
+ // I-2: the id exists exactly when the mode names a profile. A hand-edited
114
+ // `{model:'fast', modelProfileId:'x'}` therefore heals instead of lingering
115
+ // as a reference nothing reads.
116
+ const id = model === 'profile' && validText(source.modelProfileId, 128)
117
+ ? source.modelProfileId.trim() : null;
118
+ return { model, modelProfileId: id };
119
+ }
120
+ /** One entry into a safe `AgentKit`, or `null` when it cannot be salvaged. */
121
+ function normalizeKit(value) {
122
+ if (!isRecord(value))
123
+ return null;
124
+ if (!validText(value.id, 128) || !validText(value.name, KIT_LIMITS.nameMaxChars)
125
+ || !validText(value.mainProfileId, 128))
126
+ return null;
127
+ const fast = value.fastProfileId;
128
+ if (fast !== null && fast !== undefined && !validText(fast, 128))
129
+ return null;
130
+ return {
131
+ id: value.id.trim(),
132
+ name: value.name.trim(),
133
+ mainProfileId: value.mainProfileId.trim(),
134
+ fastProfileId: validText(fast, 128) ? fast.trim() : null,
135
+ subagent: normalizeSubagent(value.subagent),
136
+ };
137
+ }
138
+ /**
139
+ * White-list normalization of `kits` + `defaultKitId` (F-3 / R-2).
140
+ *
141
+ * TOTAL: no input throws. `normalizeModelProfiles` calls this on whatever the
142
+ * file holds, and a throw there would take the whole library - including the
143
+ * user's credentials - down with one malformed kit.
144
+ *
145
+ * `defaultKitRef` is a SEPARATE argument because `defaultKitId` is a sibling of
146
+ * `kits` in the library, not a member of the array; keeping it here is what
147
+ * lets the pointer self-heal against the kits that actually survived.
148
+ */
149
+ export function normalizeKits(kitsValue, defaultKitRef) {
150
+ const kits = [];
151
+ let dropped = 0;
152
+ if (Array.isArray(kitsValue)) {
153
+ const seen = new Set();
154
+ for (const raw of kitsValue.slice(0, KIT_LIMITS.maxKits)) {
155
+ const kit = normalizeKit(raw);
156
+ if (!kit || seen.has(kit.id)) {
157
+ dropped += 1;
158
+ continue;
159
+ }
160
+ seen.add(kit.id);
161
+ kits.push(kit);
162
+ }
163
+ dropped += Math.max(0, kitsValue.length - KIT_LIMITS.maxKits);
164
+ }
165
+ else if (kitsValue !== undefined && kitsValue !== null)
166
+ dropped += 1;
167
+ const defaultKitId = typeof defaultKitRef === 'string'
168
+ && kits.some((kit) => kit.id === defaultKitRef) ? defaultKitRef : null;
169
+ return { kits, defaultKitId, kitsVersion: 1, dropped };
170
+ }
171
+ /** Create a kit with a stable UUID, normalized like `createModelProfile`. */
172
+ export function createAgentKit(input) {
173
+ const draft = { ...input, id: randomUUID() };
174
+ return normalizeKit(draft) ?? { ...draft, subagent: normalizeSubagent(draft.subagent) };
175
+ }
176
+ /** Copy a kit under a free ` (copy)` name - the manager's `Duplicate` action. */
177
+ export function duplicateAgentKit(kit, existing) {
178
+ const taken = new Set(existing.map((other) => other.name));
179
+ const suffix = [...kit.name].slice(0, KIT_LIMITS.nameMaxChars - 8).join('').trimEnd();
180
+ let name = `${suffix} (copy)`;
181
+ for (let copy = 2; taken.has(name); copy += 1)
182
+ name = `${suffix} (copy ${copy})`;
183
+ return { ...kit, id: randomUUID(), name: [...name].slice(0, KIT_LIMITS.nameMaxChars).join('') };
184
+ }
185
+ /**
186
+ * Every kit a reference text may mean.
187
+ *
188
+ * ONE parser for `ctrl+K`, `/kit use` and the settings manager, so "what did I
189
+ * just type" has a single answer. `ref` is an exact id, an `#id` prefix, or an
190
+ * exact name; an ambiguous reference returns more than one hit and the caller
191
+ * reports it rather than silently taking the first (§4.9).
192
+ */
193
+ export function kitMatches(library, ref) {
194
+ if (!library || !ref)
195
+ return [];
196
+ const kits = library.kits ?? [];
197
+ const text = ref.trim();
198
+ if (!text)
199
+ return [];
200
+ const exact = kits.find((kit) => kit.id === text);
201
+ if (exact)
202
+ return [exact];
203
+ if (text.startsWith('#')) {
204
+ const prefix = text.slice(1).trim().toLowerCase();
205
+ return prefix ? kits.filter((kit) => kit.id.toLowerCase().startsWith(prefix)) : [];
206
+ }
207
+ return kits.filter((kit) => kit.name === text);
208
+ }
209
+ /** Resolve one kit, or `undefined` when the reference is missing or ambiguous. */
210
+ export function findKit(library, ref) {
211
+ const hits = kitMatches(library, ref);
212
+ return hits.length === 1 ? hits[0] : undefined;
213
+ }
214
+ /**
215
+ * THE ONE mapping from a kit to a persisted-config patch (§4.2).
216
+ *
217
+ * Both consumption paths - "set as default" (writes the result) and `ctrl+K`
218
+ * (never writes it) - call this, so a default kit and a session kit produce
219
+ * structurally identical configurations. There is no second interpretation.
220
+ *
221
+ * `defaultKitId` is only touched when the caller PASSES it (it is
222
+ * `undefined` for a session kit), which is what keeps `ctrl+K` from moving the
223
+ * persisted pointer.
224
+ */
225
+ export function kitCandidate(input) {
226
+ const { library, kit } = input;
227
+ const profileId = kit.subagent.model === 'profile' ? kit.subagent.modelProfileId : null;
228
+ return {
229
+ modelProfiles: {
230
+ ...library,
231
+ mainId: kit.mainProfileId,
232
+ fastId: kit.fastProfileId,
233
+ ...(input.defaultKitId !== undefined ? { defaultKitId: input.defaultKitId } : {}),
234
+ },
235
+ team: { model: kit.subagent.model, modelProfileId: profileId },
236
+ // D-8: `fastProfileId: null` means OFF, not "leave it alone".
237
+ fast: { enabled: kit.fastProfileId !== null },
238
+ };
239
+ }
240
+ function connectionOf(entry) {
241
+ if (!entry)
242
+ return null;
243
+ return { provider: entry.provider, model: entry.model, baseUrl: entry.baseUrl,
244
+ name: entry.name };
245
+ }
246
+ /**
247
+ * Dereference the three slots for every surface that prints them (picker,
248
+ * management page, `/kit list`).
249
+ *
250
+ * A MISSING ENTRY RETURNS `null` INSTEAD OF THROWING (RV-P1-1 / I-8): the UI
251
+ * renders `Profile missing`, the library stays readable, and only APPLYING the
252
+ * kit is refused, with a stable code.
253
+ */
254
+ export function kitSlotConnections(library, kit) {
255
+ const entries = library?.entries ?? [];
256
+ const find = (id) => id ? entries.find((entry) => entry.id === id) : undefined;
257
+ const main = find(kit.mainProfileId);
258
+ const fast = find(kit.fastProfileId);
259
+ const subProfile = kit.subagent.model === 'profile'
260
+ ? find(kit.subagent.modelProfileId) : undefined;
261
+ const target = kit.subagent.model === 'main' ? main
262
+ : kit.subagent.model === 'fast' ? fast : subProfile;
263
+ return {
264
+ main: connectionOf(main),
265
+ fast: connectionOf(fast),
266
+ subagent: {
267
+ mode: kit.subagent.model,
268
+ provider: target?.provider ?? null,
269
+ model: target?.model ?? null,
270
+ baseUrl: target?.baseUrl ?? null,
271
+ name: target?.name ?? null,
272
+ },
273
+ };
274
+ }
275
+ /**
276
+ * Pre-fill the first kit from what this machine already runs (§4.10 / D-13).
277
+ *
278
+ * `mainProfileId` comes from the caller because CREATING a profile is UI work:
279
+ * `createCustomProfile` lives in `ui/overlays/model-profile-state.ts`, and a
280
+ * `config/` module importing `ui/` would invert the dependency direction this
281
+ * package keeps everywhere else. The caller creates (or reuses) the entry and
282
+ * hands its id in; when it is absent the kit is still built, because a
283
+ * half-filled editor is a better first form than an empty one.
284
+ */
285
+ export function createKitFromCurrent(input) {
286
+ const { config } = input;
287
+ const library = config.modelProfiles;
288
+ const fastId = config.fast?.enabled === true ? library?.fastId ?? null : null;
289
+ // THE SUBAGENT PROFILE IS CARRIED, NOT BLANKED (I-2): a machine whose subagent
290
+ // already runs a specific profile would otherwise be prefilled with mode
291
+ // `profile` and no id, which `resolveSubagentModel` can only degrade to main -
292
+ // the prefill would silently change the answer it exists to record.
293
+ const subagentModel = config.team?.model ?? 'main';
294
+ const subagentProfileId = subagentModel === 'profile'
295
+ ? config.team?.modelProfileId ?? null : null;
296
+ return createAgentKit({
297
+ name: input.name ?? 'My setup',
298
+ mainProfileId: input.mainProfileId ?? library?.mainId ?? '',
299
+ fastProfileId: fastId,
300
+ subagent: { model: subagentModel, modelProfileId: subagentProfileId },
301
+ });
302
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ModelProfilesConfig, type ModelRole, type ProfileValidationIssue } from './model-profiles.js';
2
+ import type { SessionKitState } from './model-kits.js';
2
3
  import type { CliConfig, CompactionConfig, FastConfig, LogConfig, PersistedConfig, TeamConfig } from './schema.js';
3
4
  export interface ModelSettingsPatch extends Partial<Omit<PersistedConfig, 'modelProfiles' | 'fast' | 'compaction' | 'log' | 'team'>> {
4
5
  fast?: Partial<FastConfig>;
@@ -16,6 +17,30 @@ export interface ModelSettingsDraft {
16
17
  baseline: PersistedConfig;
17
18
  liveBaseline: CliConfig;
18
19
  readError?: string;
20
+ /**
21
+ * The session kit this transaction carries (agent-model-kits RV-P0-2 / §4.3).
22
+ *
23
+ * THREE-STATE, and every state is reachable: `undefined` = "this draft does
24
+ * not speak about kits" (a legacy caller, `/model`, `config set`);
25
+ * an object = "the session kit is still in effect and these slots are still
26
+ * its"; `null` = "this transaction explicitly RELEASES the session kit"
27
+ * (`Current setup`, `/kit use current`, or a save that decided all three
28
+ * slots). `createModelSettingsDraft` seeds it from the LIVE config, without
29
+ * which the object state would be unreachable and any save made from the
30
+ * settings screen would silently release the kit.
31
+ */
32
+ sessionKit?: SessionKitState | null;
33
+ /**
34
+ * Set ONLY by `applySessionKit` (agent-model-kits §4.2 exit B).
35
+ *
36
+ * IT MEANS "THIS DRAFT IS THE KIT BEING APPLIED". The release rule
37
+ * (`releasedSlots`) reads `activateRoles` / `patch.team` / binding changes as
38
+ * "the user decided that slot", and an apply writes all three of those by
39
+ * definition - so without this marker every apply would immediately release the
40
+ * slots it just installed. The marker is per-transaction state, never persisted,
41
+ * and a save from the settings screen never sets it.
42
+ */
43
+ sessionKitFresh?: boolean;
19
44
  }
20
45
  export interface ModelSettingsSaveResult {
21
46
  ok: boolean;
@@ -1,3 +1,4 @@
1
+ import { type AgentKit } from './model-kits.js';
1
2
  export type ModelRole = 'main' | 'fast';
2
3
  export interface ModelProfile {
3
4
  id: string;
@@ -14,6 +15,16 @@ export interface ModelProfilesConfig {
14
15
  entries: ModelProfile[];
15
16
  mainId: string | null;
16
17
  fastId: string | null;
18
+ /**
19
+ * Agent model kits (agent-model-kits D-2). OPTIONAL because a file written by
20
+ * a pre-KIT build simply has no such key; `emptyModelProfiles` still fills it
21
+ * in, so every library this build CREATES is explicit.
22
+ */
23
+ kits?: AgentKit[];
24
+ /** The `Set default` pointer (D-13); a dangling value normalizes to null. */
25
+ defaultKitId?: string | null;
26
+ /** Forward migration marker (D-3). Always 1; never a functional decision. */
27
+ kitsVersion?: 1;
17
28
  }
18
29
  export interface ProfileBindingState {
19
30
  selectedId: string | null;
@@ -42,7 +53,16 @@ export declare function emptyModelProfileState(invalid?: boolean): ModelProfiles
42
53
  * copy of these four clauses is one edit away from disagreeing with the config layer.
43
54
  */
44
55
  export declare function isValidEndpointUrl(value: string | null | undefined): boolean;
45
- /** Validate untrusted input; diagnostics never include supplied values. */
56
+ /**
57
+ * Validate untrusted input; diagnostics never include supplied values.
58
+ *
59
+ * THE KIT SECTION IS STRUCTURAL ONLY (agent-model-kits RV-P1-1). `mainId` /
60
+ * `fastId` above are `missing_reference` because a dangling ROLE BINDING leaves
61
+ * the whole library unreadable (there is no "nothing bound" recovery path once
62
+ * a bound id is gone), while a dangling KIT SLOT degrades at resolution time
63
+ * into `Profile missing` and a refused apply - the same discipline
64
+ * `team.modelProfileId` already follows (`schema.ts` clampTeamConfig).
65
+ */
46
66
  export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
47
67
  /**
48
68
  * Disk-tolerance gate for a profile-level context window (profile-context-window 5.3).
@@ -55,11 +75,29 @@ export declare function validateModelProfiles(value: unknown): ProfileValidation
55
75
  * (invariant 4 - one bad number must not drop the user's credentials).
56
76
  */
57
77
  export declare function normalizeProfileContextWindow(value: unknown): number | null;
58
- /** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
78
+ /**
79
+ * Normalize a valid library into a safe field whitelist; throws a fixed error if invalid.
80
+ *
81
+ * THE THREE KIT KEYS ARE LISTED EXPLICITLY, NEVER `...spread` (R-2): this
82
+ * literal IS the whitelist (F-3), so a key missing from it disappears at the
83
+ * next write - a save that only touched `thinking` would silently delete every
84
+ * kit the user owns. `normalizeKits` is TOTAL, so a hand-edited kit degrades
85
+ * instead of taking the library (and the credentials in it) with it.
86
+ */
59
87
  export declare function normalizeModelProfiles(value: unknown): ModelProfilesConfig;
60
88
  /** Create an entry with a stable UUID and a frozen default name. */
61
89
  export declare function createModelProfile(input: Omit<ModelProfile, 'id'>): ModelProfile;
62
90
  /** Duplicate the current draft entry, including its explicitly edited credential. */
63
91
  export declare function duplicateModelProfile(entry: ModelProfile): ModelProfile;
64
- /** Remove an unused entry; binding references must be changed explicitly first. */
92
+ /** Ids of the kits whose three slots name `id`, for the reference message. */
93
+ export declare function kitNamesForProfile(library: ModelProfilesConfig, id: string): string[];
94
+ /**
95
+ * Remove an unused entry; binding references must be changed explicitly first.
96
+ *
97
+ * The message NAMES the kits that would be orphaned (D-9): a throw that only
98
+ * says "still in use" sends the user hunting through three separate screens for
99
+ * the reference it means, and the kits are the reference nobody thinks of.
100
+ * (The UI never reaches this throw - it unbinds, and deletes the kits, inside
101
+ * the draft first - so this is the library-level backstop, F-25.)
102
+ */
65
103
  export declare function removeModelProfile(library: ModelProfilesConfig, id: string): ModelProfilesConfig;