@north-light/crouter 0.3.187 → 0.3.188
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/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/clients/attach/viewer.js +280 -280
- package/dist/core/__tests__/plugin-kinds.test.js +13 -0
- package/dist/core/config.js +32 -52
- package/dist/types.d.ts +5 -3
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -85,6 +85,19 @@ test('the scope\u2019s own config.json overrides its plugins; a disabled plugin
|
|
|
85
85
|
assert.equal(kinds['applet-builder']?.whenToUse, 'plugin gloss'); // patch merges, not replaces
|
|
86
86
|
assert.equal(kinds['ghost'], undefined);
|
|
87
87
|
});
|
|
88
|
+
test('a whenToUse-only entry re-glosses an existing kind without stripping its model tier', () => {
|
|
89
|
+
// The pre-change footgun: `{whenToUse}` parsed as a FULL entry and wholly
|
|
90
|
+
// replaced the kind, silently dropping explore off its cheap model tier.
|
|
91
|
+
// Now every entry field-merges over an existing kind; only a NEW kind
|
|
92
|
+
// requires whenToUse (a whenToUse-less new kind is dropped).
|
|
93
|
+
const root = userRoot();
|
|
94
|
+
writeFileSync(join(root, 'config.json'), JSON.stringify({ kinds: { explore: { whenToUse: 'custom explore gloss' }, 'new-glossless': { model: 'openai/light' } } }));
|
|
95
|
+
const kinds = readMergedLaunchConfig().kinds;
|
|
96
|
+
assert.equal(kinds['explore']?.whenToUse, 'custom explore gloss');
|
|
97
|
+
assert.equal(kinds['explore']?.model, 'openai/light'); // builtin tier survives
|
|
98
|
+
assert.equal(kinds['explore']?.orchestratorModel, 'anthropic/strong');
|
|
99
|
+
assert.equal(kinds['new-glossless'], undefined);
|
|
100
|
+
});
|
|
88
101
|
test('a project-root plugin layers above user config, and the project config above the plugin', () => {
|
|
89
102
|
const uRoot = userRoot();
|
|
90
103
|
writeFileSync(join(uRoot, 'config.json'), JSON.stringify({ kinds: { shared: { whenToUse: 'user gloss', model: 'openai/light' } } }));
|
package/dist/core/config.js
CHANGED
|
@@ -265,42 +265,20 @@ function mergeSpawnEnv(raw, base = []) {
|
|
|
265
265
|
}
|
|
266
266
|
return { allow: [...new Set([...base, ...extra])] };
|
|
267
267
|
}
|
|
268
|
-
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (typeof r.whenToUse !== 'string' || r.whenToUse.trim() === '')
|
|
277
|
-
return null;
|
|
278
|
-
const out = { whenToUse: r.whenToUse };
|
|
279
|
-
if (typeof r.model === 'string' && r.model.trim() !== '')
|
|
280
|
-
out.model = r.model;
|
|
281
|
-
if (typeof r.orchestratorModel === 'string' && r.orchestratorModel.trim() !== '')
|
|
282
|
-
out.orchestratorModel = r.orchestratorModel;
|
|
283
|
-
const isStringArray = (v) => Array.isArray(v) && v.every((item) => typeof item === 'string');
|
|
284
|
-
if (isStringArray(r.tools))
|
|
285
|
-
out.tools = r.tools;
|
|
286
|
-
if (isStringArray(r.extensions))
|
|
287
|
-
out.extensions = r.extensions;
|
|
288
|
-
if (isStringArray(r.availableTo))
|
|
289
|
-
out.availableTo = r.availableTo;
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
/** Validate one raw `kinds.<name>` entry as a sparse PATCH — an entry with no
|
|
293
|
-
* `whenToUse` that overrides individual launch knobs (model/orchestratorModel/
|
|
294
|
-
* tools/extensions/availableTo) of a kind some lower-precedence scope already
|
|
295
|
-
* defines. This is what lets a scope (e.g. a profile) persist ONLY
|
|
296
|
-
* `{ "model": "ultra" }` for a kind instead of freezing a full registry
|
|
297
|
-
* snapshot to disk (the same staleness trap `sparseModelLaddersToPersist`
|
|
298
|
-
* exists to avoid). Returns null when no valid field is present. */
|
|
299
|
-
function normalizeKindPatch(raw) {
|
|
268
|
+
/** Collect the valid `KindConfig` fields of one raw `kinds.<name>` entry —
|
|
269
|
+
* every field is optional here, `whenToUse` included; mistyped fields are
|
|
270
|
+
* dropped, never thrown (an invalid field in config.json must not break
|
|
271
|
+
* config parsing for every other kind — the loud gate is install time, see
|
|
272
|
+
* `invalidPluginKindsReasons`). Returns null when no valid field is present.
|
|
273
|
+
* Whether `whenToUse` is REQUIRED is `mergeKinds`'s call: only an entry that
|
|
274
|
+
* introduces a kind no lower layer defines needs one. */
|
|
275
|
+
function normalizeKindFields(raw) {
|
|
300
276
|
if (raw === null || typeof raw !== 'object')
|
|
301
277
|
return null;
|
|
302
278
|
const r = raw;
|
|
303
279
|
const out = {};
|
|
280
|
+
if (typeof r.whenToUse === 'string' && r.whenToUse.trim() !== '')
|
|
281
|
+
out.whenToUse = r.whenToUse;
|
|
304
282
|
if (typeof r.model === 'string' && r.model.trim() !== '')
|
|
305
283
|
out.model = r.model;
|
|
306
284
|
if (typeof r.orchestratorModel === 'string' && r.orchestratorModel.trim() !== '')
|
|
@@ -315,29 +293,31 @@ function normalizeKindPatch(raw) {
|
|
|
315
293
|
return Object.keys(out).length > 0 ? out : null;
|
|
316
294
|
}
|
|
317
295
|
/** Merge a raw `kinds` block over `base` (defaulting to the builtin registry):
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
296
|
+
* invalid entries are dropped, never thrown. Mirrors `mergeModelLadders`'s
|
|
297
|
+
* layer-over-defaults shape so a config layer can add or override a single
|
|
298
|
+
* kind without restating the whole registry. One rule, no entry shapes:
|
|
299
|
+
* - A kind some lower layer already defines: the entry FIELD-MERGES over it
|
|
300
|
+
* — any subset of fields, `whenToUse` included, so overriding just the
|
|
301
|
+
* spawn-menu guidance of a builtin kind never strips its model tier
|
|
302
|
+
* (the sparse-override shape `persistDefaultKindModel` writes).
|
|
303
|
+
* - A NEW kind: the entry defines it and must carry `whenToUse` (a kind
|
|
304
|
+
* with no spawn-menu gloss is meaningless); dropped otherwise.
|
|
305
|
+
* Deliberately NOT expressible: wholly replacing a lower layer's kind — a
|
|
306
|
+
* field can be overridden but never removed by omission. */
|
|
326
307
|
function mergeKinds(raw, base = defaultKindsConfig()) {
|
|
327
308
|
const out = { ...base };
|
|
328
309
|
if (raw !== null && typeof raw === 'object') {
|
|
329
310
|
for (const [kind, value] of Object.entries(raw)) {
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
332
|
-
out[kind] = normalized;
|
|
311
|
+
const fields = normalizeKindFields(value);
|
|
312
|
+
if (fields === null)
|
|
333
313
|
continue;
|
|
334
|
-
}
|
|
335
314
|
const existing = out[kind];
|
|
336
|
-
if (existing
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
out[kind] =
|
|
315
|
+
if (existing !== undefined) {
|
|
316
|
+
out[kind] = { ...existing, ...fields };
|
|
317
|
+
}
|
|
318
|
+
else if (fields.whenToUse !== undefined) {
|
|
319
|
+
out[kind] = fields;
|
|
320
|
+
}
|
|
341
321
|
}
|
|
342
322
|
}
|
|
343
323
|
return out;
|
|
@@ -401,14 +381,14 @@ export function invalidPluginKindsReasons(raw) {
|
|
|
401
381
|
reasons.push(`kinds.${kind}.${field} must be an array of strings`);
|
|
402
382
|
}
|
|
403
383
|
}
|
|
404
|
-
if (
|
|
405
|
-
reasons.push(`kinds.${kind}
|
|
384
|
+
if (normalizeKindFields(value) === null) {
|
|
385
|
+
reasons.push(`kinds.${kind} has no valid KindConfig field — an entry must set at least one of: ${[...KIND_CONFIG_KEYS].join(', ')} (whenToUse is required when the kind exists in no lower layer)`);
|
|
406
386
|
}
|
|
407
387
|
}
|
|
408
388
|
return reasons;
|
|
409
389
|
}
|
|
410
390
|
/** Validate one raw `remoteCanvas.targets.<name>` entry, or drop it (return
|
|
411
|
-
* null) rather than throwing — same rule `
|
|
391
|
+
* null) rather than throwing — same rule `normalizeKindFields` follows. A
|
|
412
392
|
* valid entry needs a non-empty `previewEndpoint` and `relayTokenRef`; the
|
|
413
393
|
* token itself is never stored here (see `RemoteCanvasTarget`). */
|
|
414
394
|
function normalizeRemoteCanvasTarget(raw) {
|
package/dist/types.d.ts
CHANGED
|
@@ -35,9 +35,11 @@ export interface PluginManifest {
|
|
|
35
35
|
owner?: OwnerRef;
|
|
36
36
|
/** Kind-registry contributions, keyed by full kind string (top-level e.g.
|
|
37
37
|
* `applet-builder`, or sub-kind e.g. `plan/reviewers/security`). Each entry
|
|
38
|
-
* is
|
|
39
|
-
*
|
|
40
|
-
* the
|
|
38
|
+
* is any subset of `KindConfig` fields: it FIELD-MERGES over a kind a lower
|
|
39
|
+
* layer already defines (`whenToUse` included — overriding just the spawn
|
|
40
|
+
* guidance never strips the kind's model tier), and defines a NEW kind when
|
|
41
|
+
* none does (then `whenToUse` is required) — the same rule a scope
|
|
42
|
+
* `config.json` `kinds` block follows.
|
|
41
43
|
* `readMergedLaunchConfig` layers an enabled plugin's entries directly
|
|
42
44
|
* above the builtin registry and below its host scope's own `config.json`,
|
|
43
45
|
* so a plugin can ship a persona kind (registry entry here + gated persona
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.188",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.188",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|