@aragon-agent/cli 0.6.33 → 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.
- package/CHANGELOG.md +2305 -2242
- package/README.md +119 -18
- package/dist/agent/controller.d.ts +29 -0
- package/dist/agent/controller.js +42 -1
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +1 -1
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +1 -1
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/input/keymap.js +18 -4
- package/dist/ui/App.js +135 -15
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.js +1 -1
- package/dist/ui/ScrollbackShell.d.ts +38 -13
- package/dist/ui/ScrollbackShell.js +4 -3
- package/dist/ui/StatusBar.d.ts +13 -0
- package/dist/ui/StatusBar.js +10 -3
- package/dist/ui/TeamPanel.d.ts +12 -0
- package/dist/ui/TeamPanel.js +10 -31
- package/dist/ui/Transcript.js +22 -2
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/layout/header-hints.d.ts +11 -1
- package/dist/ui/layout/header-hints.js +35 -3
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/virtual-window.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +7 -0
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/scrollback/LiveTail.d.ts +44 -10
- package/dist/ui/scrollback/LiveTail.js +20 -4
- package/dist/ui/scrollback/limits.d.ts +16 -0
- package/dist/ui/scrollback/limits.js +16 -0
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/overflow-print.d.ts +108 -0
- package/dist/ui/scrollback/overflow-print.js +120 -0
- package/dist/ui/scrollback/renderer.d.ts +11 -1
- package/dist/ui/scrollback/renderer.js +53 -25
- package/dist/ui/scrollback/tail-anchor.d.ts +68 -0
- package/dist/ui/scrollback/tail-anchor.js +77 -0
- package/dist/ui/scrollback/tail-window.d.ts +21 -8
- package/dist/ui/scrollback/tail-window.js +26 -10
- package/dist/ui/scrollback/use-scrollback-view.d.ts +38 -10
- package/dist/ui/scrollback/use-scrollback-view.js +372 -85
- package/dist/ui/team-status.d.ts +141 -0
- package/dist/ui/team-status.js +224 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-wheel-routing.js +2 -2
- package/package.json +2 -2
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { ADAPTER_PROVIDERS, clampContextWindow, isAutoToken } from './schema.js';
|
|
3
|
+
import { normalizeKits, validateKits } from './model-kits.js';
|
|
3
4
|
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
4
5
|
/** Return a fresh empty library without initiating a disk migration. */
|
|
5
6
|
export function emptyModelProfiles() {
|
|
6
|
-
return { version: 1, entries: [], mainId: null, fastId: null
|
|
7
|
+
return { version: 1, entries: [], mainId: null, fastId: null, kits: [],
|
|
8
|
+
defaultKitId: null, kitsVersion: 1 };
|
|
7
9
|
}
|
|
8
10
|
/** Return independently mutable role state. */
|
|
9
11
|
export function emptyModelProfileState(invalid = false) {
|
|
@@ -69,7 +71,16 @@ function validateEntry(value, index) {
|
|
|
69
71
|
}
|
|
70
72
|
return issues;
|
|
71
73
|
}
|
|
72
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Validate untrusted input; diagnostics never include supplied values.
|
|
76
|
+
*
|
|
77
|
+
* THE KIT SECTION IS STRUCTURAL ONLY (agent-model-kits RV-P1-1). `mainId` /
|
|
78
|
+
* `fastId` above are `missing_reference` because a dangling ROLE BINDING leaves
|
|
79
|
+
* the whole library unreadable (there is no "nothing bound" recovery path once
|
|
80
|
+
* a bound id is gone), while a dangling KIT SLOT degrades at resolution time
|
|
81
|
+
* into `Profile missing` and a refused apply - the same discipline
|
|
82
|
+
* `team.modelProfileId` already follows (`schema.ts` clampTeamConfig).
|
|
83
|
+
*/
|
|
73
84
|
export function validateModelProfiles(value) {
|
|
74
85
|
if (value === undefined)
|
|
75
86
|
return [];
|
|
@@ -99,6 +110,7 @@ export function validateModelProfiles(value) {
|
|
|
99
110
|
message: 'Select an existing profile or Current custom.' });
|
|
100
111
|
}
|
|
101
112
|
}
|
|
113
|
+
issues.push(...validateKits(value.kits));
|
|
102
114
|
return issues;
|
|
103
115
|
}
|
|
104
116
|
/**
|
|
@@ -116,19 +128,29 @@ export function normalizeProfileContextWindow(value) {
|
|
|
116
128
|
return null;
|
|
117
129
|
return clampContextWindow(value, null);
|
|
118
130
|
}
|
|
119
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Normalize a valid library into a safe field whitelist; throws a fixed error if invalid.
|
|
133
|
+
*
|
|
134
|
+
* THE THREE KIT KEYS ARE LISTED EXPLICITLY, NEVER `...spread` (R-2): this
|
|
135
|
+
* literal IS the whitelist (F-3), so a key missing from it disappears at the
|
|
136
|
+
* next write - a save that only touched `thinking` would silently delete every
|
|
137
|
+
* kit the user owns. `normalizeKits` is TOTAL, so a hand-edited kit degrades
|
|
138
|
+
* instead of taking the library (and the credentials in it) with it.
|
|
139
|
+
*/
|
|
120
140
|
export function normalizeModelProfiles(value) {
|
|
121
141
|
if (validateModelProfiles(value).length)
|
|
122
142
|
throw new Error('Invalid model profiles.');
|
|
123
143
|
if (value === undefined)
|
|
124
144
|
return emptyModelProfiles();
|
|
125
145
|
const library = value;
|
|
146
|
+
const { kits, defaultKitId, kitsVersion } = normalizeKits(library.kits, library.defaultKitId);
|
|
126
147
|
return { version: 1, mainId: library.mainId, fastId: library.fastId,
|
|
127
148
|
entries: library.entries.map((entry) => ({ id: entry.id, name: entry.name.trim()
|
|
128
149
|
|| [...entry.model.trim()].slice(0, 80).join(''), provider: entry.provider,
|
|
129
150
|
model: entry.model.trim(), baseUrl: entry.baseUrl?.trim() || null,
|
|
130
151
|
apiKey: entry.apiKey?.trim() || null,
|
|
131
|
-
contextWindow: normalizeProfileContextWindow(entry.contextWindow) }))
|
|
152
|
+
contextWindow: normalizeProfileContextWindow(entry.contextWindow) })),
|
|
153
|
+
kits, defaultKitId, kitsVersion };
|
|
132
154
|
}
|
|
133
155
|
/** Create an entry with a stable UUID and a frozen default name. */
|
|
134
156
|
export function createModelProfile(input) {
|
|
@@ -139,9 +161,28 @@ export function createModelProfile(input) {
|
|
|
139
161
|
export function duplicateModelProfile(entry) {
|
|
140
162
|
return { ...entry, id: randomUUID(), name: `${[...entry.name].slice(0, 73).join('')} (copy)` };
|
|
141
163
|
}
|
|
142
|
-
/**
|
|
164
|
+
/** Ids of the kits whose three slots name `id`, for the reference message. */
|
|
165
|
+
export function kitNamesForProfile(library, id) {
|
|
166
|
+
return (library.kits ?? []).filter((kit) => kit.mainProfileId === id
|
|
167
|
+
|| kit.fastProfileId === id
|
|
168
|
+
|| (kit.subagent.model === 'profile' && kit.subagent.modelProfileId === id))
|
|
169
|
+
.map((kit) => kit.name);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Remove an unused entry; binding references must be changed explicitly first.
|
|
173
|
+
*
|
|
174
|
+
* The message NAMES the kits that would be orphaned (D-9): a throw that only
|
|
175
|
+
* says "still in use" sends the user hunting through three separate screens for
|
|
176
|
+
* the reference it means, and the kits are the reference nobody thinks of.
|
|
177
|
+
* (The UI never reaches this throw - it unbinds, and deletes the kits, inside
|
|
178
|
+
* the draft first - so this is the library-level backstop, F-25.)
|
|
179
|
+
*/
|
|
143
180
|
export function removeModelProfile(library, id) {
|
|
144
181
|
if (library.mainId === id || library.fastId === id)
|
|
145
182
|
throw new Error('Profile is still in use.');
|
|
183
|
+
const kits = kitNamesForProfile(library, id);
|
|
184
|
+
if (kits.length) {
|
|
185
|
+
throw new Error(`Profile is used by kit ${kits.map((name) => `"${name}"`).join(', ')}.`);
|
|
186
|
+
}
|
|
146
187
|
return { ...library, entries: library.entries.filter((entry) => entry.id !== id) };
|
|
147
188
|
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1044,6 +1044,22 @@ export interface CliConfig {
|
|
|
1044
1044
|
modelSettingsRestartRequired?: boolean;
|
|
1045
1045
|
modelProfiles?: import('./model-profiles.js').ModelProfilesConfig;
|
|
1046
1046
|
modelProfileState?: import('./model-profiles.js').ModelProfilesRuntime;
|
|
1047
|
+
/**
|
|
1048
|
+
* The SESSION kit (agent-model-kits D-5 / §7.2). RUNTIME-ONLY.
|
|
1049
|
+
*
|
|
1050
|
+
* IT IS NOT IN `PersistedConfig` AND IT NEVER WILL BE (I-5 / I-14): `ctrl+K`
|
|
1051
|
+
* and `/kit use` promise "this session only", and that promise is worth more
|
|
1052
|
+
* than a saved convenience - it is what makes "one keystroke changed my
|
|
1053
|
+
* defaults" structurally impossible rather than merely unlikely.
|
|
1054
|
+
*
|
|
1055
|
+
* `held` is the only fact worth storing: WHICH slots still belong to the kit.
|
|
1056
|
+
* The bindings are derivable, and a stored copy of them would be a second
|
|
1057
|
+
* truth (I-7: every held slot must have been written into the live config).
|
|
1058
|
+
*/
|
|
1059
|
+
sessionKit?: {
|
|
1060
|
+
id: string;
|
|
1061
|
+
held: import('./model-kits.js').KitSlot[];
|
|
1062
|
+
};
|
|
1047
1063
|
apiKeyOverrideTarget?: {
|
|
1048
1064
|
provider: string;
|
|
1049
1065
|
baseUrl: string | null;
|
package/dist/input/keymap.js
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
* does not own so the caller can drop unknown control bytes instead of
|
|
8
8
|
* inserting them into the buffer.
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Map a Ctrl-modified key to an intent, if any.
|
|
12
|
+
*
|
|
13
|
+
* `Ctrl+K` IS DELIBERATELY NOT HERE (agent-model-kits D-15). That byte belongs
|
|
14
|
+
* to the Agent-kit picker on the main screen, and this recognizer runs BEFORE
|
|
15
|
+
* `PromptInput`'s `if (key.ctrl) return` - so keeping the binding would delete
|
|
16
|
+
* the user's draft in one listener while the other opened the overlay. The
|
|
17
|
+
* capability moved to `Alt+K` (`fromMeta`) instead of being dropped.
|
|
18
|
+
*/
|
|
11
19
|
function fromCtrl(input, key) {
|
|
12
20
|
if (key.leftArrow)
|
|
13
21
|
return 'wordLeft';
|
|
@@ -22,8 +30,6 @@ function fromCtrl(input, key) {
|
|
|
22
30
|
return 'deleteWordBack';
|
|
23
31
|
case 'u':
|
|
24
32
|
return 'killToStart';
|
|
25
|
-
case 'k':
|
|
26
|
-
return 'killToEnd';
|
|
27
33
|
default:
|
|
28
34
|
return null;
|
|
29
35
|
}
|
|
@@ -34,6 +40,11 @@ function fromMeta(input) {
|
|
|
34
40
|
return 'wordLeft';
|
|
35
41
|
if (input === 'f')
|
|
36
42
|
return 'wordRight';
|
|
43
|
+
// `killToEnd` LIVES HERE NOW (agent-model-kits D-15). `Alt+K` keeps the
|
|
44
|
+
// capability, and it keeps the kill family together: `M-k` is kill-sentence in
|
|
45
|
+
// the Emacs tradition this keymap already borrows `Ctrl+W` and `Alt+B/F` from.
|
|
46
|
+
if (input === 'k')
|
|
47
|
+
return 'killToEnd';
|
|
37
48
|
if (input === '\x7f' || input === '\b')
|
|
38
49
|
return 'deleteWordBack';
|
|
39
50
|
return null;
|
|
@@ -63,7 +74,10 @@ function fromRaw(input) {
|
|
|
63
74
|
return 'deleteWordBack';
|
|
64
75
|
case '\x15': // Ctrl+U (raw)
|
|
65
76
|
return 'killToStart';
|
|
66
|
-
case '\
|
|
77
|
+
case '\x1bk':
|
|
78
|
+
// Alt+K (raw ESC + k). `\x0b` (Ctrl+K) is deliberately ABSENT: it belongs
|
|
79
|
+
// to the Agent-kit picker, and a case for it here would be a dead binding
|
|
80
|
+
// that still looks alive to every future reader.
|
|
67
81
|
return 'killToEnd';
|
|
68
82
|
default:
|
|
69
83
|
return null;
|