@zenera/cli 1.1.9 → 1.1.11
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 +49 -15
- package/dist/cache.d.ts +98 -0
- package/dist/cache.js +301 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +35 -11
- package/dist/commands/cache.d.ts +7 -0
- package/dist/commands/cache.js +245 -0
- package/dist/commands/check.js +6 -3
- package/dist/commands/index.js +3 -1
- package/dist/commands/key.js +68 -14
- package/dist/commands/models.js +17 -1
- package/dist/commands/run.js +11 -3
- package/dist/commands/sandbox.js +70 -23
- package/dist/history.d.ts +18 -0
- package/dist/history.js +93 -0
- package/dist/home.d.ts +2 -2
- package/dist/home.js +2 -2
- package/dist/keys.d.ts +22 -0
- package/dist/keys.js +105 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/liveness.js +11 -0
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +43 -17
- package/dist/term.d.ts +25 -2
- package/dist/term.js +224 -10
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +542 -58
- package/dist/tui/theme.d.ts +6 -2
- package/dist/tui/theme.js +14 -8
- package/dist/tui/wrap.d.ts +92 -0
- package/dist/tui/wrap.js +147 -2
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +87 -2
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +50 -13
- package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +202 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
- package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
- package/templates/editor/.github/skills/zen-cli/references/rag.md +104 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
- package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +28 -20
- package/templates/editor/.vscode/settings.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { parse } from "../args.js";
|
|
2
|
+
import { clear, items, kinds, sweep } from "../cache.js";
|
|
3
|
+
import { paths } from "../home.js";
|
|
4
|
+
import { ago, bold, bytes, confirm, count, cyan, dim, green, isInteractive, json, note, table, usageError, writeAll, } from "../term.js";
|
|
5
|
+
const USAGE = 'zen cache [ls|prune|clear] [options]';
|
|
6
|
+
/** Enough to see what is in there; the rest is a number. */
|
|
7
|
+
const LISTED = 20;
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// The cache, from outside
|
|
10
|
+
//
|
|
11
|
+
// Everything expensive and repeatable this machine has done is kept in one
|
|
12
|
+
// place, and the reason it needs a command at all is that nothing evicts from
|
|
13
|
+
// it on its own. That is deliberate: a store that quietly deletes things is
|
|
14
|
+
// only ever noticed when it has deleted the wrong one. So retention is a
|
|
15
|
+
// decision someone makes out loud, here.
|
|
16
|
+
//
|
|
17
|
+
// Nothing in it is precious. Every entry can be recomputed, which is why
|
|
18
|
+
// `clear` is safe to reach for and why the only cost of being wrong is time.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export const cache = {
|
|
21
|
+
summary: 'What work has been kept, and getting rid of it.',
|
|
22
|
+
usage: USAGE,
|
|
23
|
+
details: [
|
|
24
|
+
' ls What is stored, by kind. Changes nothing.',
|
|
25
|
+
' prune Remove what the filters name.',
|
|
26
|
+
' clear Remove everything, or one kind of everything.',
|
|
27
|
+
'',
|
|
28
|
+
' --kind <name> Just this kind. With `ls`, list its entries.',
|
|
29
|
+
' --older-than <age> Unused for longer than e.g. 30d, 12h, 2w.',
|
|
30
|
+
' --max-size <size> Ceiling on what is left, e.g. 500MB, 2GB.',
|
|
31
|
+
' --limit <n> Entries to list. Default 20.',
|
|
32
|
+
' --yes Do not ask before removing.',
|
|
33
|
+
'',
|
|
34
|
+
'Age is when an entry was last *used*, not when it was written, so a',
|
|
35
|
+
'vector a rebuild reads every week is never old.',
|
|
36
|
+
'',
|
|
37
|
+
'`prune` with no filter is a usage error: deleting everything is what',
|
|
38
|
+
'`clear` is for, and it should have to be typed.',
|
|
39
|
+
'',
|
|
40
|
+
'Nothing here is precious. Every entry is work that can be done again,',
|
|
41
|
+
'so the only cost of removing one is paying for it a second time.',
|
|
42
|
+
],
|
|
43
|
+
run: async (ctx) => {
|
|
44
|
+
const { values, positionals } = parse(ctx.args, {
|
|
45
|
+
kind: { type: 'string' },
|
|
46
|
+
'older-than': { type: 'string' },
|
|
47
|
+
'max-size': { type: 'string' },
|
|
48
|
+
limit: { type: 'string' },
|
|
49
|
+
yes: { type: 'boolean' },
|
|
50
|
+
}, USAGE);
|
|
51
|
+
const what = positionals[0] ?? 'ls';
|
|
52
|
+
if (!['ls', 'prune', 'clear'].includes(what)) {
|
|
53
|
+
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
54
|
+
}
|
|
55
|
+
if (positionals.length > 1) {
|
|
56
|
+
throw usageError('one subcommand at a time', USAGE);
|
|
57
|
+
}
|
|
58
|
+
switch (what) {
|
|
59
|
+
case 'ls':
|
|
60
|
+
return list(values, ctx.json);
|
|
61
|
+
case 'prune':
|
|
62
|
+
return prune(values, ctx.json);
|
|
63
|
+
default:
|
|
64
|
+
return wipe(values, ctx.json);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// ls
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
function list(values, asJson) {
|
|
72
|
+
if (values.kind) {
|
|
73
|
+
return listOne(values, asJson);
|
|
74
|
+
}
|
|
75
|
+
const rows = kinds();
|
|
76
|
+
if (asJson) {
|
|
77
|
+
json({ dir: paths.cache(), kinds: rows, ...totals(rows) });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
note(`nothing cached yet ${dim(paths.cache())}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
writeAll(table([
|
|
85
|
+
[bold('KIND'), bold('ENTRIES'), bold('SIZE'), bold('OLDEST'), bold('NEWEST')],
|
|
86
|
+
...rows.map((row) => [
|
|
87
|
+
cyan(row.kind),
|
|
88
|
+
String(row.entries),
|
|
89
|
+
bytes(row.bytes),
|
|
90
|
+
dim(since(row.oldest)),
|
|
91
|
+
dim(since(row.newest)),
|
|
92
|
+
]),
|
|
93
|
+
]));
|
|
94
|
+
const all = totals(rows);
|
|
95
|
+
note('');
|
|
96
|
+
note(dim(`${count(all.entries, 'entry', 'entries')}, ${bytes(all.bytes)} in ${paths.cache()}`));
|
|
97
|
+
}
|
|
98
|
+
function listOne(values, asJson) {
|
|
99
|
+
const kind = values.kind;
|
|
100
|
+
const limit = number(values.limit, '--limit') ?? LISTED;
|
|
101
|
+
const { rows, found } = items(kind, { limit });
|
|
102
|
+
if (asJson) {
|
|
103
|
+
json({ kind, found, entries: rows });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (found === 0) {
|
|
107
|
+
note(`nothing cached under ${cyan(kind)}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
writeAll(table([
|
|
111
|
+
[bold('KEY'), bold('SIZE'), bold('USED')],
|
|
112
|
+
...rows.map((row) => [shorten(row.key), bytes(row.bytes), dim(since(row.usedAt))]),
|
|
113
|
+
]));
|
|
114
|
+
if (found > rows.length) {
|
|
115
|
+
note('');
|
|
116
|
+
note(dim(`${found - rows.length} more — raise --limit to see them`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// prune and clear
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
function prune(values, asJson) {
|
|
123
|
+
const olderThanMs = duration(values['older-than']);
|
|
124
|
+
const maxBytes = size(values['max-size']);
|
|
125
|
+
if (olderThanMs === undefined && maxBytes === undefined) {
|
|
126
|
+
throw usageError('prune needs something to go on', 'pass --older-than <age> or --max-size <size>, or run: zen cache clear');
|
|
127
|
+
}
|
|
128
|
+
report(sweep({ kind: values.kind, olderThanMs, maxBytes }), asJson);
|
|
129
|
+
}
|
|
130
|
+
async function wipe(values, asJson) {
|
|
131
|
+
const before = kinds().filter((row) => !values.kind || row.kind === values.kind);
|
|
132
|
+
const all = totals(before);
|
|
133
|
+
if (all.entries === 0) {
|
|
134
|
+
if (asJson) {
|
|
135
|
+
json({ removed: [], entries: 0, bytes: 0 });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
note(values.kind ? `nothing cached under ${cyan(values.kind)}` : 'nothing cached yet');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!values.yes && !asJson && isInteractive()) {
|
|
142
|
+
const what = values.kind
|
|
143
|
+
? `${count(all.entries, 'entry', 'entries')} under ${values.kind}`
|
|
144
|
+
: count(all.entries, 'entry', 'entries');
|
|
145
|
+
if (!(await confirm(`Remove ${what} (${bytes(all.bytes)})?`))) {
|
|
146
|
+
note('left alone');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
clear({ kind: values.kind });
|
|
151
|
+
report(before.map((row) => ({ kind: row.kind, removed: row.entries, bytes: row.bytes })), asJson);
|
|
152
|
+
}
|
|
153
|
+
function report(swept, asJson) {
|
|
154
|
+
const entries = swept.reduce((n, row) => n + row.removed, 0);
|
|
155
|
+
const freed = swept.reduce((n, row) => n + row.bytes, 0);
|
|
156
|
+
if (asJson) {
|
|
157
|
+
json({ removed: swept, entries, bytes: freed });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (entries === 0) {
|
|
161
|
+
note('nothing to remove');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const row of swept) {
|
|
165
|
+
note(`${green('removed')} ${count(row.removed, 'entry', 'entries')} ${dim(`${row.kind} · ${bytes(row.bytes)}`)}`);
|
|
166
|
+
}
|
|
167
|
+
if (swept.length > 1) {
|
|
168
|
+
note('');
|
|
169
|
+
note(dim(`${count(entries, 'entry', 'entries')}, ${bytes(freed)} freed`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Reading the filters
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
const DURATIONS = {
|
|
176
|
+
s: 1000,
|
|
177
|
+
m: 60_000,
|
|
178
|
+
h: 3_600_000,
|
|
179
|
+
d: 86_400_000,
|
|
180
|
+
w: 604_800_000,
|
|
181
|
+
};
|
|
182
|
+
/** `30d`, `12h`, `2w`. A bare number is days: nobody means milliseconds. */
|
|
183
|
+
export function duration(text) {
|
|
184
|
+
if (text === undefined) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const match = /^(\d+(?:\.\d+)?)\s*([a-z]*)$/i.exec(text.trim());
|
|
188
|
+
const unit = match ? (match[2] || 'd').toLowerCase() : '';
|
|
189
|
+
const scale = DURATIONS[unit];
|
|
190
|
+
if (!match || scale === undefined) {
|
|
191
|
+
throw usageError(`"${text}" is not an age`, 'try: 30d, 12h, 2w');
|
|
192
|
+
}
|
|
193
|
+
return Number(match[1]) * scale;
|
|
194
|
+
}
|
|
195
|
+
const SIZES = {
|
|
196
|
+
b: 1,
|
|
197
|
+
kb: 1000,
|
|
198
|
+
mb: 1000 ** 2,
|
|
199
|
+
gb: 1000 ** 3,
|
|
200
|
+
tb: 1000 ** 4,
|
|
201
|
+
};
|
|
202
|
+
/** `500MB`, `2GB`. Powers of 1000, the way `bytes()` prints them back. */
|
|
203
|
+
export function size(text) {
|
|
204
|
+
if (text === undefined) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const match = /^(\d+(?:\.\d+)?)\s*([a-z]*)$/i.exec(text.trim());
|
|
208
|
+
const unit = match ? (match[2] || 'mb').toLowerCase() : '';
|
|
209
|
+
const scale = SIZES[unit];
|
|
210
|
+
if (!match || scale === undefined) {
|
|
211
|
+
throw usageError(`"${text}" is not a size`, 'try: 500MB, 2GB');
|
|
212
|
+
}
|
|
213
|
+
return Number(match[1]) * scale;
|
|
214
|
+
}
|
|
215
|
+
function number(text, flag) {
|
|
216
|
+
if (text === undefined) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const value = Number(text);
|
|
220
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
221
|
+
throw usageError(`${flag} takes a whole number of at least 1`, `got "${text}"`);
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Small formatting
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
const totals = (rows) => ({
|
|
229
|
+
entries: rows.reduce((n, row) => n + row.entries, 0),
|
|
230
|
+
bytes: rows.reduce((n, row) => n + row.bytes, 0),
|
|
231
|
+
});
|
|
232
|
+
const since = (at) => at === undefined ? 'never' : ago(new Date(at).toISOString());
|
|
233
|
+
/**
|
|
234
|
+
* A key holds every input that produced the value, so an embedding's key is a
|
|
235
|
+
* whole paragraph. One line of it says which entry this is; the rest is noise
|
|
236
|
+
* in a table.
|
|
237
|
+
*/
|
|
238
|
+
function shorten(key) {
|
|
239
|
+
const flat = key
|
|
240
|
+
.replace(/\u0000/g, ' · ')
|
|
241
|
+
.replace(/\s+/g, ' ')
|
|
242
|
+
.trim();
|
|
243
|
+
return flat.length > 72 ? `${flat.slice(0, 71)}…` : flat || dim('(unreadable)');
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=cache.js.map
|
package/dist/commands/check.js
CHANGED
|
@@ -35,9 +35,9 @@ export const check = {
|
|
|
35
35
|
details: [
|
|
36
36
|
'Checks the whole project: the configuration parses and satisfies the',
|
|
37
37
|
'schema, every prompt, skill and catalog it names is on disk, hand-offs',
|
|
38
|
-
'and forks name agents that exist,
|
|
39
|
-
'a catalog that holds them, and the
|
|
40
|
-
'on this machine.',
|
|
38
|
+
'and forks name agents that exist, every hand-off has a way back, tool',
|
|
39
|
+
'selectors resolve, skills bind to a catalog that holds them, and the',
|
|
40
|
+
'models it declares have a credential on this machine.',
|
|
41
41
|
'',
|
|
42
42
|
'It also builds the sandbox image and runs one command in it, against a',
|
|
43
43
|
'temporary directory rather than your workspace, and --no-sandbox skips',
|
|
@@ -199,6 +199,9 @@ function render(report) {
|
|
|
199
199
|
if (report.models.length) {
|
|
200
200
|
push(...table(report.models.map((m) => [
|
|
201
201
|
` ${m.name}`,
|
|
202
|
+
// An alias names nothing the provider knows, so what it
|
|
203
|
+
// stands for is printed next to it.
|
|
204
|
+
dim(m.ref ?? ''),
|
|
202
205
|
dim(m.provider ? `${m.provider} (${m.kind})` : red('unresolved')),
|
|
203
206
|
dim(m.env ?? ''),
|
|
204
207
|
credential(m.credential),
|
package/dist/commands/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cache } from "./cache.js";
|
|
1
2
|
import { check } from "./check.js";
|
|
2
3
|
import { init } from "./init.js";
|
|
3
4
|
import { inspect } from "./inspect.js";
|
|
@@ -23,6 +24,7 @@ export const COMMANDS = {
|
|
|
23
24
|
check,
|
|
24
25
|
inspect,
|
|
25
26
|
sandbox,
|
|
27
|
+
cache,
|
|
26
28
|
version,
|
|
27
29
|
};
|
|
28
30
|
/** Names that are not listed in help but still work. */
|
|
@@ -51,7 +53,7 @@ export const EXTERNAL = {
|
|
|
51
53
|
summary: 'Retrieval over a corpus: index it, then ask it something.',
|
|
52
54
|
usage: 'zen rag <subject> <command> [args...]',
|
|
53
55
|
install: 'npm i -g @zenera/rag',
|
|
54
|
-
banner: { head: 'Zenera', accent: 'Rag', subtitle: '
|
|
56
|
+
banner: { head: 'Zenera', accent: 'Rag', subtitle: 'Corpus Retrieval' },
|
|
55
57
|
},
|
|
56
58
|
};
|
|
57
59
|
//# sourceMappingURL=index.js.map
|
package/dist/commands/key.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { parse } from "../args.js";
|
|
3
3
|
import { ensureHome } from "../home.js";
|
|
4
|
-
import { ambient, ambientId, assertNotEmpty, describe, envNames, envOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
4
|
+
import { ambient, ambientId, assertNotEmpty, checkRegion, describe, envNames, envOf, formOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
5
5
|
import { probe, probeAll } from "../liveness.js";
|
|
6
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
7
|
const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
|
|
@@ -17,6 +17,32 @@ const MARK = {
|
|
|
17
17
|
function state(entry) {
|
|
18
18
|
return entry.check ? MARK[entry.check.state] : dim('unchecked');
|
|
19
19
|
}
|
|
20
|
+
/** Where a Vertex project id came from, since only one of the three was typed. */
|
|
21
|
+
const ORIGIN = {
|
|
22
|
+
env: 'from $GOOGLE_CLOUD_PROJECT',
|
|
23
|
+
stored: 'from --gcp-project',
|
|
24
|
+
file: 'from the key file',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Only a Vertex service account addresses a project and a region. An unset
|
|
28
|
+
* region is shown as the `global` it will actually be, not left blank: it is
|
|
29
|
+
* the slow endpoint, and a blank column is how it goes unnoticed. The project
|
|
30
|
+
* is shown whether or not one was stored, because the file names one too and a
|
|
31
|
+
* blank there reads as "none" rather than "not written down here". Yellow when
|
|
32
|
+
* the environment supplied either, because then the stored one is not in play.
|
|
33
|
+
*/
|
|
34
|
+
function placed(store, entry) {
|
|
35
|
+
if (entry.provider !== 'vertex' || entry.holds !== 'file') {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
const fromEnv = process.env.GOOGLE_CLOUD_LOCATION;
|
|
39
|
+
const where = fromEnv ?? entry.location ?? 'global';
|
|
40
|
+
const project = store.projectOf(entry);
|
|
41
|
+
const id = project ? project.id : '';
|
|
42
|
+
return [fromEnv ? yellow(where) : dim(where), project?.from === 'env' ? yellow(id) : dim(id)]
|
|
43
|
+
.join(' ')
|
|
44
|
+
.trimEnd();
|
|
45
|
+
}
|
|
20
46
|
/**
|
|
21
47
|
* An ambient credential borrowed into the shape the rest of this file works
|
|
22
48
|
* in. It is not in the store and never will be: `store.find` misses it, so
|
|
@@ -35,7 +61,7 @@ function asEntry(cred) {
|
|
|
35
61
|
}
|
|
36
62
|
function rows(store, borrowed) {
|
|
37
63
|
const out = [
|
|
38
|
-
[bold(''), bold('KEY'), bold('VALUE'), bold('STATE'), bold('CHECKED')],
|
|
64
|
+
[bold(''), bold('KEY'), bold('VALUE'), bold('GCP'), bold('STATE'), bold('CHECKED')],
|
|
39
65
|
];
|
|
40
66
|
for (const provider of OWNERS) {
|
|
41
67
|
for (const entry of store.for(provider)) {
|
|
@@ -44,6 +70,7 @@ function rows(store, borrowed) {
|
|
|
44
70
|
store.isActive(entry) ? green('*') : ' ',
|
|
45
71
|
keyId(entry),
|
|
46
72
|
dim(describe(store, entry)),
|
|
73
|
+
placed(store, entry),
|
|
47
74
|
state(entry),
|
|
48
75
|
dim(entry.check ? ago(entry.check.at) : '—') +
|
|
49
76
|
(shadow && store.isActive(entry) ? yellow(` shadowed by $${shadow}`) : ''),
|
|
@@ -54,6 +81,7 @@ function rows(store, borrowed) {
|
|
|
54
81
|
dim('~'),
|
|
55
82
|
dim(ambientId(cred)),
|
|
56
83
|
dim(describe(store, entry)),
|
|
84
|
+
placed(store, entry),
|
|
57
85
|
state(entry),
|
|
58
86
|
dim(cred.env ? 'from the environment' : 'from gcloud'),
|
|
59
87
|
]);
|
|
@@ -133,9 +161,11 @@ const add = async (ctx, args) => {
|
|
|
133
161
|
const { values, positionals } = parse(args, {
|
|
134
162
|
name: { type: 'string' },
|
|
135
163
|
'no-check': { type: 'boolean' },
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
// Prefixed because every other command's --project is a Zenera one.
|
|
165
|
+
'gcp-project': { type: 'string' },
|
|
166
|
+
'gcp-location': { type: 'string' },
|
|
167
|
+
}, 'zen key add <provider>[/name] [--name <name>] [--gcp-project <id>] ' +
|
|
168
|
+
'[--gcp-location <region>] [--no-check]');
|
|
139
169
|
const ref = positionals[0];
|
|
140
170
|
if (!ref) {
|
|
141
171
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -144,8 +174,14 @@ const add = async (ctx, args) => {
|
|
|
144
174
|
const provider = parsed.provider;
|
|
145
175
|
const name = values.name ?? parsed.name ?? 'default';
|
|
146
176
|
const shape = SHAPES[provider];
|
|
147
|
-
if ((values
|
|
148
|
-
throw usageError(`--project and --location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
177
|
+
if ((values['gcp-project'] || values['gcp-location']) && provider !== 'vertex') {
|
|
178
|
+
throw usageError(`--gcp-project and --gcp-location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
179
|
+
}
|
|
180
|
+
// Checked before the prompt, so a typo does not cost you pasting the key.
|
|
181
|
+
const region = values['gcp-location'];
|
|
182
|
+
if (region !== undefined && !checkRegion(region)) {
|
|
183
|
+
note(yellow(`${region} is not a region this build knows; storing it anyway`));
|
|
184
|
+
note(dim(' if it is new, this is fine; if it is a typo, Vertex answers 404'));
|
|
149
185
|
}
|
|
150
186
|
ensureHome();
|
|
151
187
|
const store = await KeyStore.open();
|
|
@@ -164,9 +200,14 @@ const add = async (ctx, args) => {
|
|
|
164
200
|
if (shape.forms.length === 1 && shape.forms[0].holds === 'file' && !existsSync(raw)) {
|
|
165
201
|
throw usageError(`no such file: ${raw}`);
|
|
166
202
|
}
|
|
203
|
+
// Which shape was given is only knowable now, and `add` would drop these
|
|
204
|
+
// two on an express key rather than store a combination Vertex refuses.
|
|
205
|
+
if (formOf(provider, raw).holds !== 'file' && (values['gcp-project'] || region)) {
|
|
206
|
+
throw usageError('--gcp-project and --gcp-location need a service-account file', 'the value given is an express-mode key, which addresses no project or region');
|
|
207
|
+
}
|
|
167
208
|
const entry = store.add(provider, name, raw, {
|
|
168
|
-
project: values
|
|
169
|
-
location: values
|
|
209
|
+
project: values['gcp-project'],
|
|
210
|
+
location: values['gcp-location'],
|
|
170
211
|
});
|
|
171
212
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
172
213
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
@@ -186,6 +227,12 @@ const add = async (ctx, args) => {
|
|
|
186
227
|
note(dim(` ${check.fix}`));
|
|
187
228
|
}
|
|
188
229
|
}
|
|
230
|
+
else if (check.state === 'unknown' && check.fix) {
|
|
231
|
+
// Most unknowns are a plane or a proxy and say nothing worth a
|
|
232
|
+
// line. One carrying a fix is different: we know what is wrong.
|
|
233
|
+
note(`${yellow('unverified')} ${check.detail}`);
|
|
234
|
+
note(dim(` ${check.fix}`));
|
|
235
|
+
}
|
|
189
236
|
}
|
|
190
237
|
store.save();
|
|
191
238
|
if (ctx.json) {
|
|
@@ -203,7 +250,10 @@ const add = async (ctx, args) => {
|
|
|
203
250
|
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
204
251
|
}
|
|
205
252
|
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
206
|
-
|
|
253
|
+
const found = store.projectOf(entry);
|
|
254
|
+
note(dim(found
|
|
255
|
+
? `no --gcp-project given; project ${found.id}, read from the key file`
|
|
256
|
+
: 'no --gcp-project given, and the key file names no project_id'));
|
|
207
257
|
}
|
|
208
258
|
};
|
|
209
259
|
/**
|
|
@@ -329,12 +379,14 @@ const show = async (ctx, args) => {
|
|
|
329
379
|
throw usageError(`no key ${ref}`, 'see: zen key ls');
|
|
330
380
|
}
|
|
331
381
|
const value = values.reveal ? store.reveal(entry) : describe(store, entry);
|
|
382
|
+
const project = store.projectOf(entry);
|
|
332
383
|
if (ctx.json) {
|
|
333
384
|
json({
|
|
334
385
|
key: keyId(entry),
|
|
335
386
|
env: envOf(entry),
|
|
336
387
|
value,
|
|
337
388
|
revealed: Boolean(values.reveal),
|
|
389
|
+
...(project ? { project: project.id, projectFrom: project.from } : {}),
|
|
338
390
|
});
|
|
339
391
|
return;
|
|
340
392
|
}
|
|
@@ -348,8 +400,10 @@ const show = async (ctx, args) => {
|
|
|
348
400
|
[dim('key'), keyId(entry)],
|
|
349
401
|
[dim('env'), envOf(entry)],
|
|
350
402
|
[dim('value'), value],
|
|
351
|
-
...(
|
|
352
|
-
|
|
403
|
+
...(project
|
|
404
|
+
? [[dim('gcp project'), `${project.id} ${dim(ORIGIN[project.from])}`]]
|
|
405
|
+
: []),
|
|
406
|
+
...(entry.location ? [[dim('gcp location'), entry.location]] : []),
|
|
353
407
|
[dim('state'), state(entry)],
|
|
354
408
|
...(entry.check?.fix ? [[dim('fix'), entry.check.fix]] : []),
|
|
355
409
|
[dim('added'), ago(entry.addedAt)],
|
|
@@ -416,8 +470,8 @@ export const key = {
|
|
|
416
470
|
'Services the tools call: exa.',
|
|
417
471
|
'',
|
|
418
472
|
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
419
|
-
'--project and --location too, or an express-mode API key, which
|
|
420
|
-
'neither. Which one you gave is read off the value.',
|
|
473
|
+
'--gcp-project and --gcp-location too, or an express-mode API key, which',
|
|
474
|
+
'wants neither. Which one you gave is read off the value.',
|
|
421
475
|
'',
|
|
422
476
|
' zen key ls [--check] Everything stored, and its state.',
|
|
423
477
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
package/dist/commands/models.js
CHANGED
|
@@ -393,6 +393,17 @@ function verdict(probe) {
|
|
|
393
393
|
return dim('no answer');
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Which project and region a Vertex call was actually made in. A publisher
|
|
398
|
+
* model is only missing *somewhere*, and the somewhere is the part nobody
|
|
399
|
+
* typed: it comes off the key, or out of the service-account file itself.
|
|
400
|
+
*/
|
|
401
|
+
function vertexAt(where) {
|
|
402
|
+
const entry = where.fromEnv.has('vertex') ? undefined : where.store.active('vertex');
|
|
403
|
+
const id = entry ? where.store.projectOf(entry)?.id : process.env.GOOGLE_CLOUD_PROJECT;
|
|
404
|
+
const location = process.env.GOOGLE_CLOUD_LOCATION ?? entry?.location ?? 'global';
|
|
405
|
+
return `vertex: project ${id ?? 'unset'} · location ${location}`;
|
|
406
|
+
}
|
|
396
407
|
const test = async (ctx, args) => {
|
|
397
408
|
const { values, positionals } = parse(args, ROLE_OPTIONS, 'zen models test <provider:model> … [--chat|--embedding]');
|
|
398
409
|
if (positionals.length === 0) {
|
|
@@ -402,7 +413,7 @@ const test = async (ctx, args) => {
|
|
|
402
413
|
// Every ref is split before anything is built, so a typo in the third one
|
|
403
414
|
// does not arrive after two billable calls.
|
|
404
415
|
const parsed = positionals.map((ref) => ({ ref, ...split(ref) }));
|
|
405
|
-
await credentials();
|
|
416
|
+
const where = await credentials();
|
|
406
417
|
const probes = [];
|
|
407
418
|
const bar = ctx.json ? undefined : progress();
|
|
408
419
|
for (const { ref, provider, id } of parsed) {
|
|
@@ -425,6 +436,11 @@ const test = async (ctx, args) => {
|
|
|
425
436
|
note(dim(`${p.ref}: ${p.check.fix}`));
|
|
426
437
|
}
|
|
427
438
|
}
|
|
439
|
+
// A Vertex refusal is about a model *in a project*, and the project is
|
|
440
|
+
// the half of that nobody typed.
|
|
441
|
+
if (parsed.some((p) => p.provider === 'vertex')) {
|
|
442
|
+
note(dim(vertexAt(where)));
|
|
443
|
+
}
|
|
428
444
|
}
|
|
429
445
|
const failed = probes.filter((p) => p.check.state !== 'live');
|
|
430
446
|
if (failed.length > 0) {
|
package/dist/commands/run.js
CHANGED
|
@@ -104,13 +104,14 @@ export const run = {
|
|
|
104
104
|
await start(engine, {
|
|
105
105
|
readOnly: Boolean(values['read-only']),
|
|
106
106
|
theme: values.theme,
|
|
107
|
+
started: { created: where.created, freshWorkspace: where.freshWorkspace },
|
|
107
108
|
});
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
if (!prompt) {
|
|
111
112
|
throw usageError('nothing to ask', 'give a prompt, or pipe one in');
|
|
112
113
|
}
|
|
113
|
-
await once(engine, prompt, values, ctx.json, ctx.cwd);
|
|
114
|
+
await once(engine, prompt, values, ctx.json, ctx.cwd, where);
|
|
114
115
|
}
|
|
115
116
|
finally {
|
|
116
117
|
await engine.close();
|
|
@@ -120,7 +121,7 @@ export const run = {
|
|
|
120
121
|
// ---------------------------------------------------------------------------
|
|
121
122
|
// One shot
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
|
-
async function once(engine, prompt, values, asJson, cwd) {
|
|
124
|
+
async function once(engine, prompt, values, asJson, cwd, where) {
|
|
124
125
|
const narrator = new Narrator({
|
|
125
126
|
quiet: Boolean(values.quiet) || asJson,
|
|
126
127
|
live: Boolean(process.stderr.isTTY),
|
|
@@ -131,7 +132,14 @@ async function once(engine, prompt, values, asJson, cwd) {
|
|
|
131
132
|
const onInterrupt = () => stopping.abort();
|
|
132
133
|
process.once('SIGINT', onInterrupt);
|
|
133
134
|
if (!values.quiet && !asJson) {
|
|
134
|
-
|
|
135
|
+
// Two questions were just answered, possibly without being asked. Say
|
|
136
|
+
// which way they went: a run that quietly resumed the wrong session, or
|
|
137
|
+
// wrote into the directory you were standing in, is only explainable
|
|
138
|
+
// afterwards, and this is the one line where it is cheap to say.
|
|
139
|
+
note(`${bold(engine.name)} ${dim(where.created ? 'new session' : 'continuing')} ` +
|
|
140
|
+
`${dim(engine.session.id)}`);
|
|
141
|
+
note(dim(`${where.freshWorkspace ? 'new directory' : 'workspace'} ` +
|
|
142
|
+
`${display(engine.workspace, cwd)}`));
|
|
135
143
|
}
|
|
136
144
|
let outcome;
|
|
137
145
|
try {
|