@zenera/cli 1.1.24 → 1.1.26

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 (199) hide show
  1. package/README.md +65 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +31 -1
  7. package/dist/catalog.js +140 -7
  8. package/dist/commands/index.js +2 -0
  9. package/dist/commands/inspect.d.ts +52 -2
  10. package/dist/commands/inspect.js +220 -31
  11. package/dist/commands/memory.js +156 -32
  12. package/dist/commands/meta.js +333 -24
  13. package/dist/commands/models.js +97 -12
  14. package/dist/commands/open.js +4 -4
  15. package/dist/commands/run.js +38 -4
  16. package/dist/commands/sandbox.js +148 -16
  17. package/dist/commands/view.d.ts +8 -0
  18. package/dist/commands/view.js +114 -0
  19. package/dist/engine.d.ts +8 -1
  20. package/dist/engine.js +7 -0
  21. package/dist/eventlog.d.ts +64 -0
  22. package/dist/eventlog.js +170 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +35 -3
  25. package/dist/main.js +0 -0
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +43 -0
  28. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  29. package/dist/meta/finetune/analyze-brief.js +131 -0
  30. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  31. package/dist/meta/finetune/apply-brief.js +247 -0
  32. package/dist/meta/finetune/brief.d.ts +45 -0
  33. package/dist/meta/finetune/brief.js +158 -0
  34. package/dist/meta/finetune/children.d.ts +8 -0
  35. package/dist/meta/finetune/children.js +76 -0
  36. package/dist/meta/finetune/command.d.ts +30 -0
  37. package/dist/meta/finetune/command.js +516 -0
  38. package/dist/meta/finetune/cost.d.ts +46 -0
  39. package/dist/meta/finetune/cost.js +105 -0
  40. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  41. package/dist/meta/finetune/dataset/anchors.js +239 -0
  42. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  43. package/dist/meta/finetune/dataset/changes.js +319 -0
  44. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  45. package/dist/meta/finetune/dataset/command.js +1011 -0
  46. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/drift.js +71 -0
  48. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  49. package/dist/meta/finetune/dataset/export.js +51 -0
  50. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  51. package/dist/meta/finetune/dataset/index.js +2 -0
  52. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  53. package/dist/meta/finetune/dataset/sample.js +197 -0
  54. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  55. package/dist/meta/finetune/dataset/schema.js +216 -0
  56. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  57. package/dist/meta/finetune/dataset/store.js +177 -0
  58. package/dist/meta/finetune/dataset/types.d.ts +127 -0
  59. package/dist/meta/finetune/dataset/types.js +39 -0
  60. package/dist/meta/finetune/failure.d.ts +15 -0
  61. package/dist/meta/finetune/failure.js +26 -0
  62. package/dist/meta/finetune/feedback.d.ts +57 -0
  63. package/dist/meta/finetune/feedback.js +104 -0
  64. package/dist/meta/finetune/guard.d.ts +29 -0
  65. package/dist/meta/finetune/guard.js +104 -0
  66. package/dist/meta/finetune/improvements.d.ts +98 -0
  67. package/dist/meta/finetune/improvements.js +501 -0
  68. package/dist/meta/finetune/index.d.ts +2 -0
  69. package/dist/meta/finetune/index.js +2 -0
  70. package/dist/meta/finetune/journal.d.ts +35 -0
  71. package/dist/meta/finetune/journal.js +135 -0
  72. package/dist/meta/finetune/live.d.ts +84 -0
  73. package/dist/meta/finetune/live.js +272 -0
  74. package/dist/meta/finetune/loop.d.ts +3 -0
  75. package/dist/meta/finetune/loop.js +163 -0
  76. package/dist/meta/finetune/memories.d.ts +47 -0
  77. package/dist/meta/finetune/memories.js +154 -0
  78. package/dist/meta/finetune/recheck.d.ts +58 -0
  79. package/dist/meta/finetune/recheck.js +311 -0
  80. package/dist/meta/finetune/report.d.ts +40 -0
  81. package/dist/meta/finetune/report.js +1397 -0
  82. package/dist/meta/finetune/retry.d.ts +24 -0
  83. package/dist/meta/finetune/retry.js +96 -0
  84. package/dist/meta/finetune/sampler.d.ts +18 -0
  85. package/dist/meta/finetune/sampler.js +47 -0
  86. package/dist/meta/finetune/system.d.ts +27 -0
  87. package/dist/meta/finetune/system.js +132 -0
  88. package/dist/meta/finetune/train.d.ts +43 -0
  89. package/dist/meta/finetune/train.js +425 -0
  90. package/dist/meta/finetune/tui.d.ts +16 -0
  91. package/dist/meta/finetune/tui.js +316 -0
  92. package/dist/meta/finetune/tuning.d.ts +193 -0
  93. package/dist/meta/finetune/tuning.js +308 -0
  94. package/dist/meta/finetune/usage.d.ts +66 -0
  95. package/dist/meta/finetune/usage.js +260 -0
  96. package/dist/meta/finetune/view.d.ts +9 -0
  97. package/dist/meta/finetune/view.js +27 -0
  98. package/dist/meta/host.d.ts +15 -0
  99. package/dist/meta/host.js +21 -0
  100. package/dist/meta/index.d.ts +11 -0
  101. package/dist/meta/index.js +11 -0
  102. package/dist/meta/launch.d.ts +81 -0
  103. package/dist/meta/launch.js +286 -0
  104. package/dist/meta/model.d.ts +44 -0
  105. package/dist/meta/model.js +119 -0
  106. package/dist/meta/prompts.d.ts +18 -0
  107. package/dist/meta/prompts.js +61 -0
  108. package/dist/meta/relay.d.ts +20 -0
  109. package/dist/meta/relay.js +123 -0
  110. package/dist/meta/tokens.d.ts +52 -0
  111. package/dist/meta/tokens.js +226 -0
  112. package/dist/meta/window.d.ts +61 -0
  113. package/dist/meta/window.js +198 -0
  114. package/dist/meta/wire.d.ts +42 -0
  115. package/dist/meta/wire.js +129 -0
  116. package/dist/meta.d.ts +71 -1
  117. package/dist/meta.js +303 -6
  118. package/dist/modelref.d.ts +16 -0
  119. package/dist/modelref.js +41 -0
  120. package/dist/narrate.js +5 -2
  121. package/dist/podman.d.ts +26 -0
  122. package/dist/podman.js +83 -3
  123. package/dist/projects.d.ts +0 -5
  124. package/dist/projects.js +2 -15
  125. package/dist/resources.d.ts +215 -0
  126. package/dist/resources.js +856 -0
  127. package/dist/sandbox.d.ts +2 -0
  128. package/dist/sandbox.js +10 -3
  129. package/dist/scaffold.d.ts +15 -7
  130. package/dist/scaffold.js +24 -67
  131. package/dist/session.js +4 -29
  132. package/dist/term.d.ts +2 -0
  133. package/dist/term.js +11 -2
  134. package/dist/trace.d.ts +5 -0
  135. package/dist/trace.js +107 -7
  136. package/dist/tui/app.js +19 -5
  137. package/dist/tui/links.d.ts +27 -0
  138. package/dist/tui/links.js +142 -0
  139. package/dist/tui/models.d.ts +35 -0
  140. package/dist/tui/models.js +447 -0
  141. package/dist/tui/sandbox.d.ts +10 -0
  142. package/dist/tui/sandbox.js +131 -0
  143. package/dist/tui/wrap.d.ts +5 -0
  144. package/dist/tui/wrap.js +12 -0
  145. package/dist/usage.d.ts +78 -0
  146. package/dist/usage.js +51 -0
  147. package/package.json +4 -2
  148. package/templates/editor/.github/copilot-instructions.md +60 -145
  149. package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
  150. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  151. package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
  152. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  153. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  154. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  155. package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
  156. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
  157. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  158. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  159. package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
  160. package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
  161. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  162. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  163. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  164. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  165. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  166. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
  167. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  168. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  169. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  170. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
  171. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  172. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  173. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  174. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  175. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  176. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  177. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  178. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  179. package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
  180. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
  181. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  182. package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
  183. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  184. package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
  185. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
  186. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  187. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
  188. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  189. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  190. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  191. package/templates/editor/.vscode/settings.json +1 -0
  192. package/templates/project/agents/files-instructions.md +29 -20
  193. package/templates/project/agents/fork-instructions.md +78 -28
  194. package/templates/project/agents/memory-instructions.md +222 -494
  195. package/templates/project/agents/tools-instructions.md +14 -4
  196. package/templates/project/gitignore +2 -0
  197. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  198. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
  199. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
@@ -0,0 +1,142 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ /** Exactly the trees the file tools reach, under the same containment rule. */
3
+ export function mountsOf(files) {
4
+ const names = [...(files.mount ? [files.mount] : []), ...files.mounts.map((m) => m.at)];
5
+ return {
6
+ names: names.sort((a, b) => b.length - a.length),
7
+ resolve: (path) => {
8
+ try {
9
+ return files.within(path);
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ },
15
+ };
16
+ }
17
+ const FILE = 'file://';
18
+ /** Prose punctuation that ends a sentence, not a path. */
19
+ const TRAILING = /[.,;:!?'"]+$/;
20
+ /** The host `file:` url a mounted path names, or nothing. */
21
+ export function mountUrl(token, mounts) {
22
+ let path = token;
23
+ let fragment = '';
24
+ if (path.startsWith(FILE)) {
25
+ path = path.slice(FILE.length);
26
+ const cut = path.search(/[?#]/);
27
+ if (cut >= 0) {
28
+ fragment = path.slice(cut);
29
+ path = path.slice(0, cut);
30
+ }
31
+ try {
32
+ path = decodeURIComponent(path);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ // `within` also takes relative paths and `/`; only a mount name counts here.
39
+ if (!mounts.names.some((n) => path === n || path.startsWith(`${n}/`))) {
40
+ return undefined;
41
+ }
42
+ const host = mounts.resolve(path);
43
+ return host ? pathToFileURL(host).href + fragment : undefined;
44
+ }
45
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, (c) => `\\${c}`);
46
+ /** Every spelling of a mounted path in running text. */
47
+ function pattern(mounts) {
48
+ const names = mounts.names.map(escaped).join('|');
49
+ return new RegExp(`(?<![\\w/.:-])(?:${FILE})?(?:${names})(?![\\w-]|\\.\\w)(?:/[^\\s<>"'\`()[\\]{}|\\\\^]*)?`, 'g');
50
+ }
51
+ /** Each mounted path in `text`, without the punctuation that follows it. */
52
+ function* matches(text, mounts) {
53
+ if (mounts.names.length === 0) {
54
+ return;
55
+ }
56
+ for (const m of text.matchAll(pattern(mounts))) {
57
+ const token = m[0].replace(TRAILING, '');
58
+ if (token) {
59
+ yield { at: m.index, token };
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * The same spans with the same text, mounted paths carrying a host `href`.
65
+ *
66
+ * Only `href` changes, so every width measured from `span.text` still holds.
67
+ */
68
+ export function linkSpans(spans, mounts) {
69
+ const out = [];
70
+ for (const span of spans) {
71
+ if (span.href) {
72
+ const url = mountUrl(span.href, mounts);
73
+ out.push(url ? { ...span, href: url } : span);
74
+ continue;
75
+ }
76
+ let from = 0;
77
+ for (const { at, token } of matches(span.text, mounts)) {
78
+ const url = mountUrl(token, mounts);
79
+ if (!url) {
80
+ continue;
81
+ }
82
+ if (at > from) {
83
+ out.push({ ...span, text: span.text.slice(from, at) });
84
+ }
85
+ out.push({ ...span, text: token, href: url });
86
+ from = at + token.length;
87
+ }
88
+ if (from === 0) {
89
+ out.push(span);
90
+ }
91
+ else if (from < span.text.length) {
92
+ out.push({ ...span, text: span.text.slice(from) });
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ /** A block whose prose and cells link their mounted paths; a fence is left alone. */
98
+ export function linkBlock(block, mounts) {
99
+ return {
100
+ ...block,
101
+ ...(block.spans ? { spans: linkSpans(block.spans, mounts) } : {}),
102
+ ...(block.cells
103
+ ? { cells: block.cells.map((row) => row.map((cell) => linkSpans(cell, mounts))) }
104
+ : {}),
105
+ };
106
+ }
107
+ /** For a terminal with no hyperlinks: every `file:///<mount>/…` url, rewritten to the host. */
108
+ export function hostUrls(text, mounts) {
109
+ let out = '';
110
+ let from = 0;
111
+ for (const { at, token } of matches(text, mounts)) {
112
+ const url = token.startsWith(FILE) ? mountUrl(token, mounts) : undefined;
113
+ if (!url) {
114
+ continue;
115
+ }
116
+ out += text.slice(from, at) + url;
117
+ from = at + token.length;
118
+ }
119
+ return out + text.slice(from);
120
+ }
121
+ /** `text` as an OSC 8 hyperlink to `href`. */
122
+ export function hyperlink(href, text) {
123
+ return `\u001b]8;;${href}\u001b\\${text}\u001b]8;;\u001b\\`;
124
+ }
125
+ /** Whether this terminal turns OSC 8 into a link; an unknown one is assumed not to. */
126
+ export function hyperlinks(env = process.env) {
127
+ const forced = env.FORCE_HYPERLINK;
128
+ if (forced !== undefined && forced !== '') {
129
+ return forced !== '0' && forced !== 'false';
130
+ }
131
+ if (env.WT_SESSION || env.KITTY_WINDOW_ID || env.KONSOLE_VERSION) {
132
+ return true;
133
+ }
134
+ if (['iTerm.app', 'vscode', 'WezTerm', 'ghostty'].includes(env.TERM_PROGRAM ?? '')) {
135
+ return true;
136
+ }
137
+ if (env.TERM === 'xterm-kitty' || env.TERM === 'xterm-ghostty') {
138
+ return true;
139
+ }
140
+ return Number(env.VTE_VERSION ?? 0) >= 5000;
141
+ }
142
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1,35 @@
1
+ import { type Balance, type Catalog, type CatalogEntry } from '../catalog.ts';
2
+ import type { Provider } from '../keys.ts';
3
+ export interface ProviderRow {
4
+ provider: Provider;
5
+ /** where its credential came from — `environment`, `keyring` — or '' for none */
6
+ credential: string;
7
+ /** what the cached listing holds, read without asking anyone */
8
+ models: number;
9
+ origin: Catalog['origin'];
10
+ fetchedAt: string;
11
+ }
12
+ /** What asking a model one question found out. */
13
+ export interface Verdict {
14
+ state: 'live' | 'dead' | 'blocked' | 'unknown';
15
+ ms?: number;
16
+ detail?: string;
17
+ fix?: string;
18
+ dimensions?: number;
19
+ }
20
+ export interface BrowseOptions {
21
+ providers: readonly ProviderRow[];
22
+ /** opens straight into this provider's listing */
23
+ initial?: Provider;
24
+ theme?: string;
25
+ load(provider: Provider, refresh: boolean): Promise<Catalog>;
26
+ /** what the account has left to spend, for providers that say */
27
+ balance?(provider: Provider): Promise<Balance | undefined>;
28
+ test(entry: CatalogEntry): Promise<Verdict>;
29
+ }
30
+ /**
31
+ * Runs the browser until the person picks a model or leaves. Resolves to the
32
+ * picked entry, or undefined when nothing was picked.
33
+ */
34
+ export declare function browse(options: BrowseOptions): Promise<CatalogEntry | undefined>;
35
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1,447 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // ---------------------------------------------------------------------------
3
+ // zen models browse
4
+ //
5
+ // The catalog as something to walk rather than something to grep: providers,
6
+ // then what one of them serves, then one model in full — with a key to ask it
7
+ // a real question and a key to take it.
8
+ //
9
+ // Drawn on STDERR, like the banner and every other piece of narration, so the
10
+ // one thing that reaches stdout is the ref picked at the end. That is what
11
+ // makes `ref=$(zen models browse)` work the way `zen models pick` does: the
12
+ // person sees the interface, the shell sees the answer.
13
+ //
14
+ // Nothing here knows how to list or probe a model. Both arrive as callbacks
15
+ // from commands/models.ts, which already owns credentials and the catalog, so
16
+ // this file is layout and keys and nothing else.
17
+ // ---------------------------------------------------------------------------
18
+ import { Box, render, Text, useApp, useInput, useStdout } from 'ink';
19
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
20
+ import { byGroup, isPartner, matches, perMillion, } from "../catalog.js";
21
+ import { ago } from "../term.js";
22
+ import { resolveTheme } from "./theme.js";
23
+ import { clip, scrollTop, wrap } from "./wrap.js";
24
+ const ROLES = ['chat', 'embedding', 'image', 'audio'];
25
+ const message = (err) => (err instanceof Error ? err.message : String(err));
26
+ const thousands = (n) => n === undefined ? '' : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
27
+ const ms = (n) => (n < 1000 ? `${n}ms` : `${(n / 1000).toFixed(1)}s`);
28
+ const money = (n) => {
29
+ const abs = Math.abs(n);
30
+ return `${n < 0 ? '-' : ''}$${abs !== 0 && abs < 0.01 ? abs.toFixed(4) : abs.toFixed(2)}`;
31
+ };
32
+ /** The balance as one line; `short` for the providers screen. */
33
+ function account(b, short) {
34
+ const parts = [];
35
+ if (b.credits !== undefined) {
36
+ parts.push(`balance ${money(b.credits)}`);
37
+ }
38
+ if (b.cap && !short) {
39
+ const reset = b.cap.reset ? ` ${b.cap.reset}` : '';
40
+ parts.push(`key cap ${money(b.cap.remaining)} left of ${money(b.cap.limit)}${reset}`);
41
+ }
42
+ if (b.free && !short) {
43
+ parts.push(`free ${b.free.remaining}/${b.free.limit} requests left today`);
44
+ }
45
+ return parts.join(' · ');
46
+ }
47
+ function freshness(origin, fetchedAt) {
48
+ switch (origin) {
49
+ case 'curated':
50
+ return 'built-in list';
51
+ case 'stale':
52
+ return `stale, ${ago(fetchedAt)}`;
53
+ case 'live':
54
+ return 'fetched now';
55
+ default:
56
+ return `cached ${ago(fetchedAt)}`;
57
+ }
58
+ }
59
+ function said(verdict, theme) {
60
+ if (verdict === undefined) {
61
+ return undefined;
62
+ }
63
+ if (verdict === 'asking') {
64
+ return { text: '… asking', color: theme.warn };
65
+ }
66
+ switch (verdict.state) {
67
+ case 'live':
68
+ return {
69
+ text: `✓ answers ${verdict.ms === undefined ? '' : ms(verdict.ms)}`.trim(),
70
+ color: theme.code.color,
71
+ };
72
+ case 'blocked':
73
+ return { text: '! blocked', color: theme.warn };
74
+ case 'dead':
75
+ return { text: '✗ refused', color: theme.line.error.color };
76
+ default:
77
+ return { text: '? no answer', color: theme.chrome.color };
78
+ }
79
+ }
80
+ /** One row, never wrapped: a wrapped row is a row the frame did not budget for. */
81
+ function Row({ children, ...style }) {
82
+ return (_jsx(Text, { wrap: "truncate-end", ...style, children: children }));
83
+ }
84
+ function Keys({ theme, text }) {
85
+ return _jsx(Row, { color: theme.chrome.color, children: text });
86
+ }
87
+ function Browser({ options, theme, onPick }) {
88
+ const { exit } = useApp();
89
+ const { stdout } = useStdout();
90
+ const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
91
+ useEffect(() => {
92
+ const resized = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
93
+ stdout.on('resize', resized);
94
+ return () => {
95
+ stdout.off('resize', resized);
96
+ };
97
+ }, [stdout]);
98
+ const start = options.providers.findIndex((p) => p.provider === options.initial);
99
+ const [screen, setScreen] = useState(start >= 0 ? 'models' : 'providers');
100
+ const [at, setAt] = useState(Math.max(0, start));
101
+ const [catalogs, setCatalogs] = useState(new Map());
102
+ const [loading, setLoading] = useState();
103
+ const [failure, setFailure] = useState();
104
+ const [query, setQuery] = useState('');
105
+ const [typing, setTyping] = useState(false);
106
+ const [role, setRole] = useState(0);
107
+ const [selected, setSelected] = useState(0);
108
+ const [verdicts, setVerdicts] = useState(new Map());
109
+ const [balances, setBalances] = useState(new Map());
110
+ const [closing, setClosing] = useState(false);
111
+ const top = useRef(0);
112
+ const provider = options.providers[at].provider;
113
+ const catalog = catalogs.get(provider);
114
+ const load = (p, refresh) => {
115
+ setLoading(p);
116
+ setFailure(undefined);
117
+ options.load(p, refresh).then((cat) => {
118
+ setCatalogs((prev) => new Map(prev).set(p, cat));
119
+ setLoading((now) => (now === p ? undefined : now));
120
+ }, (err) => {
121
+ setFailure(message(err));
122
+ setLoading((now) => (now === p ? undefined : now));
123
+ });
124
+ };
125
+ const fetchBalance = (p) => {
126
+ options.balance?.(p).then((b) => b && setBalances((prev) => new Map(prev).set(p, b)), (err) => setBalances((prev) => new Map(prev).set(p, message(err))));
127
+ };
128
+ useEffect(() => {
129
+ if (start >= 0) {
130
+ load(provider, false);
131
+ }
132
+ for (const p of options.providers) {
133
+ if (p.credential) {
134
+ fetchBalance(p.provider);
135
+ }
136
+ }
137
+ // Once, for `zen models browse <provider>`; every later open is a key.
138
+ }, []);
139
+ useEffect(() => {
140
+ if (closing) {
141
+ exit();
142
+ }
143
+ }, [closing, exit]);
144
+ // Only the roles this listing actually has: cycling through `audio` on a
145
+ // provider with no audio models is a keypress spent on an empty screen.
146
+ const present = useMemo(() => [
147
+ undefined,
148
+ ...ROLES.filter((r) => catalog?.entries.some((e) => e.roles.includes(r))),
149
+ ], [catalog]);
150
+ const only = present[role % present.length];
151
+ const rows = useMemo(() => (catalog?.entries ?? [])
152
+ .filter((e) => matches(e, query, { roles: only ? [only] : [] }))
153
+ .sort(byGroup), [catalog, query, only]);
154
+ const index = Math.min(selected, Math.max(0, rows.length - 1));
155
+ const current = rows[index];
156
+ const open = (next) => {
157
+ const p = options.providers[next].provider;
158
+ setAt(next);
159
+ setScreen('models');
160
+ setQuery('');
161
+ setTyping(false);
162
+ setRole(0);
163
+ setSelected(0);
164
+ top.current = 0;
165
+ if (!catalogs.has(p)) {
166
+ load(p, false);
167
+ }
168
+ };
169
+ const ask = (entry) => {
170
+ if (verdicts.get(entry.ref) === 'asking') {
171
+ return;
172
+ }
173
+ setVerdicts((prev) => new Map(prev).set(entry.ref, 'asking'));
174
+ options.test(entry).then((v) => setVerdicts((prev) => new Map(prev).set(entry.ref, v)), (err) => setVerdicts((prev) => new Map(prev).set(entry.ref, { state: 'unknown', detail: message(err) })));
175
+ };
176
+ const pick = (entry) => {
177
+ onPick(entry);
178
+ setClosing(true);
179
+ };
180
+ // Everything but the list is a fixed number of rows, and the list gets
181
+ // what is left. A row is kept in hand: a frame as tall as the viewport is
182
+ // the one Ink cannot erase (see tui/app.tsx).
183
+ const problem = catalog?.problem;
184
+ const balance = balances.get(provider);
185
+ const funds = balance === undefined
186
+ ? []
187
+ : wrap(typeof balance === 'string'
188
+ ? `balance could not be read: ${balance}`
189
+ : account(balance, false) || 'balance: nothing reported', Math.max(20, size.columns - 1));
190
+ const short = typeof balance === 'string' || (balance?.credits !== undefined && balance.credits <= 0);
191
+ const chrome = 7 + (problem ? 1 : 0) + funds.length;
192
+ const height = Math.max(3, size.rows - 1 - chrome);
193
+ top.current = scrollTop(index, top.current, height, rows.length);
194
+ useInput((input, key) => {
195
+ if (closing) {
196
+ return;
197
+ }
198
+ const quit = () => setClosing(true);
199
+ if (screen === 'providers') {
200
+ const n = options.providers.length;
201
+ if (key.upArrow || input === 'k') {
202
+ setAt((i) => (i + n - 1) % n);
203
+ }
204
+ else if (key.downArrow || input === 'j') {
205
+ setAt((i) => (i + 1) % n);
206
+ }
207
+ else if (key.return || key.rightArrow || input === 'l') {
208
+ open(at);
209
+ }
210
+ else if (key.escape || input === 'q') {
211
+ quit();
212
+ }
213
+ return;
214
+ }
215
+ if (screen === 'detail') {
216
+ if (!current) {
217
+ setScreen('models');
218
+ }
219
+ else if (input === 't') {
220
+ ask(current);
221
+ }
222
+ else if (key.return || input === 'p') {
223
+ pick(current);
224
+ }
225
+ else if (key.escape || key.leftArrow || input === 'h') {
226
+ setScreen('models');
227
+ }
228
+ else if (input === 'q') {
229
+ quit();
230
+ }
231
+ return;
232
+ }
233
+ const move = (by) => setSelected(Math.max(0, Math.min(rows.length - 1, index + by)));
234
+ if (key.upArrow) {
235
+ return move(-1);
236
+ }
237
+ if (key.downArrow) {
238
+ return move(1);
239
+ }
240
+ if (key.pageUp) {
241
+ return move(-height);
242
+ }
243
+ if (key.pageDown) {
244
+ return move(height);
245
+ }
246
+ if (typing) {
247
+ if (key.return) {
248
+ setTyping(false);
249
+ }
250
+ else if (key.escape) {
251
+ setQuery('');
252
+ setTyping(false);
253
+ }
254
+ else if (key.backspace || key.delete) {
255
+ setQuery((q) => q.slice(0, -1));
256
+ setSelected(0);
257
+ }
258
+ else if (input && !key.ctrl && !key.meta && !key.tab) {
259
+ // Fast typing or a paste can arrive as one chunk with the Enter
260
+ // inside it: keep the text, honour the Enter, drop the rest.
261
+ const [text = '', ...after] = input.split(/[\r\n]/);
262
+ setQuery((q) => q + text.replace(/[\u0000-\u001f\u007f]/g, ''));
263
+ setSelected(0);
264
+ if (after.length > 0) {
265
+ setTyping(false);
266
+ }
267
+ }
268
+ return;
269
+ }
270
+ if (input === 'k') {
271
+ move(-1);
272
+ }
273
+ else if (input === 'j') {
274
+ move(1);
275
+ }
276
+ else if (key.home || input === 'g') {
277
+ setSelected(0);
278
+ }
279
+ else if (key.end || input === 'G') {
280
+ setSelected(Math.max(0, rows.length - 1));
281
+ }
282
+ else if (input.startsWith('/')) {
283
+ // A pasted `/gpt-5` arrives as one chunk, not as a slash and then
284
+ // the rest.
285
+ setTyping(true);
286
+ setQuery(input.slice(1));
287
+ setSelected(0);
288
+ }
289
+ else if (key.tab) {
290
+ setRole((r) => (r + 1) % present.length);
291
+ setSelected(0);
292
+ }
293
+ else if ((key.return || key.rightArrow || input === 'l') && current) {
294
+ setScreen('detail');
295
+ }
296
+ else if (input === 't' && current) {
297
+ ask(current);
298
+ }
299
+ else if (input === 'p' && current) {
300
+ pick(current);
301
+ }
302
+ else if (input === 'r') {
303
+ load(provider, true);
304
+ fetchBalance(provider);
305
+ }
306
+ else if (key.escape && query) {
307
+ setQuery('');
308
+ setSelected(0);
309
+ }
310
+ else if (key.escape || key.leftArrow || input === 'h') {
311
+ setScreen('providers');
312
+ }
313
+ else if (input === 'q') {
314
+ quit();
315
+ }
316
+ });
317
+ if (closing) {
318
+ // An empty frame erases the last one, so the shell is left as it was
319
+ // found rather than with a stale picture of a list on it.
320
+ return null;
321
+ }
322
+ const width = Math.max(20, size.columns - 1);
323
+ if (screen === 'providers') {
324
+ const nameW = Math.max(...options.providers.map((p) => p.provider.length)) + 2;
325
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Row, { bold: true, color: theme.accent, children: "Providers" }), _jsx(Text, { children: " " }), options.providers.map((p, i) => {
326
+ const here = i === at;
327
+ const cat = catalogs.get(p.provider);
328
+ const count = cat?.entries.length ?? p.models;
329
+ const fresh = cat
330
+ ? freshness(cat.origin, cat.fetchedAt)
331
+ : freshness(p.origin, p.fetchedAt);
332
+ const b = balances.get(p.provider);
333
+ return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', p.provider.padEnd(nameW)] }), p.credential ? (_jsx(Text, { color: theme.code.color, children: p.credential.padEnd(15) })) : (_jsx(Text, { color: theme.line.error.color, children: 'no credential'.padEnd(15) })), _jsxs(Text, { color: theme.chrome.color, children: [`${count} model${count === 1 ? '' : 's'}`.padEnd(13), p.credential ? fresh : `zen key add ${p.provider}`] }), b && typeof b !== 'string' ? (_jsx(Text, { color: theme.code.color, children: ` ${account(b, true)}` })) : null] }, p.provider));
334
+ }), _jsx(Text, { children: " " }), _jsx(Keys, { theme: theme, text: "\u2191\u2193 move \u00B7 enter open \u00B7 q quit" })] }));
335
+ }
336
+ if (screen === 'detail' && current) {
337
+ return (_jsx(Detail, { entry: current, catalog: catalog, verdict: verdicts.get(current.ref), theme: theme, width: width, rows: size.rows - 1 }));
338
+ }
339
+ const header = catalog
340
+ ? `${provider} · ${catalog.entries.length} models · ${freshness(catalog.origin, catalog.fetchedAt)}`
341
+ : provider;
342
+ const withMaker = rows.some((e) => e.publisher);
343
+ const priced = rows.some((e) => e.pricing);
344
+ const ROLE_W = Math.max(4, ...rows.map((e) => e.roles.join('+').length)) + 2;
345
+ const CTX_W = 9;
346
+ const PRICE_W = 10;
347
+ const fixed = (withMaker ? 12 : 0) + ROLE_W + CTX_W + (priced ? PRICE_W * 2 : 0) + 'test'.length + 2;
348
+ const idW = Math.max(10, Math.min(46, Math.max(8, ...rows.map((e) => e.id.length)) + 2, width - fixed));
349
+ const shown = rows.slice(top.current, top.current + height);
350
+ const filter = typing ? `/${query}▌` : query ? `/${query}` : '';
351
+ const columns = [
352
+ ` ${'model'.padEnd(idW)}`,
353
+ withMaker ? 'maker'.padEnd(12) : '',
354
+ 'role'.padEnd(ROLE_W),
355
+ 'context'.padStart(CTX_W - 2).padEnd(CTX_W),
356
+ priced ? 'in $/1M'.padStart(PRICE_W - 2).padEnd(PRICE_W) : '',
357
+ priced ? 'out $/1M'.padStart(PRICE_W - 2).padEnd(PRICE_W) : '',
358
+ 'test',
359
+ ].join('');
360
+ let body;
361
+ if (failure) {
362
+ body = _jsx(Row, { color: theme.line.error.color, children: failure });
363
+ }
364
+ else if (loading === provider || !catalog) {
365
+ body = _jsxs(Row, { color: theme.warn, children: ["listing ", provider, " \u2026"] });
366
+ }
367
+ else if (rows.length === 0) {
368
+ body = (_jsx(Row, { color: theme.chrome.color, children: query ? `nothing matches "${query}"` : 'nothing listed' }));
369
+ }
370
+ else {
371
+ body = (_jsx(_Fragment, { children: shown.map((e, i) => {
372
+ const here = top.current + i === index;
373
+ const v = said(verdicts.get(e.ref), theme);
374
+ return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', clip(e.id, idW - 2).padEnd(idW)] }), withMaker ? (_jsx(Text, { color: isPartner(e) ? theme.warn : theme.code.color, children: clip(e.publisher ?? '', 11).padEnd(12) })) : null, _jsxs(Text, { color: theme.chrome.color, children: [e.roles.join('+').padEnd(ROLE_W), thousands(e.contextLength)
375
+ .padStart(CTX_W - 2)
376
+ .padEnd(CTX_W)] }), priced ? (_jsxs(Text, { color: e.pricing?.free ? theme.code.color : undefined, children: [perMillion(e.pricing?.prompt)
377
+ .padStart(PRICE_W - 2)
378
+ .padEnd(PRICE_W), (e.pricing?.free ? '' : perMillion(e.pricing?.completion))
379
+ .padStart(PRICE_W - 2)
380
+ .padEnd(PRICE_W)] })) : null, v ? _jsx(Text, { color: v.color, children: v.text }) : null] }, e.ref));
381
+ }) }));
382
+ }
383
+ const preview = current ? [current.name, current.description].filter(Boolean).join(' — ') : '';
384
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Row, { children: [_jsx(Text, { bold: true, color: theme.accent, children: header }), _jsxs(Text, { color: theme.chrome.color, children: [' role: ', only ?? 'all', filter ? ' ' : ''] }), _jsx(Text, { color: theme.code.color, children: filter })] }), problem ? (_jsxs(Row, { color: theme.warn, children: ["could not be listed: ", problem.detail ?? 'no reason given'] })) : null, funds.map((line, i) => (_jsx(Row, { color: short ? theme.warn : theme.code.color, children: line }, `funds${i}`))), _jsx(Text, { children: " " }), _jsx(Row, { bold: true, color: theme.chrome.color, children: columns }), _jsx(Box, { flexDirection: "column", height: height, children: body }), _jsx(Row, { color: theme.chrome.color, children: clip(preview, width) || ' ' }), _jsx(Row, { color: theme.chrome.color, children: rows.length === 0
385
+ ? ' '
386
+ : `${index + 1} of ${rows.length}${rows.length < (catalog?.entries.length ?? 0) ? ` (${catalog.entries.length} in all)` : ''}` }), _jsx(Keys, { theme: theme, text: typing
387
+ ? 'type to filter · enter keep · esc clear · ↑↓ move'
388
+ : '↑↓ · / filter · tab role · ⏎ more · t test · p pick · r reload · esc back' })] }));
389
+ }
390
+ function Detail({ entry, catalog, verdict, theme, width, rows }) {
391
+ const facts = [];
392
+ const add = (label, value) => {
393
+ if (value) {
394
+ facts.push([label, value]);
395
+ }
396
+ };
397
+ add('name', entry.name);
398
+ add('roles', entry.roles.join(', '));
399
+ add('context', entry.contextLength ? `${entry.contextLength.toLocaleString()} tokens` : undefined);
400
+ add('max output', entry.maxOutputTokens ? `${entry.maxOutputTokens.toLocaleString()} tokens` : undefined);
401
+ add('dimensions', entry.dimensions ? String(entry.dimensions) : undefined);
402
+ add('input', entry.modalities?.input?.join(', '));
403
+ add('output', entry.modalities?.output?.join(', '));
404
+ add('supports', Object.entries(entry.supports ?? {})
405
+ .filter(([, on]) => on)
406
+ .map(([k]) => k)
407
+ .join(', '));
408
+ add('pricing', entry.pricing?.free
409
+ ? 'free'
410
+ : entry.pricing?.prompt
411
+ ? `${perMillion(entry.pricing.prompt)} per 1M input tokens, ${perMillion(entry.pricing.completion) || '?'} per 1M output tokens`
412
+ : undefined);
413
+ add('released', entry.created);
414
+ add('source', entry.source === 'live' && catalog
415
+ ? `${entry.provider}, ${freshness(catalog.origin, catalog.fetchedAt)}`
416
+ : 'built-in list');
417
+ const v = said(verdict, theme);
418
+ const reason = verdict && verdict !== 'asking'
419
+ ? [
420
+ verdict.dimensions ? `${verdict.dimensions} dims` : '',
421
+ verdict.state === 'live' ? '' : (verdict.detail ?? ''),
422
+ verdict.fix ? `fix: ${verdict.fix}` : '',
423
+ ]
424
+ .filter(Boolean)
425
+ .join(' · ')
426
+ : '';
427
+ // Header, blank, facts, the verdict row, blank, keys — the description has
428
+ // whatever is left, and is cut rather than allowed to push the keys off.
429
+ const room = Math.max(0, rows - facts.length - 6);
430
+ const about = entry.description ? wrap(entry.description, width - 2).slice(0, room) : [];
431
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Row, { bold: true, color: theme.accent, children: entry.ref }), _jsx(Text, { children: " " }), facts.map(([label, value]) => (_jsxs(Row, { children: [_jsx(Text, { color: theme.chrome.color, children: ` ${label.padEnd(12)}` }), _jsx(Text, { children: value })] }, label))), _jsxs(Row, { children: [_jsxs(Text, { color: theme.chrome.color, children: [' ', 'asked'.padEnd(12)] }), v ? (_jsx(Text, { color: v.color, children: v.text })) : (_jsx(Text, { color: theme.chrome.color, children: "not yet \u2014 t to ask it one question" })), reason ? _jsx(Text, { color: v?.color, children: ` ${reason}` }) : null] }), about.length > 0 ? _jsx(Text, { children: " " }) : null, about.map((line, i) => (_jsx(Row, { color: theme.chrome.color, children: ` ${line}` }, i))), _jsx(Text, { children: " " }), _jsx(Keys, { theme: theme, text: "t test \u00B7 p pick (prints the ref) \u00B7 esc back \u00B7 q quit" })] }));
432
+ }
433
+ /**
434
+ * Runs the browser until the person picks a model or leaves. Resolves to the
435
+ * picked entry, or undefined when nothing was picked.
436
+ */
437
+ export async function browse(options) {
438
+ // Asked before Ink takes the terminal, as tui/app.tsx does.
439
+ const theme = await resolveTheme(options.theme);
440
+ let chosen;
441
+ const instance = render(_jsx(Browser, { options: options, theme: theme, onPick: (entry) => {
442
+ chosen = entry;
443
+ } }), { stdout: process.stderr });
444
+ await instance.waitUntilExit();
445
+ return chosen;
446
+ }
447
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1,10 @@
1
+ import { type ResourceSampler } from '../resources.ts';
2
+ export interface WatchOptions {
3
+ sampler: ResourceSampler;
4
+ intervalMs: number;
5
+ explain: boolean;
6
+ theme?: string;
7
+ }
8
+ /** Runs the live screen until the person leaves it. */
9
+ export declare function watch(options: WatchOptions): Promise<void>;
10
+ //# sourceMappingURL=sandbox.d.ts.map