@zenera/cli 1.1.28 → 1.1.30
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 +181 -17
- 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 +29 -4
- package/dist/keys.js +67 -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 +29 -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, warn, 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
|
}
|
|
@@ -50,25 +54,52 @@ function placed(store, entry) {
|
|
|
50
54
|
* intent — it can be listed and probed, not chosen or forgotten.
|
|
51
55
|
*/
|
|
52
56
|
function asEntry(cred) {
|
|
57
|
+
const url = isGateway(cred.provider) ? envUrl() : undefined;
|
|
53
58
|
return {
|
|
54
59
|
provider: cred.provider,
|
|
55
60
|
name: cred.env ? `$${cred.env}` : 'adc',
|
|
56
61
|
holds: cred.holds,
|
|
57
62
|
value: cred.value,
|
|
58
63
|
env: cred.env,
|
|
64
|
+
...(url ? { url } : {}),
|
|
59
65
|
addedAt: new Date().toISOString(),
|
|
60
66
|
};
|
|
61
67
|
}
|
|
68
|
+
const envUrl = () => process.env[GATEWAY_URL_ENV]?.trim() || undefined;
|
|
69
|
+
/** Where a gateway mode sends runs, and with which key: the environment beats the keyring. */
|
|
70
|
+
function route(store, mode) {
|
|
71
|
+
const entry = store.active(mode);
|
|
72
|
+
const shell = envNames(mode).find((name) => process.env[name]);
|
|
73
|
+
return {
|
|
74
|
+
url: envUrl() ?? entry?.url,
|
|
75
|
+
key: shell ? `$${shell}` : entry ? keyId(entry) : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const FROM_ENV = `set by $${GATEWAY_URL_ENV} in the environment`;
|
|
62
79
|
function rows(store, borrowed) {
|
|
63
80
|
const out = [
|
|
64
|
-
[bold(''), bold('KEY'), bold('VALUE'), bold('
|
|
81
|
+
[bold(''), bold('KEY'), bold('VALUE'), bold('WHERE'), bold('STATE'), bold('CHECKED')],
|
|
65
82
|
];
|
|
66
83
|
for (const provider of OWNERS) {
|
|
84
|
+
// Held but not in play under the current mode: drawn grey, so the live set stands out.
|
|
85
|
+
const idle = unused(store, provider);
|
|
67
86
|
for (const entry of store.for(provider)) {
|
|
68
87
|
const shadow = envNames(provider).find((name) => process.env[name]);
|
|
88
|
+
if (idle) {
|
|
89
|
+
out.push([
|
|
90
|
+
dim('-'),
|
|
91
|
+
dim(keyId(entry)),
|
|
92
|
+
dim(describe(store, entry)),
|
|
93
|
+
dim(entry.url ?? ''),
|
|
94
|
+
dim(entry.check?.state ?? 'unchecked'),
|
|
95
|
+
dim(`not used in ${store.mode} mode`),
|
|
96
|
+
]);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const gateway = isGateway(provider);
|
|
69
100
|
out.push([
|
|
70
101
|
store.isActive(entry) ? green('*') : ' ',
|
|
71
|
-
keyId(entry),
|
|
102
|
+
gateway && store.isActive(entry) ? bold(cyan(keyId(entry))) : keyId(entry),
|
|
72
103
|
dim(describe(store, entry)),
|
|
73
104
|
placed(store, entry),
|
|
74
105
|
state(entry),
|
|
@@ -78,17 +109,49 @@ function rows(store, borrowed) {
|
|
|
78
109
|
}
|
|
79
110
|
for (const [cred, entry] of borrowed.filter(([c]) => c.provider === provider)) {
|
|
80
111
|
out.push([
|
|
81
|
-
dim('~'),
|
|
112
|
+
dim(idle ? '-' : '~'),
|
|
82
113
|
dim(ambientId(cred)),
|
|
83
114
|
dim(describe(store, entry)),
|
|
84
|
-
placed(store, entry),
|
|
85
|
-
state(entry),
|
|
86
|
-
dim(
|
|
115
|
+
idle ? '' : placed(store, entry),
|
|
116
|
+
idle ? dim(entry.check?.state ?? 'unchecked') : state(entry),
|
|
117
|
+
dim(idle
|
|
118
|
+
? `not used in ${store.mode} mode`
|
|
119
|
+
: cred.env
|
|
120
|
+
? 'from the environment'
|
|
121
|
+
: 'from gcloud'),
|
|
87
122
|
]);
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
125
|
return table(out);
|
|
91
126
|
}
|
|
127
|
+
/** A vendor key under a gateway mode, or a gateway key under any other. */
|
|
128
|
+
function unused(store, provider) {
|
|
129
|
+
if (isGateway(provider)) {
|
|
130
|
+
return store.mode !== provider;
|
|
131
|
+
}
|
|
132
|
+
return SHAPES[provider].kind === 'model' && store.mode !== 'direct';
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Above the table, because it decides which half of the table matters. Said
|
|
136
|
+
* only once a gateway key exists: before that there is no choice to report.
|
|
137
|
+
*/
|
|
138
|
+
function modeLine(store) {
|
|
139
|
+
const mode = store.mode;
|
|
140
|
+
if (mode === 'direct') {
|
|
141
|
+
if (GATEWAYS.some((g) => store.active(g))) {
|
|
142
|
+
note(`${bold('mode')} ${cyan('direct')} ${dim('— each provider with its own key; zen key mode bifrost to send every model through the gateway')}`);
|
|
143
|
+
note('');
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { url, key } = route(store, mode);
|
|
148
|
+
note(`${cyan(badge(`⇄ ${mode.toUpperCase()} MODE`))} ${bold(url ?? 'no url stored')}`);
|
|
149
|
+
note(dim(`every model goes through the gateway with ${key ?? 'no key'}; vendor keys are not used — ` +
|
|
150
|
+
(store.modeFrom === 'env'
|
|
151
|
+
? `${FROM_ENV}, unset it to stop`
|
|
152
|
+
: 'zen key mode direct to stop')));
|
|
153
|
+
note('');
|
|
154
|
+
}
|
|
92
155
|
/**
|
|
93
156
|
* Checking is a network round trip per key, so it says which ones have come
|
|
94
157
|
* back. Silence for ten seconds is indistinguishable from a hang.
|
|
@@ -123,6 +186,7 @@ const ls = async (ctx, args) => {
|
|
|
123
186
|
}
|
|
124
187
|
if (ctx.json) {
|
|
125
188
|
json({
|
|
189
|
+
mode: store.mode,
|
|
126
190
|
keys: store.entries.map((e) => ({
|
|
127
191
|
...e,
|
|
128
192
|
value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
|
|
@@ -140,13 +204,17 @@ const ls = async (ctx, args) => {
|
|
|
140
204
|
});
|
|
141
205
|
return;
|
|
142
206
|
}
|
|
143
|
-
if (store.entries.length === 0 && borrowed.length === 0) {
|
|
207
|
+
if (store.entries.length === 0 && borrowed.length === 0 && store.mode === 'direct') {
|
|
144
208
|
note('the keyring is empty');
|
|
145
209
|
note(dim('add one: zen key add openai'));
|
|
146
210
|
return;
|
|
147
211
|
}
|
|
212
|
+
modeLine(store);
|
|
148
213
|
writeAll(rows(store, borrowed));
|
|
149
|
-
const missing = OWNERS.filter((p) => !store.active(p) &&
|
|
214
|
+
const missing = OWNERS.filter((p) => !store.active(p) &&
|
|
215
|
+
!borrowed.some(([c]) => c.provider === p) &&
|
|
216
|
+
(!isGateway(p) || store.mode === p) &&
|
|
217
|
+
(store.mode === 'direct' || SHAPES[p].kind !== 'model'));
|
|
150
218
|
if (missing.length) {
|
|
151
219
|
note('');
|
|
152
220
|
note(dim(`no key for: ${missing.join(', ')}`));
|
|
@@ -164,8 +232,9 @@ const add = async (ctx, args) => {
|
|
|
164
232
|
// Prefixed because every other command's --project is a Zenera one.
|
|
165
233
|
'gcp-project': { type: 'string' },
|
|
166
234
|
'gcp-location': { type: 'string' },
|
|
235
|
+
url: { type: 'string' },
|
|
167
236
|
}, 'zen key add <provider>[/name] [--name <name>] [--gcp-project <id>] ' +
|
|
168
|
-
'[--gcp-location <region>] [--no-check]');
|
|
237
|
+
'[--gcp-location <region>] [--url <gateway url>] [--no-check]');
|
|
169
238
|
const ref = positionals[0];
|
|
170
239
|
if (!ref) {
|
|
171
240
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -177,6 +246,7 @@ const add = async (ctx, args) => {
|
|
|
177
246
|
if ((values['gcp-project'] || values['gcp-location']) && provider !== 'vertex') {
|
|
178
247
|
throw usageError(`--gcp-project and --gcp-location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
179
248
|
}
|
|
249
|
+
const url = gatewayUrl(provider, values.url);
|
|
180
250
|
// Checked before the prompt, so a typo does not cost you pasting the key.
|
|
181
251
|
const region = values['gcp-location'];
|
|
182
252
|
if (region !== undefined && !checkRegion(region)) {
|
|
@@ -208,6 +278,7 @@ const add = async (ctx, args) => {
|
|
|
208
278
|
const entry = store.add(provider, name, raw, {
|
|
209
279
|
project: values['gcp-project'],
|
|
210
280
|
location: values['gcp-location'],
|
|
281
|
+
url,
|
|
211
282
|
});
|
|
212
283
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
213
284
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
@@ -249,6 +320,9 @@ const add = async (ctx, args) => {
|
|
|
249
320
|
if (process.env[envOf(entry)]) {
|
|
250
321
|
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
251
322
|
}
|
|
323
|
+
if (isGateway(provider) && store.mode !== provider) {
|
|
324
|
+
note(dim(`not in use yet — send every model through it with: zen key mode ${provider}`));
|
|
325
|
+
}
|
|
252
326
|
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
253
327
|
const found = store.projectOf(entry);
|
|
254
328
|
note(dim(found
|
|
@@ -256,6 +330,36 @@ const add = async (ctx, args) => {
|
|
|
256
330
|
: 'no --gcp-project given, and the key file names no project_id'));
|
|
257
331
|
}
|
|
258
332
|
};
|
|
333
|
+
/**
|
|
334
|
+
* A gateway is nothing without its address, and nothing else has one. Only
|
|
335
|
+
* http(s) is taken: the url ends up as an SDK base url, where anything else
|
|
336
|
+
* fails three frames down.
|
|
337
|
+
*/
|
|
338
|
+
function gatewayUrl(provider, raw) {
|
|
339
|
+
if (!isGateway(provider)) {
|
|
340
|
+
if (raw !== undefined) {
|
|
341
|
+
throw usageError(`--url means nothing to ${SHAPES[provider].label}`, 'it locates a gateway');
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
if (!raw) {
|
|
346
|
+
throw usageError(`${SHAPES[provider].label} needs --url`, `e.g. zen key add ${provider} --url https://bifrost.example.com`);
|
|
347
|
+
}
|
|
348
|
+
let url;
|
|
349
|
+
try {
|
|
350
|
+
url = new URL(raw);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
throw usageError(`"${raw}" is not a url`);
|
|
354
|
+
}
|
|
355
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
356
|
+
throw usageError(`"${raw}" is not an http(s) url`);
|
|
357
|
+
}
|
|
358
|
+
if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
|
|
359
|
+
note(yellow('plain http: the virtual key will cross the network unencrypted'));
|
|
360
|
+
}
|
|
361
|
+
return url.href.replace(/\/+$/, '');
|
|
362
|
+
}
|
|
259
363
|
/**
|
|
260
364
|
* A provider with two forms cannot ask for one of them. It asks with the echo
|
|
261
365
|
* off, because one of the two answers is a secret and there is no way to know
|
|
@@ -294,11 +398,53 @@ const use = async (ctx, args) => {
|
|
|
294
398
|
}
|
|
295
399
|
note(`${green('using')} ${bold(keyId(entry))} for ${SHAPES[provider].label}`);
|
|
296
400
|
};
|
|
401
|
+
/** Bare, it says which mode a run would use; with a word, it switches. */
|
|
402
|
+
const mode = async (ctx, args) => {
|
|
403
|
+
const { positionals } = parse(args, {}, `zen key mode [${MODES.join('|')}]`);
|
|
404
|
+
const wanted = positionals[0];
|
|
405
|
+
const store = await KeyStore.open();
|
|
406
|
+
if (wanted !== undefined) {
|
|
407
|
+
if (!MODES.includes(wanted)) {
|
|
408
|
+
throw usageError(`unknown mode "${wanted}"`, `one of: ${MODES.join(', ')}`);
|
|
409
|
+
}
|
|
410
|
+
// Already on through the environment: storing it as well needs a gateway key of its own.
|
|
411
|
+
const viaEnv = store.modeFrom === 'env' && wanted === store.mode;
|
|
412
|
+
if (!viaEnv || store.active(store.mode)?.url) {
|
|
413
|
+
store.setMode(wanted);
|
|
414
|
+
store.save();
|
|
415
|
+
}
|
|
416
|
+
if (store.modeFrom === 'env' && wanted !== store.mode) {
|
|
417
|
+
warn(`saved, but ${FROM_ENV} wins: unset it to call vendors directly`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const now = store.mode;
|
|
421
|
+
const from = store.modeFrom;
|
|
422
|
+
const { url, key } = now === 'direct' ? {} : route(store, now);
|
|
423
|
+
if (ctx.json) {
|
|
424
|
+
json({ mode: now, from, ...(key ? { key } : {}), ...(url ? { url } : {}) });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (now === 'direct') {
|
|
428
|
+
note(`${cyan('direct')} ${dim('— each provider is called with its own key')}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
note(`${cyan(now)} ${dim('— every model goes through')} ${url ?? 'no url stored'} ` +
|
|
432
|
+
dim(`with ${key ?? 'no key'}`));
|
|
433
|
+
if (from === 'env') {
|
|
434
|
+
note(dim(FROM_ENV));
|
|
435
|
+
}
|
|
436
|
+
};
|
|
297
437
|
const check = async (ctx, args) => {
|
|
298
438
|
const { positionals } = parse(args, {}, 'zen key check [provider[/name]]');
|
|
299
439
|
const store = await KeyStore.open();
|
|
300
|
-
|
|
301
|
-
const
|
|
440
|
+
// Checked like stored keys, never recorded: `store.record` finds no entry for them.
|
|
441
|
+
const borrowed = ambient(store).map(asEntry);
|
|
442
|
+
const targets = positionals[0]
|
|
443
|
+
? select(store, positionals[0], borrowed)
|
|
444
|
+
: [...store.entries, ...borrowed];
|
|
445
|
+
if (targets.length === 0) {
|
|
446
|
+
assertNotEmpty(store);
|
|
447
|
+
}
|
|
302
448
|
const checks = await checkAll(ctx, store, targets);
|
|
303
449
|
for (const [entry, result] of checks) {
|
|
304
450
|
store.record(entry, result);
|
|
@@ -327,12 +473,13 @@ const check = async (ctx, args) => {
|
|
|
327
473
|
throw credentialError('at least one key authenticated but cannot be used');
|
|
328
474
|
}
|
|
329
475
|
};
|
|
330
|
-
function select(store, ref) {
|
|
476
|
+
function select(store, ref, borrowed = []) {
|
|
331
477
|
const { provider, name } = parseRef(ref);
|
|
332
478
|
if (!name) {
|
|
333
|
-
return store.for(provider);
|
|
479
|
+
return [...store.for(provider), ...borrowed.filter((e) => e.provider === provider)];
|
|
334
480
|
}
|
|
335
|
-
const entry = store.find(provider, name)
|
|
481
|
+
const entry = store.find(provider, name) ??
|
|
482
|
+
borrowed.find((e) => e.provider === provider && e.name === name);
|
|
336
483
|
if (!entry) {
|
|
337
484
|
throw usageError(`no key ${ref}`, 'see: zen key ls');
|
|
338
485
|
}
|
|
@@ -404,6 +551,7 @@ const show = async (ctx, args) => {
|
|
|
404
551
|
? [[dim('gcp project'), `${project.id} ${dim(ORIGIN[project.from])}`]]
|
|
405
552
|
: []),
|
|
406
553
|
...(entry.location ? [[dim('gcp location'), entry.location]] : []),
|
|
554
|
+
...(entry.url ? [[dim('url'), entry.url]] : []),
|
|
407
555
|
[dim('state'), state(entry)],
|
|
408
556
|
...(entry.check?.fix ? [[dim('fix'), entry.check.fix]] : []),
|
|
409
557
|
[dim('added'), ago(entry.addedAt)],
|
|
@@ -424,11 +572,18 @@ const env = async (ctx, args) => {
|
|
|
424
572
|
// Ignore what is already exported: the point is to produce the exports.
|
|
425
573
|
const vars = {};
|
|
426
574
|
for (const provider of only ?? OWNERS) {
|
|
575
|
+
// Unasked-for, a gateway is exported only in its own mode, where it is the switch.
|
|
576
|
+
if (!only && isGateway(provider) && store.mode !== provider) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
427
579
|
const entry = store.active(provider);
|
|
428
580
|
if (!entry) {
|
|
429
581
|
continue;
|
|
430
582
|
}
|
|
431
583
|
vars[envOf(entry)] = store.reveal(entry);
|
|
584
|
+
if (entry.url) {
|
|
585
|
+
vars[GATEWAY_URL_ENV] = entry.url;
|
|
586
|
+
}
|
|
432
587
|
// A service account says which project it belongs to but not which
|
|
433
588
|
// region to call, and a shell that has the file and not the region is
|
|
434
589
|
// still a shell that cannot reach a model.
|
|
@@ -498,6 +653,7 @@ const SUBS = {
|
|
|
498
653
|
list: ls,
|
|
499
654
|
add,
|
|
500
655
|
use,
|
|
656
|
+
mode,
|
|
501
657
|
check,
|
|
502
658
|
rm,
|
|
503
659
|
remove: rm,
|
|
@@ -516,14 +672,22 @@ export const key = {
|
|
|
516
672
|
'',
|
|
517
673
|
'Model providers: openai, anthropic, google, vertex, openrouter.',
|
|
518
674
|
'Services the tools call: exa.',
|
|
675
|
+
`Gateways every model can go through: ${GATEWAYS.join(', ')}.`,
|
|
519
676
|
'',
|
|
520
677
|
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
521
678
|
'--gcp-project and --gcp-location too, or an express-mode API key, which',
|
|
522
679
|
'wants neither. Which one you gave is read off the value.',
|
|
523
680
|
'',
|
|
681
|
+
'A gateway holds one virtual key and its --url. In its mode every model',
|
|
682
|
+
'goes through it, each in its own vendor protocol, and agents.yaml does',
|
|
683
|
+
'not change: vertex:gemini-3.5-flash reaches <url>/genai as',
|
|
684
|
+
'vertex/gemini-3.5-flash. Vendor keys are withheld from runs meanwhile.',
|
|
685
|
+
'',
|
|
524
686
|
' zen key ls [--check] Everything stored, and its state.',
|
|
525
687
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
|
688
|
+
' zen key add bifrost --url <url> A gateway key, and where the gateway is.',
|
|
526
689
|
' zen key use <provider>/<name> Choose which one a run uses.',
|
|
690
|
+
' zen key mode [direct|bifrost] Call vendors directly, or via the gateway.',
|
|
527
691
|
' zen key check [provider[/name]] Ask the provider whether it still works.',
|
|
528
692
|
' zen key rm <provider>/<name> Forget one.',
|
|
529
693
|
' 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;
|