@north-light/crouter 0.3.161 → 0.3.163
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/api/client.d.ts +3 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/modelauth.d.ts +32 -0
- package/dist/api/dto/modelauth.js +2 -1
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +88 -396
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +215 -419
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +8 -3
- package/dist/clients/attach/render/crtr-output.js +31 -2
- package/dist/clients/attach/render/edit-diff.js +26 -2
- package/dist/clients/attach/render/tool-calls.d.ts +7 -2
- package/dist/clients/attach/render/tool-calls.js +25 -4
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +441 -439
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
- package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
- package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/model-pin-durability.test.js +7 -5
- package/dist/core/__tests__/model-routes.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes.test.js +41 -0
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/auth-file.d.ts +3 -2
- package/dist/core/auth-file.js +5 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/canvas/types.d.ts +6 -1
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.js +88 -2
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/model-routes.d.ts +37 -0
- package/dist/core/model-routes.js +140 -0
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.d.ts +0 -4
- package/dist/core/runtime/broker.js +119 -385
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +10 -12
- package/dist/core/runtime/launch.js +16 -22
- package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
- package/dist/core/runtime/managed-provider-cooling.js +0 -19
- package/dist/core/runtime/model-swap.js +7 -1
- package/dist/core/runtime/promote.js +1 -0
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/subscription-state.d.ts +12 -36
- package/dist/core/subscription-state.js +19 -203
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
- package/dist/daemon/api/handlers/modelauth.js +51 -2
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +74 -0
- package/dist/types.d.ts +17 -0
- package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -68,7 +68,7 @@ test('review still permits equal-or-stronger explicit models', () => {
|
|
|
68
68
|
assert.equal(spec('review', 'strong'), expected('strong'));
|
|
69
69
|
assert.equal(spec('review', 'ultra'), expected('ultra'));
|
|
70
70
|
});
|
|
71
|
-
test('
|
|
71
|
+
test('concrete overrides stay pinned while logical provider/strength overrides remain unpinned', () => {
|
|
72
72
|
const qualified = buildLaunchSpec('developer', 'base', {
|
|
73
73
|
lifecycle: 'terminal', hasManager: true, cwd: home, profileId: null, model: 'anthropic/ultra',
|
|
74
74
|
}).launch;
|
|
@@ -78,7 +78,7 @@ test('explicit provider-qualified overrides preserve their provider pin through
|
|
|
78
78
|
const bare = buildLaunchSpec('developer', 'base', {
|
|
79
79
|
lifecycle: 'terminal', hasManager: true, cwd: home, profileId: null, model: 'ultra',
|
|
80
80
|
}).launch;
|
|
81
|
-
assert.equal(qualified.modelProviderPinned,
|
|
81
|
+
assert.equal(qualified.modelProviderPinned, false);
|
|
82
82
|
assert.equal(exact.modelProviderPinned, true);
|
|
83
83
|
assert.equal(bare.modelProviderPinned, false);
|
|
84
84
|
});
|
|
@@ -193,9 +193,9 @@ test('buildPiArgv restores mandatory canvas extensions when persisted launch met
|
|
|
193
193
|
}
|
|
194
194
|
assert.deepEqual(extensions.slice(0, required.size), mandatory, 'mandatory extensions are loaded first and once');
|
|
195
195
|
});
|
|
196
|
-
test('buildPiArgv replaces the
|
|
196
|
+
test('buildPiArgv replaces the previous mandatory prefix with the current canvas hooks', () => {
|
|
197
197
|
const historicalNames = [
|
|
198
|
-
'canvas-
|
|
198
|
+
'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
|
|
199
199
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
200
200
|
'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
|
|
201
201
|
];
|
|
@@ -224,6 +224,7 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
224
224
|
const remapped = installTmuxBindings(resolveKeybindings({
|
|
225
225
|
'crtr.tmux.menu.open': ['alt+shift+z'],
|
|
226
226
|
'crtr.tmux.menu.graph': ['shift+z'],
|
|
227
|
+
'crtr.tmux.menu-global.fold-tools': ['alt+shift+f'],
|
|
227
228
|
'crtr.tmux.node.next': [],
|
|
228
229
|
'crtr.tmux.view.next': ['alt+z > n'],
|
|
229
230
|
'crtr.tmux.view.previous': ['alt+z > p'],
|
|
@@ -231,10 +232,15 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
231
232
|
assert.equal(remapped.ok, true, JSON.stringify(remapped.diagnostics));
|
|
232
233
|
assert.equal(tmuxBinding(socket, 'root', 'M-c'), null);
|
|
233
234
|
assert.equal(tmuxBinding(socket, 'root', 'M-]'), null);
|
|
234
|
-
|
|
235
|
+
assert.equal(tmuxBinding(socket, 'root', 'M-F'), null);
|
|
236
|
+
assert.equal(tmuxBinding(socket, 'root', 'M-S-f'), null);
|
|
237
|
+
const foldTools = tmuxBinding(socket, 'root', 'M-S-F') ?? '';
|
|
238
|
+
assert.match(foldTools, /fold-tools/);
|
|
239
|
+
assert.doesNotMatch(foldTools, /#\{pane_id\}/);
|
|
240
|
+
const menu = tmuxBinding(socket, 'root', 'M-S-Z') ?? '';
|
|
235
241
|
assert.match(menu, /graph view/);
|
|
236
242
|
assert.match(menu, /Z/);
|
|
237
|
-
assert.match(menu, /"Alt\+Shift\+Z to close" M-Z refresh-client/);
|
|
243
|
+
assert.match(menu, /"Alt\+Shift\+Z to close" M-S-Z refresh-client/);
|
|
238
244
|
const prefix = tmuxBinding(socket, 'root', 'M-z') ?? '';
|
|
239
245
|
assert.match(prefix, /switch-client/);
|
|
240
246
|
const again = installTmuxBindings(resolveKeybindings({
|
|
@@ -244,11 +250,11 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
|
|
|
244
250
|
'crtr.tmux.view.previous': ['alt+z > p'],
|
|
245
251
|
}));
|
|
246
252
|
assert.equal(again.ok, true, JSON.stringify(again.diagnostics));
|
|
247
|
-
isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-Z', 'display-message', 'user-owned']);
|
|
253
|
+
isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-S-Z', 'display-message', 'user-owned']);
|
|
248
254
|
const occupied = installTmuxBindings(resolveKeybindings({ 'crtr.tmux.menu.open': ['alt+shift+z'] }));
|
|
249
255
|
assert.equal(occupied.ok, true);
|
|
250
256
|
assert.ok(occupied.diagnostics.some((diagnostic) => diagnostic.code === 'occupied' && diagnostic.bindingId === 'crtr.tmux.menu.open'));
|
|
251
|
-
assert.match(tmuxBinding(socket, 'root', 'M-Z') ?? '', /user-owned/);
|
|
257
|
+
assert.match(tmuxBinding(socket, 'root', 'M-S-Z') ?? '', /user-owned/);
|
|
252
258
|
}
|
|
253
259
|
finally {
|
|
254
260
|
if (priorTmux === undefined)
|
|
@@ -386,15 +386,15 @@ test('close still blocks on real .crouter/memory/ work — a tracked-file edit a
|
|
|
386
386
|
run(['config', 'user.email', 'test@example.com'], memoryRepo);
|
|
387
387
|
run(['config', 'user.name', 'Test'], memoryRepo);
|
|
388
388
|
mkdirSync(join(memoryRepo, '.crouter', 'memory'), { recursive: true });
|
|
389
|
-
writeFileSync(join(memoryRepo, '.crouter', 'memory', '
|
|
389
|
+
writeFileSync(join(memoryRepo, '.crouter', 'memory', 'tracked.md'), 'tracked memory doc\n');
|
|
390
390
|
run(['add', '.'], memoryRepo);
|
|
391
391
|
run(['commit', '-m', 'init with tracked memory'], memoryRepo);
|
|
392
392
|
const editNodeId = 'wt-real-crouter-memory-edit';
|
|
393
393
|
const editWt = createManagedWorktree(memoryRepo, editNodeId);
|
|
394
394
|
createNode(node(editNodeId, { cwd: editWt.path, managed_worktree: editWt }));
|
|
395
|
-
writeFileSync(join(editWt.path, '.crouter', 'memory', '
|
|
395
|
+
writeFileSync(join(editWt.path, '.crouter', 'memory', 'tracked.md'), 'edited tracked memory doc\n');
|
|
396
396
|
const editStatus = gitSync(['status', '--porcelain'], editWt.path).stdout;
|
|
397
|
-
assert.ok(editStatus.includes('M .crouter/memory/
|
|
397
|
+
assert.ok(editStatus.includes('M .crouter/memory/tracked.md'), 'a tracked .crouter/memory/ edit must still show as modified — exclude can never hide a tracked-file change');
|
|
398
398
|
assert.throws(() => closeManagedWorktree(editNodeId), (error) => error instanceof Error && 'code' in error && error.code === 'dirty_worktree');
|
|
399
399
|
const newDocNodeId = 'wt-real-crouter-memory-new-doc';
|
|
400
400
|
const newDocWt = createManagedWorktree(memoryRepo, newDocNodeId);
|
package/dist/core/auth-file.d.ts
CHANGED
|
@@ -20,8 +20,9 @@ export declare function readAuthCredentials(authPath?: string): Promise<AuthFile
|
|
|
20
20
|
export declare function readAuthCredential(providerId: string, authPath?: string): Promise<Credential | undefined>;
|
|
21
21
|
/** Persist one provider's credential under the auth.json lock, leaving every other entry intact. */
|
|
22
22
|
export declare function setAuthCredential(providerId: string, credential: Credential, authPath?: string): Promise<void>;
|
|
23
|
-
/** Remove one provider's credential (logout) under the auth.json lock.
|
|
24
|
-
|
|
23
|
+
/** Remove one provider's credential (logout) under the auth.json lock. Returns
|
|
24
|
+
* whether a record was removed from the exact snapshot this write replaced. */
|
|
25
|
+
export declare function deleteAuthCredential(providerId: string, authPath?: string): Promise<boolean>;
|
|
25
26
|
/**
|
|
26
27
|
* The `CredentialStore` crouter hands to `ModelRuntime`. Backed directly by auth.json rather
|
|
27
28
|
* than by an in-memory cache, so `modify` is a real cross-process read-modify-write: pi's
|
package/dist/core/auth-file.js
CHANGED
|
@@ -126,12 +126,14 @@ export async function setAuthCredential(providerId, credential, authPath = defau
|
|
|
126
126
|
return { result: undefined, next: serialize(data) };
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
/** Remove one provider's credential (logout) under the auth.json lock.
|
|
129
|
+
/** Remove one provider's credential (logout) under the auth.json lock. Returns
|
|
130
|
+
* whether a record was removed from the exact snapshot this write replaced. */
|
|
130
131
|
export async function deleteAuthCredential(providerId, authPath = defaultAuthPath()) {
|
|
131
|
-
|
|
132
|
+
return new LockedJsonFile(authPath).withLockAsync(async (current) => {
|
|
132
133
|
const data = parse(current);
|
|
134
|
+
const removed = Object.hasOwn(data, providerId);
|
|
133
135
|
delete data[providerId];
|
|
134
|
-
return { result:
|
|
136
|
+
return { result: removed, next: serialize(data) };
|
|
135
137
|
});
|
|
136
138
|
}
|
|
137
139
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve"];
|
|
1
|
+
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve"];
|
|
2
2
|
/** Remove a complete persisted canvas prefix while retaining every genuine
|
|
3
3
|
* caller/kind extra, including a custom extension whose basename happens to
|
|
4
4
|
* resemble a canvas hook. */
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
// Canvas-extension identity is structural, not basename-based: old runtime roots
|
|
2
|
-
// are recognized only when they carry
|
|
2
|
+
// are recognized only when they carry a complete shipped canvas prefix.
|
|
3
3
|
// canvas-inbox-watcher must load before canvas-stophook: at agent_settled it
|
|
4
4
|
// commits the cursor the stophook then reads to decide idle release.
|
|
5
5
|
export const CANVAS_EXTENSION_NAMES = [
|
|
6
6
|
'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
|
|
7
7
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
8
|
-
'canvas-
|
|
8
|
+
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
9
|
+
'canvas-bash-valve',
|
|
9
10
|
];
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const HISTORICAL_CANVAS_EXTENSION_PREFIXES = [
|
|
12
|
+
[
|
|
13
|
+
'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
|
|
14
|
+
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
15
|
+
'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
'canvas-stophook', 'canvas-inbox-watcher', 'canvas-recap',
|
|
19
|
+
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
20
|
+
'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
|
|
21
|
+
],
|
|
14
22
|
];
|
|
15
23
|
function matchesExtensionPrefix(extensions, names) {
|
|
24
|
+
if (extensions.length < names.length)
|
|
25
|
+
return false;
|
|
16
26
|
const paths = extensions.slice(0, names.length);
|
|
17
27
|
const first = paths[0].replaceAll('\\', '/');
|
|
18
28
|
const marker = '/pi-extensions/';
|
|
@@ -24,11 +34,8 @@ function matchesExtensionPrefix(extensions, names) {
|
|
|
24
34
|
return names.every((name, index) => paths[index].replaceAll('\\', '/') === `${root}${name}${suffix}`);
|
|
25
35
|
}
|
|
26
36
|
function extensionPrefixLength(extensions) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return matchesExtensionPrefix(extensions, CANVAS_EXTENSION_NAMES) || matchesExtensionPrefix(extensions, LEGACY_CANVAS_EXTENSION_NAMES)
|
|
30
|
-
? CANVAS_EXTENSION_NAMES.length
|
|
31
|
-
: 0;
|
|
37
|
+
const prefixes = [CANVAS_EXTENSION_NAMES, ...HISTORICAL_CANVAS_EXTENSION_PREFIXES];
|
|
38
|
+
return prefixes.find((names) => matchesExtensionPrefix(extensions, names))?.length ?? 0;
|
|
32
39
|
}
|
|
33
40
|
/** Remove a complete persisted canvas prefix while retaining every genuine
|
|
34
41
|
* caller/kind extra, including a custom extension whose basename happens to
|
|
@@ -23,9 +23,8 @@ export declare function fullName(node: NamedNode): string;
|
|
|
23
23
|
* badge. */
|
|
24
24
|
export declare function personaLabel(meta: Pick<NodeMeta, 'kind' | 'mode'>): string;
|
|
25
25
|
/** The pi session display name — the editor label in the top-left. It carries
|
|
26
|
-
* ONE name:
|
|
27
|
-
*
|
|
28
|
-
* the chip doesn't read as two names side by side. Persona lives in
|
|
29
|
-
* editor's bottom-right badge. Recomputed from meta on every revive
|
|
30
|
-
* live when naming generates the first task description. */
|
|
26
|
+
* ONE name: a person's explicit handle when present, otherwise the generated
|
|
27
|
+
* description of the node's first task. Unlike `fullName` it never prints
|
|
28
|
+
* both, so the chip doesn't read as two names side by side. Persona lives in
|
|
29
|
+
* the editor's bottom-right badge. Recomputed from meta on every revive. */
|
|
31
30
|
export declare function editorLabel(meta: NodeMeta): string;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// One source of truth for "what do we call this node on screen". `fullName`
|
|
4
4
|
// combines the node's explicit handle (meta.name) with the pi-generated
|
|
5
|
-
// description of its first task (meta.description)
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// description of its first task (meta.description) for canvas and tmux surfaces.
|
|
6
|
+
// `editorLabel` keeps the editor chip compact by choosing the explicit handle
|
|
7
|
+
// when a person supplied one, otherwise the generated description.
|
|
8
8
|
/** Nerd Font glyph for a node's mode in its persona badge. A base worker reads
|
|
9
9
|
* as a single figure (nf-fa-user, U+F007); an orchestrator reads as a fan-out
|
|
10
10
|
* hierarchy (nf-fa-sitemap, U+F0E8). */
|
|
@@ -30,14 +30,14 @@ export function personaLabel(meta) {
|
|
|
30
30
|
return `${modeIcon(meta.mode)} ${meta.kind}`;
|
|
31
31
|
}
|
|
32
32
|
/** The pi session display name — the editor label in the top-left. It carries
|
|
33
|
-
* ONE name:
|
|
34
|
-
*
|
|
35
|
-
* the chip doesn't read as two names side by side. Persona lives in
|
|
36
|
-
* editor's bottom-right badge. Recomputed from meta on every revive
|
|
37
|
-
* live when naming generates the first task description. */
|
|
33
|
+
* ONE name: a person's explicit handle when present, otherwise the generated
|
|
34
|
+
* description of the node's first task. Unlike `fullName` it never prints
|
|
35
|
+
* both, so the chip doesn't read as two names side by side. Persona lives in
|
|
36
|
+
* the editor's bottom-right badge. Recomputed from meta on every revive. */
|
|
38
37
|
export function editorLabel(meta) {
|
|
39
38
|
const icon = (meta.icon ?? '').trim();
|
|
39
|
+
const handle = meta.name && meta.name !== meta.kind ? meta.name.trim() : '';
|
|
40
40
|
const desc = (meta.description ?? '').trim();
|
|
41
|
-
const name = desc !== '' ? desc :
|
|
41
|
+
const name = handle !== '' ? handle : desc !== '' ? desc : meta.name;
|
|
42
42
|
return icon !== '' ? `${icon} ${name}` : name;
|
|
43
43
|
}
|
|
@@ -25,7 +25,12 @@ export type EdgeType = 'subscribes_to' | 'spawned_by';
|
|
|
25
25
|
export interface LaunchSpec {
|
|
26
26
|
/** Model id/pattern passed to pi `--model`. */
|
|
27
27
|
model?: string;
|
|
28
|
-
/**
|
|
28
|
+
/** Original logical family/strength preference, retained after normalization. */
|
|
29
|
+
modelIntent?: {
|
|
30
|
+
family: string;
|
|
31
|
+
strength: import('../../types.js').ModelStrength;
|
|
32
|
+
};
|
|
33
|
+
/** True only for a caller's explicit concrete `provider/id` override. */
|
|
29
34
|
modelProviderPinned?: boolean;
|
|
30
35
|
/** True for an exact-consult launch: `model` is the verbatim captured
|
|
31
36
|
* `provider/id:thinking` spec, bypassing the review floor/normalize and
|
|
@@ -27,7 +27,7 @@ const CONFIGURED_LEAF_KEYS = new Set([...LEAF_KEYS_COMMON, 'rest']);
|
|
|
27
27
|
const ROOT_ENTRY_KEYS = new Set(['concept', 'description', 'whenToUse']);
|
|
28
28
|
const FILE_ENCODINGS = new Set(['text', 'base64']);
|
|
29
29
|
const PARAM_KEYS_BY_KIND = {
|
|
30
|
-
positional: new Set(['kind', 'name', 'type', 'required', 'constraint', 'encoding', 'defaultFromEnv']),
|
|
30
|
+
positional: new Set(['kind', 'name', 'type', 'required', 'constraint', 'repeatable', 'encoding', 'defaultFromEnv']),
|
|
31
31
|
flag: new Set([
|
|
32
32
|
'kind',
|
|
33
33
|
'name',
|
|
@@ -137,18 +137,31 @@ function validateParams(raw, path, issue) {
|
|
|
137
137
|
issue('command_node_invalid', `positional type must be string|path`, String(p['type']), 'string | path | omitted', 'Fix the positional type.', `${pp}.type`);
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
|
+
let repeatable = false;
|
|
141
|
+
if (p['repeatable'] !== undefined) {
|
|
142
|
+
if (typeof p['repeatable'] !== 'boolean') {
|
|
143
|
+
issue('command_node_invalid', `repeatable must be a boolean`, typeName(p['repeatable']), 'true | false | omitted', 'Fix repeatable.', `${pp}.repeatable`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
repeatable = p['repeatable'];
|
|
147
|
+
}
|
|
140
148
|
const encoding = validateFileEncoding(p, pp, issue);
|
|
141
149
|
if (encoding === null)
|
|
142
150
|
return null;
|
|
143
151
|
const envDefault = validateDefaultFromEnv(p, pp, issue);
|
|
144
152
|
if (envDefault === null)
|
|
145
153
|
return null;
|
|
154
|
+
if (repeatable && envDefault !== undefined) {
|
|
155
|
+
issue('command_node_invalid', `repeatable positional must not declare defaultFromEnv`, name, 'no defaultFromEnv on a repeatable positional (values accumulate into an array)', 'Remove defaultFromEnv, or drop repeatable.', `${pp}.defaultFromEnv`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
146
158
|
out.push({
|
|
147
159
|
kind: 'positional',
|
|
148
160
|
name,
|
|
149
161
|
...(p['type'] !== undefined ? { type: p['type'] } : {}),
|
|
150
162
|
required: p['required'],
|
|
151
163
|
constraint: p['constraint'],
|
|
164
|
+
...(repeatable ? { repeatable: true } : {}),
|
|
152
165
|
...(encoding !== undefined ? { encoding } : {}),
|
|
153
166
|
...(envDefault !== undefined ? { defaultFromEnv: envDefault } : {}),
|
|
154
167
|
});
|
|
@@ -251,7 +264,7 @@ function validateFileEncoding(p, pp, issue) {
|
|
|
251
264
|
* so it is meaningful only on a string|path param, and it cannot coexist with a
|
|
252
265
|
* static `default` (two competing sources for one slot is an authoring error,
|
|
253
266
|
* not a precedence puzzle for the reader). The caller checks `repeatable`
|
|
254
|
-
* separately, where that
|
|
267
|
+
* separately, where that param is in scope: a repeatable param accumulates an
|
|
255
268
|
* ARRAY, and one env string cannot honestly fill it. Returns the env var name,
|
|
256
269
|
* undefined when absent, or null when invalid. */
|
|
257
270
|
function validateDefaultFromEnv(p, pp, issue) {
|
|
@@ -716,11 +729,11 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
716
729
|
return null;
|
|
717
730
|
}
|
|
718
731
|
}
|
|
719
|
-
// A repeatable
|
|
732
|
+
// A repeatable param's parsed value is an array; only JSON body placement
|
|
720
733
|
// can carry it faithfully (query/header/path would String() it into "a,b").
|
|
721
734
|
const mappedParam = paramsByName.get(paramName);
|
|
722
|
-
if (mappedParam.kind === 'flag' && mappedParam.repeatable === true && inValue !== 'body') {
|
|
723
|
-
issue('command_rest_invalid', `repeatable
|
|
735
|
+
if ((mappedParam.kind === 'flag' || mappedParam.kind === 'positional') && mappedParam.repeatable === true && inValue !== 'body') {
|
|
736
|
+
issue('command_rest_invalid', `repeatable param must use body placement`, `${paramName}: in "${inValue}"`, 'in: "body" (array values ship as a JSON array)', `Map "${paramName}" to body placement.`, `${path}.params.${paramName}.in`);
|
|
724
737
|
return null;
|
|
725
738
|
}
|
|
726
739
|
// Collect body params for coalesced source validation
|
package/dist/core/command.js
CHANGED
|
@@ -283,7 +283,8 @@ export async function parseArgv(params, tokens, options) {
|
|
|
283
283
|
result[flagNameToKey(f.name)] = f.default;
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
-
|
|
286
|
+
const positionalValues = [];
|
|
287
|
+
const repeatablePositional = positionalParam?.repeatable === true;
|
|
287
288
|
let positionalWasSupplied = false; // Track if positional came from argv (not stdin)
|
|
288
289
|
let forcedPositional = false; // true after bare --
|
|
289
290
|
let i = 0;
|
|
@@ -385,7 +386,13 @@ export async function parseArgv(params, tokens, options) {
|
|
|
385
386
|
continue;
|
|
386
387
|
}
|
|
387
388
|
// Positional (or token after --)
|
|
388
|
-
if (
|
|
389
|
+
if (repeatablePositional && positionalParam !== undefined) {
|
|
390
|
+
positionalValues.push(token);
|
|
391
|
+
positionalWasSupplied = true;
|
|
392
|
+
i++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (positionalValues.length > 0) {
|
|
389
396
|
// A leaf that also declares a `stdin` param treats its stdin value as
|
|
390
397
|
// write-only/sensitive by convention (`profile env set`'s value, etc.).
|
|
391
398
|
// An unexpected extra positional here is the single most likely misuse
|
|
@@ -404,13 +411,15 @@ export async function parseArgv(params, tokens, options) {
|
|
|
404
411
|
if (positionalParam === undefined && stdinParam === undefined) {
|
|
405
412
|
throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
|
|
406
413
|
}
|
|
407
|
-
|
|
414
|
+
positionalValues.push(token);
|
|
408
415
|
positionalWasSupplied = true;
|
|
409
416
|
i++;
|
|
410
417
|
}
|
|
411
418
|
// Assign positional (already tracked in provided set above)
|
|
412
|
-
if (
|
|
413
|
-
result[flagNameToKey(positionalParam.name)] =
|
|
419
|
+
if (positionalValues.length > 0 && positionalParam !== undefined) {
|
|
420
|
+
result[flagNameToKey(positionalParam.name)] = repeatablePositional
|
|
421
|
+
? positionalValues
|
|
422
|
+
: positionalValues[0];
|
|
414
423
|
}
|
|
415
424
|
// Resolve stdin if declared. A positional token (when there's no dedicated
|
|
416
425
|
// positional param to claim it) satisfies the stdin param directly, so
|
|
@@ -421,7 +430,8 @@ export async function parseArgv(params, tokens, options) {
|
|
|
421
430
|
// and name both sources.
|
|
422
431
|
let stdinWasSupplied = false;
|
|
423
432
|
if (stdinParam !== undefined) {
|
|
424
|
-
if (
|
|
433
|
+
if (positionalValues.length > 0 && positionalParam === undefined) {
|
|
434
|
+
const positionalValue = positionalValues[0];
|
|
425
435
|
if (positionalValue === '-') {
|
|
426
436
|
const raw = await readStdinRaw();
|
|
427
437
|
// Empty required stdin is NOT thrown here: it is left unset so the
|
|
@@ -492,7 +502,7 @@ export async function parseArgv(params, tokens, options) {
|
|
|
492
502
|
const envValue = process.env[p.defaultFromEnv];
|
|
493
503
|
if (envValue === undefined || envValue === '')
|
|
494
504
|
continue;
|
|
495
|
-
result[key] = envValue;
|
|
505
|
+
result[key] = p.kind === 'positional' && p.repeatable === true ? [envValue] : envValue;
|
|
496
506
|
provided.add(key);
|
|
497
507
|
}
|
|
498
508
|
// Validate required params — COMPLETE, never first-failure. Drip-feeding one
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength } from '../types.js';
|
|
1
|
+
import type { Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
2
2
|
export declare function configPath(scope: Scope): string | null;
|
|
3
3
|
export declare function statePath(scope: Scope): string | null;
|
|
4
4
|
export declare function readConfig(scope: Scope): ScopeConfig;
|
|
@@ -33,6 +33,8 @@ export declare function readRawScopeConfig(scope: Scope): Partial<ScopeConfig> |
|
|
|
33
33
|
export interface MergedLaunchConfig {
|
|
34
34
|
kinds: Record<string, KindConfig>;
|
|
35
35
|
modelLadders: ModelLaddersConfig;
|
|
36
|
+
modelRoutes?: Record<string, ModelRouteConfig>;
|
|
37
|
+
modelRouting?: ModelRoutingConfig;
|
|
36
38
|
/** The merged `spawnEnv.allow` set (project stack > profile > user >
|
|
37
39
|
* builtin, additive) — read by `buildOperationalEnvBase`
|
|
38
40
|
* (`core/runtime/spawn-env.ts`) as source C of the broker spawn-env
|
package/dist/core/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
5
|
+
import { emitEvent } from './events/emit.js';
|
|
5
6
|
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
6
7
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
7
8
|
import { profileRoot } from './profiles/manifest.js';
|
|
@@ -172,6 +173,83 @@ function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
|
172
173
|
}
|
|
173
174
|
return out;
|
|
174
175
|
}
|
|
176
|
+
function validStrength(value) {
|
|
177
|
+
return value === 'ultra' || value === 'strong' || value === 'medium' || value === 'light';
|
|
178
|
+
}
|
|
179
|
+
/** Validate and overlay route entries. Invalid declarations are loud but local:
|
|
180
|
+
* a typo must not prevent unrelated launch configuration from resolving. */
|
|
181
|
+
function mergeModelRoutes(raw, base = {}) {
|
|
182
|
+
const out = { ...base };
|
|
183
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
184
|
+
return out;
|
|
185
|
+
for (const [routeId, value] of Object.entries(raw)) {
|
|
186
|
+
const patch = value;
|
|
187
|
+
const invalid = (rule) => { emitEvent({ level: 'warn', event: 'config.model_routes.invalid', fields: { route_id: routeId, rule } }); };
|
|
188
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
189
|
+
invalid('route must be an object');
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const route = { ...out[routeId], ...patch, models: { ...out[routeId]?.models, ...patch.models } };
|
|
193
|
+
if (typeof route.family !== 'string' || route.family.trim() === '') {
|
|
194
|
+
invalid('family must be a non-empty string');
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (typeof route.credentialSource !== 'string' || route.credentialSource.trim() === '') {
|
|
198
|
+
invalid('credentialSource must be a non-empty string');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (typeof route.providerId !== 'string' || route.providerId.trim() === '') {
|
|
202
|
+
invalid('providerId must be a non-empty string');
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!route.models || typeof route.models !== 'object' || Array.isArray(route.models)) {
|
|
206
|
+
invalid('models must be an object');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const models = {};
|
|
210
|
+
let rule;
|
|
211
|
+
for (const [strength, spec] of Object.entries(route.models)) {
|
|
212
|
+
if (!validStrength(strength)) {
|
|
213
|
+
rule = `models.${strength} is not a model strength`;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (typeof spec !== 'string' || spec.trim() === '') {
|
|
217
|
+
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const slash = spec.indexOf('/');
|
|
221
|
+
if (slash <= 0 || slash === spec.length - 1) {
|
|
222
|
+
rule = `models.${strength} must be provider/id`;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (spec.slice(0, slash) !== route.providerId) {
|
|
226
|
+
rule = `models.${strength} provider prefix must equal providerId`;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
models[strength] = spec;
|
|
230
|
+
}
|
|
231
|
+
if (rule !== undefined) {
|
|
232
|
+
invalid(rule);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out[routeId] = { family: route.family, credentialSource: route.credentialSource, providerId: route.providerId, models };
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
function mergeModelRouting(raw, base) {
|
|
240
|
+
if (raw === undefined)
|
|
241
|
+
return base;
|
|
242
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
243
|
+
return base;
|
|
244
|
+
const value = raw;
|
|
245
|
+
const strings = (items) => Array.isArray(items) && items.every((item) => typeof item === 'string' && item.trim() !== '') ? [...items] : undefined;
|
|
246
|
+
const strengths = Array.isArray(value.strengthFallback) && value.strengthFallback.every((item) => validStrength(item)) ? [...value.strengthFallback] : undefined;
|
|
247
|
+
return {
|
|
248
|
+
...(strings(value.credentialSourceOrder) !== undefined ? { credentialSourceOrder: strings(value.credentialSourceOrder) } : {}),
|
|
249
|
+
...(strings(value.familyOrder) !== undefined ? { familyOrder: strings(value.familyOrder) } : {}),
|
|
250
|
+
...(strengths !== undefined ? { strengthFallback: strengths } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
175
253
|
/** True when `pattern` (a `spawnEnv.allow` entry) matches EVERY possible env
|
|
176
254
|
* var name — a bare `*`, or any run of nothing but `*` characters (`**`,
|
|
177
255
|
* `***`, …). This is the one glob shape that reopens the inherit-all hole
|
|
@@ -362,10 +440,12 @@ function mergeConfig(partial) {
|
|
|
362
440
|
const fold_finished_tools = typeof partial.fold_finished_tools === 'boolean' ? partial.fold_finished_tools : defaults.fold_finished_tools;
|
|
363
441
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
364
442
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
443
|
+
const modelRoutes = mergeModelRoutes(partial.modelRoutes);
|
|
444
|
+
const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
|
|
365
445
|
const kinds = mergeKinds(partial.kinds);
|
|
366
446
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
367
447
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
368
|
-
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
448
|
+
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
369
449
|
}
|
|
370
450
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
371
451
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -450,18 +530,24 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
450
530
|
const projectRawsFarthestFirst = readRawProjectScopeConfigs(targetCwd, targetProfileId);
|
|
451
531
|
let kinds = mergeKinds(userRaw?.kinds, defaults.kinds);
|
|
452
532
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
533
|
+
let modelRoutes = mergeModelRoutes(userRaw?.modelRoutes);
|
|
534
|
+
let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
|
|
453
535
|
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
454
536
|
if (profileRaw !== null) {
|
|
455
537
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
456
538
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
539
|
+
modelRoutes = mergeModelRoutes(profileRaw.modelRoutes, modelRoutes);
|
|
540
|
+
modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
|
|
457
541
|
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
458
542
|
}
|
|
459
543
|
for (const raw of projectRawsFarthestFirst) {
|
|
460
544
|
kinds = mergeKinds(raw.kinds, kinds);
|
|
461
545
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
546
|
+
modelRoutes = mergeModelRoutes(raw.modelRoutes, modelRoutes);
|
|
547
|
+
modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
|
|
462
548
|
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
463
549
|
}
|
|
464
|
-
return { kinds, modelLadders, spawnEnv };
|
|
550
|
+
return { kinds, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), spawnEnv };
|
|
465
551
|
}
|
|
466
552
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
467
553
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
@@ -190,6 +190,8 @@ function materializeBodyValue(spec, param, value) {
|
|
|
190
190
|
if (param.kind === 'context-file')
|
|
191
191
|
return value;
|
|
192
192
|
if ((param.kind === 'positional' || param.kind === 'flag') && param.type === 'path') {
|
|
193
|
+
if (Array.isArray(value))
|
|
194
|
+
return value.map((item) => materializeBodyValue(spec, param, item));
|
|
193
195
|
const filePath = String(value);
|
|
194
196
|
const resolved = resolvePath(process.cwd(), filePath);
|
|
195
197
|
let stat;
|
package/dist/core/help.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface Field {
|
|
|
6
6
|
* token caps. Lives here, never in a separate Preconditions section. */
|
|
7
7
|
constraint: string;
|
|
8
8
|
}
|
|
9
|
-
/** Positional argument — at most one per leaf. */
|
|
9
|
+
/** Positional argument — at most one parameter per leaf; optionally repeatable. */
|
|
10
10
|
export interface PositionalParam {
|
|
11
11
|
kind: 'positional';
|
|
12
12
|
name: string;
|
|
@@ -14,6 +14,8 @@ export interface PositionalParam {
|
|
|
14
14
|
type?: 'string' | 'path';
|
|
15
15
|
required: boolean;
|
|
16
16
|
constraint: string;
|
|
17
|
+
/** When true, every positional token is collected into an array in argv order. */
|
|
18
|
+
repeatable?: boolean;
|
|
17
19
|
/** Local-file materialization contract, valid only on a `path` param. The
|
|
18
20
|
* CLI-side value is a path to a local file; the invoker reads that file and
|
|
19
21
|
* sends its CONTENT as the mapped value — 'text' as UTF-8, 'base64' as the
|
package/dist/core/help.js
CHANGED
|
@@ -175,7 +175,7 @@ export function renderBranch(h) {
|
|
|
175
175
|
/** Build the display label for a param entry (left column). */
|
|
176
176
|
function paramLabel(p) {
|
|
177
177
|
if (p.kind === 'positional')
|
|
178
|
-
return p.name.toUpperCase()
|
|
178
|
+
return `${p.name.toUpperCase()}${p.repeatable === true ? '...' : ''}`;
|
|
179
179
|
if (p.kind === 'stdin')
|
|
180
180
|
return 'stdin';
|
|
181
181
|
if (p.kind === 'context-file')
|
|
@@ -207,7 +207,10 @@ function envDefaultNote(envVar) {
|
|
|
207
207
|
function paramDesc(p) {
|
|
208
208
|
const req = p.required ? 'required' : 'optional';
|
|
209
209
|
if (p.kind === 'positional') {
|
|
210
|
-
|
|
210
|
+
const repeatable = p.repeatable === true
|
|
211
|
+
? ' Repeatable — pass multiple values to accumulate in argv order.'
|
|
212
|
+
: '';
|
|
213
|
+
return `positional, ${req}.${fileEncodingNote(p.encoding)}${envDefaultNote(p.defaultFromEnv)} ${p.constraint}${repeatable}`;
|
|
211
214
|
}
|
|
212
215
|
if (p.kind === 'stdin')
|
|
213
216
|
return `${req}. ${p.constraint}`;
|
|
@@ -13,9 +13,12 @@ void rejectedBindingId;
|
|
|
13
13
|
function ids(prefix, actions) {
|
|
14
14
|
return actions.map((action) => `${prefix}.${action}`);
|
|
15
15
|
}
|
|
16
|
+
function pairedMenuIds(actions) {
|
|
17
|
+
return actions.flatMap((action) => [`crtr.tmux.menu.${action}`, `crtr.tmux.menu-global.${action}`]);
|
|
18
|
+
}
|
|
16
19
|
const REQUIRED_IDS = [
|
|
17
|
-
|
|
18
|
-
...
|
|
20
|
+
'crtr.tmux.menu.open',
|
|
21
|
+
...pairedMenuIds(['promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'view-settings', 'fold-tools', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues', 'issue.idea', 'issue.bug']),
|
|
19
22
|
...ids('crtr.tmux.node', ['next', 'previous']),
|
|
20
23
|
'crtr.tmux.inbox.toggle',
|
|
21
24
|
...ids('crtr.tmux.view', ['next', 'previous']),
|