@zenera/cli 1.1.28 → 1.1.29
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/README.md +73 -24
- package/dist/audit.d.ts +2 -2
- package/dist/audit.js +11 -4
- package/dist/candidates.d.ts +9 -0
- package/dist/candidates.js +17 -0
- package/dist/catalog.d.ts +10 -5
- package/dist/catalog.js +93 -12
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.js +70 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/key.js +142 -11
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +134 -29
- package/dist/commands/models.js +56 -98
- package/dist/completion-install.d.ts +16 -0
- package/dist/completion-install.js +85 -0
- package/dist/completion.d.ts +9 -0
- package/dist/completion.js +93 -0
- package/dist/keys.d.ts +27 -4
- package/dist/keys.js +63 -3
- package/dist/liveness.js +73 -3
- package/dist/main.js +22 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.js +10 -0
- package/dist/sandbox.js +4 -3
- package/dist/term.d.ts +2 -0
- package/dist/term.js +2 -0
- package/dist/tui/links.d.ts +2 -0
- package/dist/tui/links.js +31 -7
- package/dist/tui/models.d.ts +5 -0
- package/dist/tui/models.js +21 -14
- package/dist/validate.js +2 -2
- package/package.json +8 -6
- package/postinstall.mjs +7 -0
- package/templates/completions/zen.bash +16 -0
- package/templates/completions/zen.fish +22 -0
- package/templates/completions/zen.zsh +28 -0
- package/templates/editor/.github/copilot-instructions.md +5 -4
- package/templates/editor/.github/prompts/spec-edit.prompt.md +250 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/keys.md +27 -2
- package/templates/editor/.github/skills/zen-cli/references/models.md +22 -23
package/dist/commands/key.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { GATEWAY_URL_ENV } from '@zenera/neo';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { parse } from "../args.js";
|
|
3
4
|
import { ensureHome } from "../home.js";
|
|
4
|
-
import { ambient, ambientId, assertNotEmpty, checkRegion, describe, envNames, envOf, formOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
5
|
+
import { ambient, ambientId, assertNotEmpty, checkRegion, describe, envNames, envOf, formOf, GATEWAYS, isGateway, keyId, KeyStore, mask, MODES, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
5
6
|
import { probe, probeAll } from "../liveness.js";
|
|
6
|
-
import { ago, ask, askSecret, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
7
|
-
const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
|
|
7
|
+
import { ago, ask, askSecret, badge, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
8
|
+
const USAGE = 'zen key <ls|add|use|mode|check|rm|show|env> [ref] [options]';
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Display
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -32,6 +33,9 @@ const ORIGIN = {
|
|
|
32
33
|
* the environment supplied either, because then the stored one is not in play.
|
|
33
34
|
*/
|
|
34
35
|
function placed(store, entry) {
|
|
36
|
+
if (entry.url) {
|
|
37
|
+
return dim(entry.url);
|
|
38
|
+
}
|
|
35
39
|
if (entry.provider !== 'vertex' || entry.holds !== 'file') {
|
|
36
40
|
return '';
|
|
37
41
|
}
|
|
@@ -61,14 +65,28 @@ function asEntry(cred) {
|
|
|
61
65
|
}
|
|
62
66
|
function rows(store, borrowed) {
|
|
63
67
|
const out = [
|
|
64
|
-
[bold(''), bold('KEY'), bold('VALUE'), bold('
|
|
68
|
+
[bold(''), bold('KEY'), bold('VALUE'), bold('WHERE'), bold('STATE'), bold('CHECKED')],
|
|
65
69
|
];
|
|
66
70
|
for (const provider of OWNERS) {
|
|
71
|
+
// Held but not in play under the current mode: drawn grey, so the live set stands out.
|
|
72
|
+
const idle = unused(store, provider);
|
|
67
73
|
for (const entry of store.for(provider)) {
|
|
68
74
|
const shadow = envNames(provider).find((name) => process.env[name]);
|
|
75
|
+
if (idle) {
|
|
76
|
+
out.push([
|
|
77
|
+
dim('-'),
|
|
78
|
+
dim(keyId(entry)),
|
|
79
|
+
dim(describe(store, entry)),
|
|
80
|
+
dim(entry.url ?? ''),
|
|
81
|
+
dim(entry.check?.state ?? 'unchecked'),
|
|
82
|
+
dim(`not used in ${store.mode} mode`),
|
|
83
|
+
]);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const gateway = isGateway(provider);
|
|
69
87
|
out.push([
|
|
70
88
|
store.isActive(entry) ? green('*') : ' ',
|
|
71
|
-
keyId(entry),
|
|
89
|
+
gateway && store.isActive(entry) ? bold(cyan(keyId(entry))) : keyId(entry),
|
|
72
90
|
dim(describe(store, entry)),
|
|
73
91
|
placed(store, entry),
|
|
74
92
|
state(entry),
|
|
@@ -78,17 +96,47 @@ function rows(store, borrowed) {
|
|
|
78
96
|
}
|
|
79
97
|
for (const [cred, entry] of borrowed.filter(([c]) => c.provider === provider)) {
|
|
80
98
|
out.push([
|
|
81
|
-
dim('~'),
|
|
99
|
+
dim(idle ? '-' : '~'),
|
|
82
100
|
dim(ambientId(cred)),
|
|
83
101
|
dim(describe(store, entry)),
|
|
84
|
-
placed(store, entry),
|
|
85
|
-
state(entry),
|
|
86
|
-
dim(
|
|
102
|
+
idle ? '' : placed(store, entry),
|
|
103
|
+
idle ? dim(entry.check?.state ?? 'unchecked') : state(entry),
|
|
104
|
+
dim(idle
|
|
105
|
+
? `not used in ${store.mode} mode`
|
|
106
|
+
: cred.env
|
|
107
|
+
? 'from the environment'
|
|
108
|
+
: 'from gcloud'),
|
|
87
109
|
]);
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
return table(out);
|
|
91
113
|
}
|
|
114
|
+
/** A vendor key under a gateway mode, or a gateway key under any other. */
|
|
115
|
+
function unused(store, provider) {
|
|
116
|
+
if (isGateway(provider)) {
|
|
117
|
+
return store.mode !== provider;
|
|
118
|
+
}
|
|
119
|
+
return SHAPES[provider].kind === 'model' && store.mode !== 'direct';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Above the table, because it decides which half of the table matters. Said
|
|
123
|
+
* only once a gateway key exists: before that there is no choice to report.
|
|
124
|
+
*/
|
|
125
|
+
function modeLine(store) {
|
|
126
|
+
const mode = store.mode;
|
|
127
|
+
if (mode === 'direct') {
|
|
128
|
+
if (GATEWAYS.some((g) => store.active(g))) {
|
|
129
|
+
note(`${bold('mode')} ${cyan('direct')} ${dim('— each provider with its own key; zen key mode bifrost to send every model through the gateway')}`);
|
|
130
|
+
note('');
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const gateway = store.active(mode);
|
|
135
|
+
note(`${cyan(badge(`⇄ ${mode.toUpperCase()} MODE`))} ${bold(gateway?.url ?? 'no url stored')}`);
|
|
136
|
+
note(dim(`every model goes through the gateway with ${gateway ? keyId(gateway) : 'no key'}; ` +
|
|
137
|
+
'vendor keys are not used — zen key mode direct to stop'));
|
|
138
|
+
note('');
|
|
139
|
+
}
|
|
92
140
|
/**
|
|
93
141
|
* Checking is a network round trip per key, so it says which ones have come
|
|
94
142
|
* back. Silence for ten seconds is indistinguishable from a hang.
|
|
@@ -123,6 +171,7 @@ const ls = async (ctx, args) => {
|
|
|
123
171
|
}
|
|
124
172
|
if (ctx.json) {
|
|
125
173
|
json({
|
|
174
|
+
mode: store.mode,
|
|
126
175
|
keys: store.entries.map((e) => ({
|
|
127
176
|
...e,
|
|
128
177
|
value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
|
|
@@ -145,8 +194,12 @@ const ls = async (ctx, args) => {
|
|
|
145
194
|
note(dim('add one: zen key add openai'));
|
|
146
195
|
return;
|
|
147
196
|
}
|
|
197
|
+
modeLine(store);
|
|
148
198
|
writeAll(rows(store, borrowed));
|
|
149
|
-
const missing = OWNERS.filter((p) => !store.active(p) &&
|
|
199
|
+
const missing = OWNERS.filter((p) => !store.active(p) &&
|
|
200
|
+
!borrowed.some(([c]) => c.provider === p) &&
|
|
201
|
+
(!isGateway(p) || store.mode === p) &&
|
|
202
|
+
(store.mode === 'direct' || SHAPES[p].kind !== 'model'));
|
|
150
203
|
if (missing.length) {
|
|
151
204
|
note('');
|
|
152
205
|
note(dim(`no key for: ${missing.join(', ')}`));
|
|
@@ -164,8 +217,9 @@ const add = async (ctx, args) => {
|
|
|
164
217
|
// Prefixed because every other command's --project is a Zenera one.
|
|
165
218
|
'gcp-project': { type: 'string' },
|
|
166
219
|
'gcp-location': { type: 'string' },
|
|
220
|
+
url: { type: 'string' },
|
|
167
221
|
}, 'zen key add <provider>[/name] [--name <name>] [--gcp-project <id>] ' +
|
|
168
|
-
'[--gcp-location <region>] [--no-check]');
|
|
222
|
+
'[--gcp-location <region>] [--url <gateway url>] [--no-check]');
|
|
169
223
|
const ref = positionals[0];
|
|
170
224
|
if (!ref) {
|
|
171
225
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -177,6 +231,7 @@ const add = async (ctx, args) => {
|
|
|
177
231
|
if ((values['gcp-project'] || values['gcp-location']) && provider !== 'vertex') {
|
|
178
232
|
throw usageError(`--gcp-project and --gcp-location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
179
233
|
}
|
|
234
|
+
const url = gatewayUrl(provider, values.url);
|
|
180
235
|
// Checked before the prompt, so a typo does not cost you pasting the key.
|
|
181
236
|
const region = values['gcp-location'];
|
|
182
237
|
if (region !== undefined && !checkRegion(region)) {
|
|
@@ -208,6 +263,7 @@ const add = async (ctx, args) => {
|
|
|
208
263
|
const entry = store.add(provider, name, raw, {
|
|
209
264
|
project: values['gcp-project'],
|
|
210
265
|
location: values['gcp-location'],
|
|
266
|
+
url,
|
|
211
267
|
});
|
|
212
268
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
213
269
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
@@ -249,6 +305,9 @@ const add = async (ctx, args) => {
|
|
|
249
305
|
if (process.env[envOf(entry)]) {
|
|
250
306
|
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
251
307
|
}
|
|
308
|
+
if (isGateway(provider) && store.mode !== provider) {
|
|
309
|
+
note(dim(`not in use yet — send every model through it with: zen key mode ${provider}`));
|
|
310
|
+
}
|
|
252
311
|
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
253
312
|
const found = store.projectOf(entry);
|
|
254
313
|
note(dim(found
|
|
@@ -256,6 +315,36 @@ const add = async (ctx, args) => {
|
|
|
256
315
|
: 'no --gcp-project given, and the key file names no project_id'));
|
|
257
316
|
}
|
|
258
317
|
};
|
|
318
|
+
/**
|
|
319
|
+
* A gateway is nothing without its address, and nothing else has one. Only
|
|
320
|
+
* http(s) is taken: the url ends up as an SDK base url, where anything else
|
|
321
|
+
* fails three frames down.
|
|
322
|
+
*/
|
|
323
|
+
function gatewayUrl(provider, raw) {
|
|
324
|
+
if (!isGateway(provider)) {
|
|
325
|
+
if (raw !== undefined) {
|
|
326
|
+
throw usageError(`--url means nothing to ${SHAPES[provider].label}`, 'it locates a gateway');
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
if (!raw) {
|
|
331
|
+
throw usageError(`${SHAPES[provider].label} needs --url`, `e.g. zen key add ${provider} --url https://bifrost.example.com`);
|
|
332
|
+
}
|
|
333
|
+
let url;
|
|
334
|
+
try {
|
|
335
|
+
url = new URL(raw);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
throw usageError(`"${raw}" is not a url`);
|
|
339
|
+
}
|
|
340
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
341
|
+
throw usageError(`"${raw}" is not an http(s) url`);
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
|
|
344
|
+
note(yellow('plain http: the virtual key will cross the network unencrypted'));
|
|
345
|
+
}
|
|
346
|
+
return url.href.replace(/\/+$/, '');
|
|
347
|
+
}
|
|
259
348
|
/**
|
|
260
349
|
* A provider with two forms cannot ask for one of them. It asks with the echo
|
|
261
350
|
* off, because one of the two answers is a secret and there is no way to know
|
|
@@ -294,6 +383,31 @@ const use = async (ctx, args) => {
|
|
|
294
383
|
}
|
|
295
384
|
note(`${green('using')} ${bold(keyId(entry))} for ${SHAPES[provider].label}`);
|
|
296
385
|
};
|
|
386
|
+
/** Bare, it says which mode a run would use; with a word, it switches. */
|
|
387
|
+
const mode = async (ctx, args) => {
|
|
388
|
+
const { positionals } = parse(args, {}, `zen key mode [${MODES.join('|')}]`);
|
|
389
|
+
const wanted = positionals[0];
|
|
390
|
+
const store = await KeyStore.open();
|
|
391
|
+
if (wanted !== undefined) {
|
|
392
|
+
if (!MODES.includes(wanted)) {
|
|
393
|
+
throw usageError(`unknown mode "${wanted}"`, `one of: ${MODES.join(', ')}`);
|
|
394
|
+
}
|
|
395
|
+
store.setMode(wanted);
|
|
396
|
+
store.save();
|
|
397
|
+
}
|
|
398
|
+
const now = store.mode;
|
|
399
|
+
const gateway = now === 'direct' ? undefined : store.active(now);
|
|
400
|
+
if (ctx.json) {
|
|
401
|
+
json({ mode: now, ...(gateway ? { key: keyId(gateway), url: gateway.url } : {}) });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!gateway) {
|
|
405
|
+
note(`${cyan('direct')} ${dim('— each provider is called with its own key')}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
note(`${cyan(now)} ${dim('— every model goes through')} ${gateway.url} ` +
|
|
409
|
+
dim(`with ${keyId(gateway)}`));
|
|
410
|
+
};
|
|
297
411
|
const check = async (ctx, args) => {
|
|
298
412
|
const { positionals } = parse(args, {}, 'zen key check [provider[/name]]');
|
|
299
413
|
const store = await KeyStore.open();
|
|
@@ -404,6 +518,7 @@ const show = async (ctx, args) => {
|
|
|
404
518
|
? [[dim('gcp project'), `${project.id} ${dim(ORIGIN[project.from])}`]]
|
|
405
519
|
: []),
|
|
406
520
|
...(entry.location ? [[dim('gcp location'), entry.location]] : []),
|
|
521
|
+
...(entry.url ? [[dim('url'), entry.url]] : []),
|
|
407
522
|
[dim('state'), state(entry)],
|
|
408
523
|
...(entry.check?.fix ? [[dim('fix'), entry.check.fix]] : []),
|
|
409
524
|
[dim('added'), ago(entry.addedAt)],
|
|
@@ -424,11 +539,18 @@ const env = async (ctx, args) => {
|
|
|
424
539
|
// Ignore what is already exported: the point is to produce the exports.
|
|
425
540
|
const vars = {};
|
|
426
541
|
for (const provider of only ?? OWNERS) {
|
|
542
|
+
// Unasked-for, a gateway is exported only in its own mode, where it is the switch.
|
|
543
|
+
if (!only && isGateway(provider) && store.mode !== provider) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
427
546
|
const entry = store.active(provider);
|
|
428
547
|
if (!entry) {
|
|
429
548
|
continue;
|
|
430
549
|
}
|
|
431
550
|
vars[envOf(entry)] = store.reveal(entry);
|
|
551
|
+
if (entry.url) {
|
|
552
|
+
vars[GATEWAY_URL_ENV] = entry.url;
|
|
553
|
+
}
|
|
432
554
|
// A service account says which project it belongs to but not which
|
|
433
555
|
// region to call, and a shell that has the file and not the region is
|
|
434
556
|
// still a shell that cannot reach a model.
|
|
@@ -498,6 +620,7 @@ const SUBS = {
|
|
|
498
620
|
list: ls,
|
|
499
621
|
add,
|
|
500
622
|
use,
|
|
623
|
+
mode,
|
|
501
624
|
check,
|
|
502
625
|
rm,
|
|
503
626
|
remove: rm,
|
|
@@ -516,14 +639,22 @@ export const key = {
|
|
|
516
639
|
'',
|
|
517
640
|
'Model providers: openai, anthropic, google, vertex, openrouter.',
|
|
518
641
|
'Services the tools call: exa.',
|
|
642
|
+
`Gateways every model can go through: ${GATEWAYS.join(', ')}.`,
|
|
519
643
|
'',
|
|
520
644
|
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
521
645
|
'--gcp-project and --gcp-location too, or an express-mode API key, which',
|
|
522
646
|
'wants neither. Which one you gave is read off the value.',
|
|
523
647
|
'',
|
|
648
|
+
'A gateway holds one virtual key and its --url. In its mode every model',
|
|
649
|
+
'goes through it, each in its own vendor protocol, and agents.yaml does',
|
|
650
|
+
'not change: vertex:gemini-3.5-flash reaches <url>/genai as',
|
|
651
|
+
'vertex/gemini-3.5-flash. Vendor keys are withheld from runs meanwhile.',
|
|
652
|
+
'',
|
|
524
653
|
' zen key ls [--check] Everything stored, and its state.',
|
|
525
654
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
|
655
|
+
' zen key add bifrost --url <url> A gateway key, and where the gateway is.',
|
|
526
656
|
' zen key use <provider>/<name> Choose which one a run uses.',
|
|
657
|
+
' zen key mode [direct|bifrost] Call vendors directly, or via the gateway.',
|
|
527
658
|
' zen key check [provider[/name]] Ask the provider whether it still works.',
|
|
528
659
|
' zen key rm <provider>/<name> Forget one.',
|
|
529
660
|
' zen key show <ref> [--reveal] Masked by default.',
|
package/dist/commands/meta.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type Candidate } from '../candidates.ts';
|
|
1
2
|
import type { Command } from '../command.ts';
|
|
2
3
|
export declare const meta: Command;
|
|
4
|
+
/** `args` is what came after `meta`, without the word being completed. */
|
|
5
|
+
export declare function completeMeta(args: readonly string[], cur: string, cwd: string): Promise<Candidate[]>;
|
|
3
6
|
//# sourceMappingURL=meta.d.ts.map
|
package/dist/commands/meta.js
CHANGED
|
@@ -5,10 +5,11 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
7
|
import { parse } from "../args.js";
|
|
8
|
+
import { matching, projectNames } from "../candidates.js";
|
|
8
9
|
import { loadProjectEnv } from "../env.js";
|
|
9
10
|
import { copilotEventLog } from "../eventlog.js";
|
|
10
11
|
import { ensureHome } from "../home.js";
|
|
11
|
-
import { assertOwner, KeyStore, PROVIDERS } from "../keys.js";
|
|
12
|
+
import { assertOwner, isProvider, KeyStore, PROVIDERS } from "../keys.js";
|
|
12
13
|
import { probeChat, probeModel } from "../liveness.js";
|
|
13
14
|
import { answerBox, chooseModel, DATASET_DETAILS, DATASET_SUMMARY, DATASET_USAGE, datasetProjectFlag, DEFAULT_RESUMES, defaultRef, editorFiles, FINETUNE_DETAILS, FINETUNE_SUMMARY, FINETUNE_USAGE, finetuneProjectFlag, lastSession, launch, listPrompts, loadPrompt, locate, masked, misspelledProvider, MODEL_ENV, openLog, ORDER, PROMPT_DIR, providersWarning, readMeta, recordSession, RESUME_PROMPT, resumeDelayMs, runDataset, runFinetune, sameModel, sessionModel, sessionTotals, SOURCE_LABELS, startVertexRelay, tailSpans, Tally, terminalSink, transient, wire, writeMeta, } from "../meta/index.js";
|
|
14
15
|
import { splitRef } from "../modelref.js";
|
|
@@ -57,6 +58,32 @@ function page(p) {
|
|
|
57
58
|
write(line ? ` ${line}` : '');
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/** Kept apart from `run` so that shell completion offers exactly what is parsed. */
|
|
62
|
+
const OPTIONS = {
|
|
63
|
+
project: { type: 'string' },
|
|
64
|
+
provider: { type: 'string' },
|
|
65
|
+
model: { type: 'string' },
|
|
66
|
+
prompt: { type: 'string', short: 'p' },
|
|
67
|
+
'prompt-file': { type: 'string' },
|
|
68
|
+
agent: { type: 'string' },
|
|
69
|
+
effort: { type: 'string' },
|
|
70
|
+
share: { type: 'string' },
|
|
71
|
+
events: { type: 'string' },
|
|
72
|
+
resume: { type: 'string' },
|
|
73
|
+
continue: { type: 'boolean' },
|
|
74
|
+
'session-id': { type: 'string' },
|
|
75
|
+
retries: { type: 'string' },
|
|
76
|
+
'allow-all': { type: 'boolean' },
|
|
77
|
+
'allow-tool': { type: 'string' },
|
|
78
|
+
ask: { type: 'boolean' },
|
|
79
|
+
'add-dir': { type: 'string', multiple: true },
|
|
80
|
+
'dry-run': { type: 'boolean' },
|
|
81
|
+
local: { type: 'boolean' },
|
|
82
|
+
pick: { type: 'boolean' },
|
|
83
|
+
clear: { type: 'boolean' },
|
|
84
|
+
force: { type: 'boolean' },
|
|
85
|
+
'no-refresh': { type: 'boolean' },
|
|
86
|
+
};
|
|
60
87
|
const RUN_FORMS = [
|
|
61
88
|
' zen meta run [project] "<question>" ask it something',
|
|
62
89
|
' zen meta run [project] /<name> [words] run a stored prompt',
|
|
@@ -65,7 +92,8 @@ const RUN_FORMS = [
|
|
|
65
92
|
const RUN_ARGUMENTS = [
|
|
66
93
|
'Arguments:',
|
|
67
94
|
' [project] Name of a project. Default: the one you are in.',
|
|
68
|
-
' [prompt] Your question, in quotes.
|
|
95
|
+
' [prompt] Your question, in quotes. Or --prompt, or piped in when neither',
|
|
96
|
+
' it nor --prompt-file is given.',
|
|
69
97
|
' /<name> A prompt file in .github/prompts/<name>.prompt.md.',
|
|
70
98
|
' [words] Appended to that prompt as a final line.',
|
|
71
99
|
];
|
|
@@ -74,7 +102,10 @@ const RUN_OPTIONS = [
|
|
|
74
102
|
' --project <name|dir> Which project to work in. The agent is rooted there.',
|
|
75
103
|
' --model <ref> Model for this run, e.g. vertex/gemini-3.8-flash.',
|
|
76
104
|
' --provider <name> Force the key to use. Default: the model ref says.',
|
|
77
|
-
' --prompt
|
|
105
|
+
' -p, --prompt <text> The question, in place of [prompt].',
|
|
106
|
+
' Ignored with /<name>: put words after it instead.',
|
|
107
|
+
' --prompt-file <file> Added after the question - or after /<name> and its',
|
|
108
|
+
' words - past a blank line. Alone, it is the question.',
|
|
78
109
|
' --agent <name> A .github/agents/<name>.agent.md.',
|
|
79
110
|
' --effort <level> none, minimal, low, medium, high, xhigh or max.',
|
|
80
111
|
' --allow-tool <list> Only these: read, shell(git:*), write(README.md).',
|
|
@@ -182,31 +213,7 @@ export const meta = {
|
|
|
182
213
|
});
|
|
183
214
|
return await run({ args, json: ctx.json, cwd: ctx.cwd }, found);
|
|
184
215
|
}
|
|
185
|
-
const { values, positionals } = parse(ctx.args,
|
|
186
|
-
project: { type: 'string' },
|
|
187
|
-
provider: { type: 'string' },
|
|
188
|
-
model: { type: 'string' },
|
|
189
|
-
prompt: { type: 'string', short: 'p' },
|
|
190
|
-
'prompt-file': { type: 'string' },
|
|
191
|
-
agent: { type: 'string' },
|
|
192
|
-
effort: { type: 'string' },
|
|
193
|
-
share: { type: 'string' },
|
|
194
|
-
events: { type: 'string' },
|
|
195
|
-
resume: { type: 'string' },
|
|
196
|
-
continue: { type: 'boolean' },
|
|
197
|
-
'session-id': { type: 'string' },
|
|
198
|
-
retries: { type: 'string' },
|
|
199
|
-
'allow-all': { type: 'boolean' },
|
|
200
|
-
'allow-tool': { type: 'string' },
|
|
201
|
-
ask: { type: 'boolean' },
|
|
202
|
-
'add-dir': { type: 'string', multiple: true },
|
|
203
|
-
'dry-run': { type: 'boolean' },
|
|
204
|
-
local: { type: 'boolean' },
|
|
205
|
-
pick: { type: 'boolean' },
|
|
206
|
-
clear: { type: 'boolean' },
|
|
207
|
-
force: { type: 'boolean' },
|
|
208
|
-
'no-refresh': { type: 'boolean' },
|
|
209
|
-
}, USAGE);
|
|
216
|
+
const { values, positionals } = parse(ctx.args, OPTIONS, USAGE);
|
|
210
217
|
// `zen meta acme run` is typed at least as often as `zen meta run acme`,
|
|
211
218
|
// and read literally it asks the agent the one-word question "run" —
|
|
212
219
|
// a whole model call spent on a typo, with nothing on screen to say so.
|
|
@@ -388,6 +395,104 @@ async function pickPrompt(ctx, project) {
|
|
|
388
395
|
return await choose('Which prompt?', names.map((n) => ({ label: `/${n}`, value: n })));
|
|
389
396
|
}
|
|
390
397
|
// ---------------------------------------------------------------------------
|
|
398
|
+
// Completing it
|
|
399
|
+
//
|
|
400
|
+
// `zen meta run acme /<Tab>` is where a terminal can show what an editor's menu
|
|
401
|
+
// shows. It reads the line the way `stored` does — a leading slash is a prompt,
|
|
402
|
+
// a bare word that names a project is the project — and it only ever reads:
|
|
403
|
+
// the editor files are not refreshed for the sake of a Tab.
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
async function findProject(name) {
|
|
406
|
+
try {
|
|
407
|
+
return await Projects.find(name);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function promptsOf(project) {
|
|
414
|
+
if (!project) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
const names = await listPrompts(project.dir);
|
|
418
|
+
return await Promise.all(names.map(async (name) => ({
|
|
419
|
+
value: `/${name}`,
|
|
420
|
+
description: (await loadPrompt(project.dir, name).catch(() => undefined))?.description,
|
|
421
|
+
})));
|
|
422
|
+
}
|
|
423
|
+
/** `args` is what came after `meta`, without the word being completed. */
|
|
424
|
+
export async function completeMeta(args, cur, cwd) {
|
|
425
|
+
const words = [];
|
|
426
|
+
let flagged;
|
|
427
|
+
let waiting;
|
|
428
|
+
for (const arg of args) {
|
|
429
|
+
if (waiting) {
|
|
430
|
+
if (waiting === 'project') {
|
|
431
|
+
flagged = arg;
|
|
432
|
+
}
|
|
433
|
+
waiting = undefined;
|
|
434
|
+
}
|
|
435
|
+
else if (arg.startsWith('--')) {
|
|
436
|
+
const [flag, value] = arg.slice(2).split(/=(.*)/s);
|
|
437
|
+
if (value !== undefined) {
|
|
438
|
+
flagged = flag === 'project' ? value : flagged;
|
|
439
|
+
}
|
|
440
|
+
else if (OPTIONS[flag]?.type === 'string') {
|
|
441
|
+
waiting = flag;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (arg === '-p') {
|
|
445
|
+
waiting = 'prompt';
|
|
446
|
+
}
|
|
447
|
+
else if (!arg.startsWith('-')) {
|
|
448
|
+
words.push(arg);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// A flag's value is the flag's to name, and most of them are free text or a
|
|
452
|
+
// path — which is the shell's.
|
|
453
|
+
if (waiting === 'provider') {
|
|
454
|
+
return matching(PROVIDERS.map((value) => ({ value })), cur);
|
|
455
|
+
}
|
|
456
|
+
if (waiting === 'project') {
|
|
457
|
+
return matching(await projectNames(), cur);
|
|
458
|
+
}
|
|
459
|
+
if (waiting) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
const at = words.findIndex((w) => VERBS.has(w));
|
|
463
|
+
const named = at === 1 ? words[0] : undefined;
|
|
464
|
+
const verb = at === 0 || (named && (await findProject(named))) ? words[at] : undefined;
|
|
465
|
+
if (cur.startsWith('-')) {
|
|
466
|
+
return verb === 'dataset' || verb === 'finetune'
|
|
467
|
+
? []
|
|
468
|
+
: matching(Object.keys(OPTIONS).map((flag) => ({ value: `--${flag}` })), cur);
|
|
469
|
+
}
|
|
470
|
+
if (!verb) {
|
|
471
|
+
const verbs = [...VERBS].map((value) => ({ value }));
|
|
472
|
+
if (words.length === 0) {
|
|
473
|
+
return matching([...verbs, ...(await projectNames())], cur);
|
|
474
|
+
}
|
|
475
|
+
return words.length === 1 && (await findProject(words[0])) ? matching(verbs, cur) : [];
|
|
476
|
+
}
|
|
477
|
+
const rest = words.slice(at + 1);
|
|
478
|
+
if (verb === 'prompts' || verb === 'resume') {
|
|
479
|
+
return flagged || named || rest.length > 0 ? [] : matching(await projectNames(), cur);
|
|
480
|
+
}
|
|
481
|
+
if (verb !== 'run' || rest.some((w) => w.startsWith('/'))) {
|
|
482
|
+
// Past the prompt's name, what follows is files and words.
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
const given = flagged ?? named ?? rest[0];
|
|
486
|
+
if (given === undefined) {
|
|
487
|
+
const here = await Projects.current(cwd).catch(() => undefined);
|
|
488
|
+
return matching([...(await promptsOf(here)), ...(await projectNames())], cur);
|
|
489
|
+
}
|
|
490
|
+
const project = await findProject(given);
|
|
491
|
+
const fromRest = !flagged && !named;
|
|
492
|
+
const free = rest.length - (fromRest && project ? 1 : 0);
|
|
493
|
+
return free > 0 ? [] : matching(await promptsOf(project), cur);
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
391
496
|
// zen meta prompts [project]
|
|
392
497
|
//
|
|
393
498
|
// A slash command is only discoverable inside an editor, where a menu drops
|
|
@@ -789,7 +894,7 @@ function count(text, flag) {
|
|
|
789
894
|
}
|
|
790
895
|
function asProvider(name) {
|
|
791
896
|
const owner = assertOwner(name);
|
|
792
|
-
if (owner
|
|
897
|
+
if (!isProvider(owner)) {
|
|
793
898
|
throw usageError(`${name} is not a model provider`);
|
|
794
899
|
}
|
|
795
900
|
return owner;
|