@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.
Files changed (43) hide show
  1. package/README.md +73 -24
  2. package/dist/audit.d.ts +2 -2
  3. package/dist/audit.js +11 -4
  4. package/dist/candidates.d.ts +9 -0
  5. package/dist/candidates.js +17 -0
  6. package/dist/catalog.d.ts +10 -5
  7. package/dist/catalog.js +93 -12
  8. package/dist/commands/completion.d.ts +9 -0
  9. package/dist/commands/completion.js +70 -0
  10. package/dist/commands/index.js +2 -0
  11. package/dist/commands/init.js +1 -1
  12. package/dist/commands/key.js +181 -17
  13. package/dist/commands/meta.d.ts +3 -0
  14. package/dist/commands/meta.js +134 -29
  15. package/dist/commands/models.js +56 -98
  16. package/dist/completion-install.d.ts +16 -0
  17. package/dist/completion-install.js +85 -0
  18. package/dist/completion.d.ts +9 -0
  19. package/dist/completion.js +93 -0
  20. package/dist/keys.d.ts +29 -4
  21. package/dist/keys.js +67 -3
  22. package/dist/liveness.js +73 -3
  23. package/dist/main.js +22 -0
  24. package/dist/postinstall.d.ts +2 -0
  25. package/dist/postinstall.js +10 -0
  26. package/dist/sandbox.js +4 -3
  27. package/dist/term.d.ts +2 -0
  28. package/dist/term.js +2 -0
  29. package/dist/tui/links.d.ts +2 -0
  30. package/dist/tui/links.js +31 -7
  31. package/dist/tui/models.d.ts +5 -0
  32. package/dist/tui/models.js +21 -14
  33. package/dist/validate.js +2 -2
  34. package/package.json +8 -6
  35. package/postinstall.mjs +7 -0
  36. package/templates/completions/zen.bash +16 -0
  37. package/templates/completions/zen.fish +22 -0
  38. package/templates/completions/zen.zsh +28 -0
  39. package/templates/editor/.github/copilot-instructions.md +5 -4
  40. package/templates/editor/.github/prompts/spec-edit.prompt.md +250 -0
  41. package/templates/editor/.github/skills/zen-cli/SKILL.md +2 -2
  42. package/templates/editor/.github/skills/zen-cli/references/keys.md +29 -2
  43. package/templates/editor/.github/skills/zen-cli/references/models.md +22 -23
package/README.md CHANGED
@@ -117,6 +117,28 @@ Or without installing anything:
117
117
  npx @zenera/cli --help
118
118
  ```
119
119
 
120
+ Tab completion for zsh, bash and fish - commands, projects, stored prompts - is
121
+ set up automatically by the global npm install. It appends a guarded loader to
122
+ your current shell's startup file, once. **Open a new terminal** to activate it;
123
+ the installer cannot update the terminal where you ran npm.
124
+
125
+ Setup respects `ZDOTDIR` for zsh and `XDG_CONFIG_HOME` for fish. For Bash on
126
+ macOS it covers both login shells (the active `.bash_profile`, `.bash_login` or
127
+ `.profile`) and non-login shells (`.bashrc`). The loader does nothing when `zen`
128
+ is no longer installed.
129
+
130
+ Local installs and `npx` do not change shell files. Automatic setup is skipped
131
+ under root/sudo, in CI, on Windows, or when `$SHELL` is missing or unsupported.
132
+ Disabling lifecycle scripts with `--ignore-scripts` also disables setup.
133
+ If automatic setup is skipped or fails, or to enable another shell, run:
134
+
135
+ ```sh
136
+ zen completion --install # the shell you are in; --yes to skip the question
137
+ zen completion fish --install # or name one
138
+ ```
139
+
140
+ Or add it yourself: `eval "$(zen completion zsh)"` in `~/.zshrc`.
141
+
120
142
  ## Quickstart
121
143
 
122
144
  From nothing to a specialized agent system:
@@ -557,24 +579,25 @@ The commands to run next are printed for you to read and type.
557
579
 
558
580
  ## Commands
559
581
 
560
- | Command | Does |
561
- | --------- | -------------------------------------------------------------------- |
562
- | `init` | Creates a project here, or in `<dir>`, and registers it. |
563
- | `list` | Every known project: sessions, last run, whether one is live. |
564
- | `open` | Opens a project in your editor. |
565
- | `key` | The credential keyring - add, check, switch, remove. |
566
- | `models` | What this machine can use - list, search, test, pick. |
567
- | `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
568
- | `meta` | Runs the meta agent over the project - the one that edits it. |
569
- | `inspect` | Opens or rebuilds a run's `report.html`. |
570
- | `view` | Shows a folder in the browser, redrawn live as its files change. |
571
- | `memory` | What the agents remember - size, listing, one node, or a whole page. |
572
- | `check` | Validates the project and every file it names, and asks the models. |
573
- | `export` | Writes the project to a shareable zip archive. |
574
- | `import` | Unpacks one, registers it, and runs nothing in it. |
575
- | `sandbox` | Checks and prepares the container that command-line tools run in. |
576
- | `cache` | What work has been kept, and getting rid of it. |
577
- | `version` | CLI, library and Node versions. |
582
+ | Command | Does |
583
+ | ------------ | -------------------------------------------------------------------- |
584
+ | `init` | Creates a project here, or in `<dir>`, and registers it. |
585
+ | `list` | Every known project: sessions, last run, whether one is live. |
586
+ | `open` | Opens a project in your editor. |
587
+ | `key` | The credential keyring - add, check, switch, remove. |
588
+ | `models` | What this machine can use - list, search, test, pick. |
589
+ | `run` | Runs the project - the TUI on a terminal, a single answer otherwise. |
590
+ | `meta` | Runs the meta agent over the project - the one that edits it. |
591
+ | `inspect` | Opens or rebuilds a run's `report.html`. |
592
+ | `view` | Shows a folder in the browser, redrawn live as its files change. |
593
+ | `memory` | What the agents remember - size, listing, one node, or a whole page. |
594
+ | `check` | Validates the project and every file it names, and asks the models. |
595
+ | `export` | Writes the project to a shareable zip archive. |
596
+ | `import` | Unpacks one, registers it, and runs nothing in it. |
597
+ | `sandbox` | Checks and prepares the container that command-line tools run in. |
598
+ | `cache` | What work has been kept, and getting rid of it. |
599
+ | `completion` | Tab completion for zsh, bash and fish - printed, or installed. |
600
+ | `version` | CLI, library and Node versions. |
578
601
 
579
602
  ### Commands from other packages
580
603
 
@@ -760,6 +783,9 @@ fixes and the memory are making runs cheaper; each case has its own
760
783
  every try. `start` prints a local address that shows those pages in a browser,
761
784
  with their diagrams drawn, updated in place as the tuning writes them.
762
785
 
786
+ The whole method, with how to write a rubric that tunes the right thing:
787
+ [docs/finetuning.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/finetuning.md).
788
+
763
789
  ## Credentials
764
790
 
765
791
  One keyring serves every provider, and a key goes in the same way whatever it
@@ -816,7 +842,6 @@ zen models openai # everything OpenAI serves this acco
816
842
  zen models search haiku --tools --free # narrow it
817
843
  zen models show openrouter:anthropic/claude-haiku-4.5
818
844
  zen models test vertex:gemini-embedding-001 # one real call, one verdict
819
- zen models pick --embedding # the first ref that answers, on stdout
820
845
  zen models browse # walk providers and models with the arrow keys
821
846
  ```
822
847
 
@@ -840,15 +865,15 @@ $ zen models test vertex:gemini-embedding-001
840
865
  vertex:gemini-embedding-001 blocked Vertex AI API has not been used in project my-proj …
841
866
  fix vertex:gemini-embedding-001 gcloud services enable aiplatform.googleapis.com --project my-proj
842
867
  error 1 of 1 did not answer
843
- find one that does: zen models pick --embedding
868
+ see what else there is: zen models ls --embeddings
844
869
  ```
845
870
 
846
- `zen models pick` tries a short list of candidates one at a time, stops at the
847
- first that works, and prints the bare reference on stdout. So recovering from
848
- the above is a single substitution, whether a person or an agent is doing it:
871
+ To take another model instead, list what the account serves, test one, and put
872
+ its ref in `agents.yaml` or the flag that needs it:
849
873
 
850
874
  ```sh
851
- zen rag schema index --embedding "$(zen models pick --embedding)" ./specs/*.yaml
875
+ zen models ls --embeddings
876
+ zen models test openai:text-embedding-3-small
852
877
  ```
853
878
 
854
879
  `zen models browse [provider]` is the interactive way in: arrow keys through
@@ -1026,6 +1051,26 @@ clear where a working provider actually comes from.
1026
1051
 
1027
1052
  </details>
1028
1053
 
1054
+ <details>
1055
+ <summary><strong>Every model through a Bifrost gateway</strong></summary>
1056
+
1057
+ With one virtual key in place of every vendor key:
1058
+
1059
+ ```sh
1060
+ zen key add bifrost --url https://bifrost.example.com
1061
+ zen key mode bifrost # every run goes through the gateway
1062
+ zen key mode direct # back to the vendor keys
1063
+ ```
1064
+
1065
+ `agents.yaml` does not change. Each model keeps its own vendor's protocol and
1066
+ is sent to the gateway's matching endpoint - `vertex:gemini-3.5-flash` reaches
1067
+ `/genai` as `vertex/gemini-3.5-flash` - so thinking and cache accounting
1068
+ survive the trip. The whole story - variables, the sandbox, `zen models`, known
1069
+ limits - is in
1070
+ [docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md).
1071
+
1072
+ </details>
1073
+
1029
1074
  ## Cache - work you have already paid for
1030
1075
 
1031
1076
  Embedding a paragraph, parsing a document, asking a provider what it serves: all
@@ -1075,6 +1120,10 @@ terminal:
1075
1120
  - making an API description searchable and mockable.
1076
1121
  - [docs/memory.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/memory.md)
1077
1122
  - what agents keep between sessions.
1123
+ - [docs/finetuning.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/finetuning.md)
1124
+ - building a dataset, writing rubrics, and tuning the project on them.
1125
+ - [docs/gateway.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/gateway.md)
1126
+ - every model through a Bifrost gateway, with one virtual key.
1078
1127
 
1079
1128
  ## License
1080
1129
 
package/dist/audit.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ModelRequirement } from '@zenera/neo';
2
- import { type KeyStore, type Provider } from './keys.ts';
2
+ import { type KeyOwner, type KeyStore } from './keys.ts';
3
3
  /** Which key of the config named a reference: `model:`/`models:` or `embedding:`/`embeddings:`. */
4
4
  export type DeclaredRole = 'model' | 'embedding';
5
5
  export interface ModelIssue {
@@ -19,7 +19,7 @@ export interface ModelIssue {
19
19
  /** what to do about a refusal that a new key would not fix */
20
20
  fix?: string;
21
21
  /** the keyring provider the fix names, when the kind is one of them */
22
- add?: Provider;
22
+ add?: KeyOwner;
23
23
  }
24
24
  /**
25
25
  * Whether a requirement is actually met, and under which variable.
package/dist/audit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { projectRegistry, readProjectConfig, } from '@zenera/neo';
2
- import { envNames, form, gcloudAdc, isProvider, SHAPES, } from "./keys.js";
2
+ import { envNames, form, gcloudAdc, isOwner, isProvider, SHAPES, } from "./keys.js";
3
3
  import { bold, dim } from "./term.js";
4
4
  /**
5
5
  * Every model the project names, keyed by how one would refer to it.
@@ -48,6 +48,9 @@ function declared(config) {
48
48
  * such answer, so it names the form the provider is usually reached by.
49
49
  */
50
50
  export function credentialFor(need) {
51
+ if (need.gateway) {
52
+ return { env: need.apiKeyEnv, present: need.satisfied };
53
+ }
51
54
  const provider = isProvider(need.kind) ? need.kind : undefined;
52
55
  // A provider with a file form can authenticate without an api key, which is
53
56
  // the only thing `satisfied` knows how to look for.
@@ -86,7 +89,11 @@ export function auditModels(projectDir, store) {
86
89
  continue;
87
90
  }
88
91
  const { env, present } = credentialFor(need);
89
- const provider = isProvider(need.kind) ? need.kind : undefined;
92
+ const provider = need.gateway && isOwner(need.gateway)
93
+ ? need.gateway
94
+ : isProvider(need.kind)
95
+ ? need.kind
96
+ : undefined;
90
97
  if (!present) {
91
98
  issues.push({
92
99
  name,
@@ -132,8 +139,8 @@ export function describeIssue(issue) {
132
139
  }
133
140
  const why = issue.detail ? `: ${issue.detail}` : '';
134
141
  if (issue.reason === 'blocked') {
135
- const pick = `zen models pick --${issue.role === 'embedding' ? 'embedding' : 'chat'}`;
136
- return `${what} authenticated and was then refused${why} — ${dim(issue.fix ?? pick)}`;
142
+ const list = `zen models ls --${issue.role === 'embedding' ? 'embeddings' : 'chat'}`;
143
+ return `${what} authenticated and was then refused${why} — ${dim(issue.fix ?? list)}`;
137
144
  }
138
145
  const fix = `zen key check ${issue.add ?? ''}`.trim();
139
146
  return `${what} was rejected when last checked${why} — ${dim(fix)}`;
@@ -0,0 +1,9 @@
1
+ export interface Candidate {
2
+ value: string;
3
+ description?: string;
4
+ }
5
+ /** Candidates the word being typed is a prefix of. */
6
+ export declare const matching: (found: readonly Candidate[], prefix: string) => Candidate[];
7
+ /** Registered projects whose directory is still there. */
8
+ export declare function projectNames(): Promise<Candidate[]>;
9
+ //# sourceMappingURL=candidates.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import * as Projects from "./projects.js";
3
+ /** Candidates the word being typed is a prefix of. */
4
+ export const matching = (found, prefix) => found.filter((c) => c.value.startsWith(prefix));
5
+ /** Registered projects whose directory is still there. */
6
+ export async function projectNames() {
7
+ try {
8
+ const registry = await Projects.Registry.open();
9
+ return registry.entries
10
+ .filter((e) => existsSync(e.path))
11
+ .map((e) => ({ value: e.name, description: e.path }));
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ //# sourceMappingURL=candidates.js.map
package/dist/catalog.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Gateway } from '@zenera/neo';
1
2
  import { type KeyCheck, type Provider } from './keys.ts';
2
3
  /**
3
4
  * What a model is *for*. A model can hold more than one — Gemini's embedding
@@ -50,6 +51,11 @@ export interface Catalog {
50
51
  fetchedAt: string;
51
52
  /** why the live call was not used, when it was tried and failed */
52
53
  problem?: KeyCheck;
54
+ /** the gateway it was listed through, when the keyring is in a gateway mode */
55
+ via?: {
56
+ name: string;
57
+ url: string;
58
+ };
53
59
  }
54
60
  /** A day. Model lists change on the scale of weeks; a stale row costs a retry. */
55
61
  export declare const CATALOG_TTL_MS: number;
@@ -64,11 +70,8 @@ export declare const CATALOG_KIND = "catalog";
64
70
  */
65
71
  export declare const CURATED: Record<Provider, readonly Omit<CatalogEntry, 'ref' | 'provider' | 'source'>[]>;
66
72
  /**
67
- * The order `zen models pick` walks, per provider and per role.
68
- *
69
- * Cheap and fast first. `pick` exists to answer "give me something that works"
70
- * in one round trip where it can, and the small models answer soonest and cost
71
- * least when the answer is thrown away — which it always is.
73
+ * The cheap, fast models per provider and role, best first. `zen init` takes
74
+ * its default embedder from here.
72
75
  */
73
76
  export declare const PREFERRED: Record<Provider, {
74
77
  chat: readonly string[];
@@ -85,6 +88,8 @@ export declare function fromVertexPartners(): Promise<CatalogEntry[]>;
85
88
  * `partners` is the same seam for Vertex's second listing.
86
89
  */
87
90
  export declare function fetchCatalog(provider: Provider, client?: unknown, partners?: () => Promise<CatalogEntry[]>): Promise<CatalogEntry[]>;
91
+ /** What the gateway serves under one provider's name, enriched like a vendor listing. */
92
+ export declare function fetchGatewayCatalog(provider: Provider, at: Gateway): Promise<CatalogEntry[]>;
88
93
  /** The key one provider's listing is filed under, version and all. */
89
94
  export declare const catalogKey: (provider: Provider) => string;
90
95
  export interface CatalogOptions {
package/dist/catalog.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // `source` for exactly that reason: a curated guess must never be mistaken for
17
17
  // the vendor's own word.
18
18
  // ---------------------------------------------------------------------------
19
- import { ModelRegistry, vertexEndpoint } from '@zenera/neo';
19
+ import { GATEWAY_KEY_HEADER, GATEWAY_NAME, GATEWAY_UPSTREAMS, gatewayOf, ModelRegistry, vertexEndpoint, } from '@zenera/neo';
20
20
  import { Cache, cacheKey } from "./cache.js";
21
21
  import { PROVIDERS } from "./keys.js";
22
22
  import { classify } from "./liveness.js";
@@ -72,11 +72,8 @@ export const CURATED = {
72
72
  ],
73
73
  };
74
74
  /**
75
- * The order `zen models pick` walks, per provider and per role.
76
- *
77
- * Cheap and fast first. `pick` exists to answer "give me something that works"
78
- * in one round trip where it can, and the small models answer soonest and cost
79
- * least when the answer is thrown away — which it always is.
75
+ * The cheap, fast models per provider and role, best first. `zen init` takes
76
+ * its default embedder from here.
80
77
  */
81
78
  export const PREFERRED = {
82
79
  openai: {
@@ -369,6 +366,65 @@ export async function fetchCatalog(provider, client = new ModelRegistry().client
369
366
  return enrich(provider, await fromRouter(client, provider));
370
367
  }
371
368
  }
369
+ const UPSTREAM_OF = new Map(PROVIDERS.map((p) => [GATEWAY_UPSTREAMS[p], p]));
370
+ /** One request per gateway per process, however many providers ask. */
371
+ const listings = new Map();
372
+ function gatewayListing(at) {
373
+ const key = `${at.baseURL}\0${at.apiKey ?? ''}`;
374
+ let listing = listings.get(key);
375
+ if (!listing) {
376
+ listing = fetchGatewayListing(at);
377
+ listings.set(key, listing);
378
+ listing.catch(() => listings.delete(key));
379
+ }
380
+ return listing;
381
+ }
382
+ async function fetchGatewayListing(at) {
383
+ const res = await fetch(`${at.baseURL}/v1/models`, {
384
+ headers: at.apiKey
385
+ ? { authorization: `Bearer ${at.apiKey}`, [GATEWAY_KEY_HEADER]: at.apiKey }
386
+ : {},
387
+ signal: AbortSignal.timeout(30_000),
388
+ });
389
+ if (!res.ok) {
390
+ const body = await res.text().catch(() => '');
391
+ throw Object.assign(new Error(`${GATEWAY_NAME} answered ${res.status}: ${body}`), {
392
+ status: res.status,
393
+ });
394
+ }
395
+ const { data } = (await res.json());
396
+ const by = new Map();
397
+ for (const model of data ?? []) {
398
+ if (typeof model.id !== 'string') {
399
+ continue;
400
+ }
401
+ const slash = model.id.indexOf('/');
402
+ const provider = slash > 0 ? UPSTREAM_OF.get(model.id.slice(0, slash)) : undefined;
403
+ const id = model.id.slice(slash + 1);
404
+ const role = openaiRole(id);
405
+ if (!provider || !id || !role) {
406
+ continue;
407
+ }
408
+ const rows = by.get(provider) ?? [];
409
+ rows.push(entry(provider, {
410
+ id,
411
+ roles: [role],
412
+ name: typeof model.name === 'string' ? model.name : undefined,
413
+ contextLength: typeof model.context_length === 'number' ? model.context_length : undefined,
414
+ created: iso(typeof model.created === 'number' ? model.created : undefined),
415
+ publisher: provider === 'vertex' ? publisherOf(id) : undefined,
416
+ }));
417
+ by.set(provider, rows);
418
+ }
419
+ return by;
420
+ }
421
+ function publisherOf(id) {
422
+ return /^claude-/.test(id) ? 'anthropic' : id.includes('/') ? id.split('/')[0] : 'google';
423
+ }
424
+ /** What the gateway serves under one provider's name, enriched like a vendor listing. */
425
+ export async function fetchGatewayCatalog(provider, at) {
426
+ return enrich(provider, (await gatewayListing(at)).get(provider) ?? []);
427
+ }
372
428
  // ---------------------------------------------------------------------------
373
429
  // Enrichment and cache
374
430
  // ---------------------------------------------------------------------------
@@ -407,6 +463,10 @@ const store = () => new Cache(CATALOG_KIND, { mode: 0o644 });
407
463
  * to both. A project left to the service-account file is not seen here.
408
464
  */
409
465
  function scope(provider) {
466
+ const at = gatewayOf();
467
+ if (at) {
468
+ return cacheKey(GATEWAY_NAME, at.baseURL);
469
+ }
410
470
  if (provider !== 'vertex') {
411
471
  return undefined;
412
472
  }
@@ -433,19 +493,31 @@ function readCache(provider) {
433
493
  * model list to four rows.
434
494
  */
435
495
  export async function loadCatalog(provider, opts = {}) {
496
+ const at = gatewayOf();
497
+ const via = at ? { via: { name: GATEWAY_NAME, url: at.baseURL } } : {};
498
+ // The built-in list names vendor models the gateway may not serve at all.
499
+ const fallback = () => (at ? [] : curated(provider));
436
500
  const cached = readCache(provider);
437
501
  const fresh = cached && Date.now() - new Date(cached.fetchedAt).getTime() < CATALOG_TTL_MS
438
502
  ? cached
439
503
  : undefined;
440
504
  if (fresh && !opts.refresh) {
441
- return { provider, entries: fresh.entries, origin: 'cache', fetchedAt: fresh.fetchedAt };
505
+ return {
506
+ provider,
507
+ entries: fresh.entries,
508
+ origin: 'cache',
509
+ fetchedAt: fresh.fetchedAt,
510
+ ...via,
511
+ };
442
512
  }
443
513
  if (!opts.offline) {
444
514
  try {
445
- const entries = await fetchCatalog(provider);
515
+ const entries = at
516
+ ? await fetchGatewayCatalog(provider, at)
517
+ : await fetchCatalog(provider);
446
518
  const fetchedAt = new Date().toISOString();
447
519
  store().put(catalogKey(provider), { provider, fetchedAt, entries });
448
- return { provider, entries, origin: 'live', fetchedAt };
520
+ return { provider, entries, origin: 'live', fetchedAt, ...via };
449
521
  }
450
522
  catch (err) {
451
523
  const problem = classify(err);
@@ -456,25 +528,34 @@ export async function loadCatalog(provider, opts = {}) {
456
528
  origin: 'stale',
457
529
  fetchedAt: cached.fetchedAt,
458
530
  problem,
531
+ ...via,
459
532
  };
460
533
  }
461
534
  return {
462
535
  provider,
463
- entries: curated(provider),
536
+ entries: fallback(),
464
537
  origin: 'curated',
465
538
  fetchedAt: new Date(0).toISOString(),
466
539
  problem,
540
+ ...via,
467
541
  };
468
542
  }
469
543
  }
470
544
  if (cached) {
471
- return { provider, entries: cached.entries, origin: 'stale', fetchedAt: cached.fetchedAt };
545
+ return {
546
+ provider,
547
+ entries: cached.entries,
548
+ origin: 'stale',
549
+ fetchedAt: cached.fetchedAt,
550
+ ...via,
551
+ };
472
552
  }
473
553
  return {
474
554
  provider,
475
- entries: curated(provider),
555
+ entries: fallback(),
476
556
  origin: 'curated',
477
557
  fetchedAt: new Date(0).toISOString(),
558
+ ...via,
478
559
  };
479
560
  }
480
561
  // ---------------------------------------------------------------------------
@@ -0,0 +1,9 @@
1
+ import type { Command } from '../command.ts';
2
+ export { install, installed, loader, rcFile } from '../completion-install.ts';
3
+ /**
4
+ * Prints the shell's side of tab completion. The script is a few lines that
5
+ * call back into `zen __complete`, so the project names and stored prompts it
6
+ * offers are always read from the machine it runs on, never baked in.
7
+ */
8
+ export declare const completion: Command;
9
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { one, parse } from "../args.js";
4
+ import { install, installed, loader, rcFiles, shellOf } from "../completion-install.js";
5
+ import { TEMPLATES } from "../scaffold.js";
6
+ import { bold, confirm, usageError, write } from "../term.js";
7
+ export { install, installed, loader, rcFile } from "../completion-install.js";
8
+ const USAGE = 'zen completion [<zsh|bash|fish>] [--install [--yes]]';
9
+ /**
10
+ * Prints the shell's side of tab completion. The script is a few lines that
11
+ * call back into `zen __complete`, so the project names and stored prompts it
12
+ * offers are always read from the machine it runs on, never baked in.
13
+ */
14
+ export const completion = {
15
+ summary: 'Print the tab-completion script for your shell.',
16
+ usage: USAGE,
17
+ banner: { head: 'Zenera', accent: 'Completion', subtitle: 'Shell Integration', hue: 'magenta' },
18
+ quiet: () => true,
19
+ details: [
20
+ 'Global npm installs set this up automatically. Open a new shell afterward.',
21
+ '',
22
+ 'Add one line to your shell’s startup file:',
23
+ ' zsh eval "$(zen completion zsh)" in ~/.zshrc',
24
+ ' bash eval "$(zen completion bash)" in ~/.bashrc',
25
+ ' fish zen completion fish | source in ~/.config/fish/config.fish',
26
+ '',
27
+ 'Or let zen do it — it appends that line to the file, once, after asking:',
28
+ ' zen completion --install for the shell you are in',
29
+ ' zen completion zsh --install --yes without asking',
30
+ '',
31
+ 'Then, in a new shell:',
32
+ ' zen meta run my-project /<Tab> the project’s stored prompts',
33
+ ' zen meta <Tab> run, prompts, resume, model …',
34
+ ' zen run <Tab> your projects',
35
+ '',
36
+ 'The prompts are the files in .github/prompts/ — the same ones an editor',
37
+ 'offers as slash commands. Where there is nothing to offer, file names are.',
38
+ ],
39
+ run: async (ctx) => {
40
+ const { values, positionals } = parse(ctx.args, { install: { type: 'boolean' }, yes: { type: 'boolean' } }, USAGE);
41
+ const name = one(positionals, 'shell', USAGE) ?? basename(process.env.SHELL ?? '');
42
+ const shell = shellOf(name);
43
+ if (!shell) {
44
+ throw usageError(name ? `no completion for "${name}"` : 'which shell?', 'try: zen completion zsh|bash|fish');
45
+ }
46
+ if (values.yes && !values.install) {
47
+ throw usageError('--yes only goes with --install', USAGE);
48
+ }
49
+ if (values.install) {
50
+ const files = rcFiles(shell).filter((file) => !installed(shell, file));
51
+ if (files.length === 0) {
52
+ write(`Already set up in ${rcFiles(shell).join(', ')}.`);
53
+ return;
54
+ }
55
+ const line = bold(loader(shell));
56
+ if (!values.yes && !(await confirm(`Add ${line} to ${files.join(', ')}?`, true))) {
57
+ write('Nothing changed.');
58
+ return;
59
+ }
60
+ for (const file of files) {
61
+ install(shell, file);
62
+ write(`Added to ${file}.`);
63
+ }
64
+ write('Open a new shell to use tab completion.');
65
+ return;
66
+ }
67
+ process.stdout.write(readFileSync(`${TEMPLATES}/completions/zen.${shell}`, 'utf8'));
68
+ },
69
+ };
70
+ //# sourceMappingURL=completion.js.map
@@ -1,5 +1,6 @@
1
1
  import { cache } from "./cache.js";
2
2
  import { check } from "./check.js";
3
+ import { completion } from "./completion.js";
3
4
  import { pack } from "./export.js";
4
5
  import { unpack } from "./import.js";
5
6
  import { init } from "./init.js";
@@ -39,6 +40,7 @@ export const COMMANDS = {
39
40
  memory,
40
41
  sandbox,
41
42
  cache,
43
+ completion,
42
44
  version,
43
45
  };
44
46
  /** Names that are not listed in help but still work. */
@@ -47,7 +47,7 @@ const DEFAULT_MODEL = {
47
47
  /**
48
48
  * What vectorises the project's memory, given the provider it ends up on.
49
49
  *
50
- * Taken from the same table `zen models pick` ranks by rather than written out
50
+ * Taken from the catalogue's `PREFERRED` table rather than written out
51
51
  * again here, so the scaffold cannot drift from what the catalogue calls
52
52
  * current. Anthropic publishes no embeddings API, and a `--model` naming a
53
53
  * provider we do not ship gives nothing to guess from; both scaffold without