@bytevion/cli 0.2.0 → 0.4.0

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 (61) hide show
  1. package/dist/base.js +8 -1
  2. package/dist/commands/opt/preset.js +8 -5
  3. package/dist/commands/opt/show.js +20 -6
  4. package/dist/commands/providers/add.js +10 -2
  5. package/dist/commands/run.js +1 -1
  6. package/dist/commands/setup.d.ts +1 -0
  7. package/dist/commands/setup.js +210 -39
  8. package/dist/commands/usage.js +32 -8
  9. package/dist/hooks/init/home.js +41 -5
  10. package/dist/lib/api.d.ts +1 -0
  11. package/dist/lib/api.js +6 -0
  12. package/dist/lib/friendly.d.ts +8 -0
  13. package/dist/lib/friendly.js +86 -0
  14. package/dist/lib/home.d.ts +16 -0
  15. package/dist/lib/home.js +97 -0
  16. package/dist/lib/output.d.ts +3 -0
  17. package/dist/lib/output.js +43 -0
  18. package/dist/lib/presets.d.ts +9 -0
  19. package/dist/lib/presets.js +40 -0
  20. package/dist/lib/tui.d.ts +105 -0
  21. package/dist/lib/tui.gate.test.d.ts +1 -0
  22. package/dist/lib/tui.gate.test.js +96 -0
  23. package/dist/lib/tui.js +62 -0
  24. package/dist/lib/ui.js +13 -1
  25. package/dist/tui/__tests__/home.render.test.d.ts +1 -0
  26. package/dist/tui/__tests__/home.render.test.js +59 -0
  27. package/dist/tui/__tests__/state.test.d.ts +1 -0
  28. package/dist/tui/__tests__/state.test.js +88 -0
  29. package/dist/tui/components/App.d.ts +7 -0
  30. package/dist/tui/components/App.js +129 -0
  31. package/dist/tui/components/Brand.d.ts +9 -0
  32. package/dist/tui/components/Brand.js +13 -0
  33. package/dist/tui/components/Card.d.ts +11 -0
  34. package/dist/tui/components/Card.js +12 -0
  35. package/dist/tui/components/DoneScreen.d.ts +11 -0
  36. package/dist/tui/components/DoneScreen.js +30 -0
  37. package/dist/tui/components/Home.d.ts +12 -0
  38. package/dist/tui/components/Home.js +144 -0
  39. package/dist/tui/components/KeyStep.d.ts +13 -0
  40. package/dist/tui/components/KeyStep.js +44 -0
  41. package/dist/tui/components/Panel.d.ts +11 -0
  42. package/dist/tui/components/Panel.js +12 -0
  43. package/dist/tui/components/Picker.d.ts +19 -0
  44. package/dist/tui/components/Picker.js +68 -0
  45. package/dist/tui/components/PresetStep.d.ts +12 -0
  46. package/dist/tui/components/PresetStep.js +26 -0
  47. package/dist/tui/components/ProviderStep.d.ts +20 -0
  48. package/dist/tui/components/ProviderStep.js +159 -0
  49. package/dist/tui/components/Stepper.d.ts +9 -0
  50. package/dist/tui/components/Stepper.js +29 -0
  51. package/dist/tui/contract.d.ts +99 -0
  52. package/dist/tui/contract.js +5 -0
  53. package/dist/tui/index.d.ts +3 -0
  54. package/dist/tui/index.js +78 -0
  55. package/dist/tui/package.json +1 -0
  56. package/dist/tui/state.d.ts +77 -0
  57. package/dist/tui/state.js +84 -0
  58. package/dist/tui/theme.d.ts +23 -0
  59. package/dist/tui/theme.js +49 -0
  60. package/oclif.manifest.json +152 -150
  61. package/package.json +13 -3
package/dist/base.js CHANGED
@@ -6,6 +6,7 @@ const api_1 = require("./lib/api");
6
6
  const config_1 = require("./lib/config");
7
7
  const credentials_1 = require("./lib/credentials");
8
8
  const errors_1 = require("./lib/errors");
9
+ const friendly_1 = require("./lib/friendly");
9
10
  class BaseCommand extends core_1.Command {
10
11
  static enableJsonFlag = true;
11
12
  static baseFlags = {
@@ -41,7 +42,13 @@ class BaseCommand extends core_1.Command {
41
42
  }
42
43
  async catch(err) {
43
44
  if (err instanceof errors_1.ByteError) {
44
- return this.error(err.message, { code: err.code, exit: err.exit });
45
+ // --json callers want the machine-readable oclif error; humans get a panel that
46
+ // says what broke, why, and the exact next command — then we exit with the code.
47
+ if (this.jsonEnabled()) {
48
+ return this.error(err.message, { code: err.code, exit: err.exit });
49
+ }
50
+ this.logToStderr((0, friendly_1.renderErrorPanel)((0, friendly_1.friendlyError)(err)));
51
+ return this.exit(err.exit);
45
52
  }
46
53
  return super.catch(err);
47
54
  }
@@ -2,21 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const base_1 = require("../../base");
5
+ const presets_1 = require("../../lib/presets");
5
6
  class OptPreset extends base_1.BaseCommand {
6
- static description = 'Apply an optimization preset for the whole org.';
7
+ static description = 'Apply an optimization mode for the whole org (maximum, balanced, max_savings, lowest_latency, reliability_first).';
7
8
  static args = {
8
9
  preset: core_1.Args.string({
9
- description: 'Preset to apply',
10
+ description: 'Mode to apply',
10
11
  required: true,
11
- options: ['balanced', 'max_savings', 'lowest_latency', 'reliability_first'],
12
+ options: presets_1.PRESET_VALUES,
12
13
  }),
13
14
  };
14
15
  async run() {
15
16
  const { args, flags } = await this.parse(OptPreset);
16
17
  this.requireToken(flags);
17
18
  const res = await this.api(flags).optPatch({ default_mode: args.preset });
18
- if (!this.jsonEnabled())
19
- this.log(`Optimization preset set to "${args.preset}".`);
19
+ if (!this.jsonEnabled()) {
20
+ const card = (0, presets_1.presetCard)(args.preset);
21
+ this.log(`Optimization mode set to "${args.preset}".${card ? ` ${card.blurb}` : ''}`);
22
+ }
20
23
  return res;
21
24
  }
22
25
  }
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const picocolors_1 = __importDefault(require("picocolors"));
3
7
  const base_1 = require("../../base");
8
+ const presets_1 = require("../../lib/presets");
4
9
  class OptShow extends base_1.BaseCommand {
5
- static description = 'Show the current optimization preset and layer states for the org.';
10
+ static description = 'Show the current optimization mode and every layer running on your requests.';
6
11
  async run() {
7
12
  const { flags } = await this.parse(OptShow);
8
13
  this.requireToken(flags);
@@ -10,14 +15,23 @@ class OptShow extends base_1.BaseCommand {
10
15
  if (this.jsonEnabled())
11
16
  return res;
12
17
  const settings = res.settings ?? res;
13
- this.log(`Preset (default_mode): ${settings.default_mode ?? '-'}`);
14
- this.log(`Cache mode: ${settings.cache_mode ?? '-'}`);
18
+ const mode = settings.default_mode ?? '-';
19
+ const card = (0, presets_1.presetCard)(mode);
20
+ this.log('');
21
+ this.log(` ${picocolors_1.default.bold('Optimization mode')} ${picocolors_1.default.cyan(mode)}${card ? picocolors_1.default.dim(` ${card.hint}`) : ''}`);
22
+ this.log(` ${picocolors_1.default.bold('Cache mode')} ${picocolors_1.default.cyan(settings.cache_mode ?? '-')}`);
15
23
  const layers = Array.isArray(res.layers) ? res.layers : [];
16
24
  if (layers.length) {
17
- const enabled = layers.filter((l) => l.enabled).length;
18
- this.log(`Layers enabled: ${enabled}/${layers.length}`);
25
+ const on = layers.filter((l) => l.enabled).length;
26
+ this.log(` ${picocolors_1.default.bold('Active layers')} ${picocolors_1.default.green(String(on))}${picocolors_1.default.dim(`/${layers.length}`)} running on every request`);
27
+ this.log('');
28
+ for (const l of layers) {
29
+ const name = String(l.title ?? l.plain_label ?? l.key ?? 'layer');
30
+ this.log(l.enabled ? ` ${picocolors_1.default.green('●')} ${name}` : ` ${picocolors_1.default.dim('○')} ${picocolors_1.default.dim(name)}`);
31
+ }
19
32
  }
20
- this.log('Run with --json for the full settings object.');
33
+ this.log('');
34
+ this.log(picocolors_1.default.dim(' Run with --json for the full settings object.'));
21
35
  return res;
22
36
  }
23
37
  }
@@ -106,16 +106,24 @@ class ProvidersAdd extends base_1.BaseCommand {
106
106
  message: 'Model id to use in your tools',
107
107
  options: models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })),
108
108
  });
109
- await ui.note(alias, 'Model id (use this everywhere)');
109
+ // Enable the chosen model so the gateway can serve it immediately.
110
+ const picked = models.find((m) => m.byte_alias === alias);
111
+ if (picked?.id)
112
+ await api.modelEnable(picked.id).catch(() => undefined);
113
+ await ui.note(`${alias}\n\n${ui.theme.muted('Enabled and ready — use this id in your tools.')}`, 'Model id');
110
114
  }
111
115
  return res;
112
116
  }
113
117
  const res = await api.providersAdd(body);
118
+ // Enable the first imported model so the gateway is usable right away.
119
+ const firstModel = (res.fetch_result?.models ?? res.models ?? [])[0];
120
+ if (firstModel?.id)
121
+ await api.modelEnable(firstModel.id).catch(() => undefined);
114
122
  if (this.jsonEnabled())
115
123
  return res;
116
124
  const id = res.id ?? res.connection?.id;
117
125
  const imported = res.fetch_result?.imported ?? res.imported ?? 0;
118
- this.log(`Provider connection added (id ${id}). Models imported: ${imported}.`);
126
+ this.log(`Provider connection added (id ${id}). Models imported: ${imported}.${firstModel?.byte_alias ? ` Enabled ${firstModel.byte_alias}.` : ''}`);
119
127
  return res;
120
128
  }
121
129
  }
@@ -9,7 +9,7 @@ class Run extends base_1.BaseCommand {
9
9
  prompt: core_1.Args.string({ description: 'The prompt to send', required: true }),
10
10
  };
11
11
  static flags = {
12
- model: core_1.Flags.string({ description: 'Model or Byte alias', default: 'byte-default' }),
12
+ model: core_1.Flags.string({ description: 'Model or Byte alias (default: auto — routes to your enabled model)', default: 'auto' }),
13
13
  'max-tokens': core_1.Flags.integer({ description: 'Maximum output tokens' }),
14
14
  };
15
15
  async run() {
@@ -14,6 +14,7 @@ export default class Setup extends BaseCommand {
14
14
  connect: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  };
16
16
  run(): Promise<unknown>;
17
+ private buildWizardPorts;
17
18
  private connectProvider;
18
19
  private runNonInteractive;
19
20
  }
@@ -37,6 +37,7 @@ const core_1 = require("@oclif/core");
37
37
  const base_1 = require("../base");
38
38
  const auth_1 = require("../lib/auth");
39
39
  const credentials_1 = require("../lib/credentials");
40
+ const presets_1 = require("../lib/presets");
40
41
  const providers_1 = require("../lib/providers");
41
42
  const tty_1 = require("../lib/tty");
42
43
  const ui = __importStar(require("../lib/ui"));
@@ -50,9 +51,9 @@ class Setup extends base_1.BaseCommand {
50
51
  'provider-base-url': core_1.Flags.string({ description: 'Custom provider base URL (for --provider custom)' }),
51
52
  mode: core_1.Flags.string({ description: 'Gateway mode override (byte_compatible/byte_messages/byte_generative)' }),
52
53
  preset: core_1.Flags.string({
53
- description: 'Optimization preset',
54
- options: ['balanced', 'max_savings', 'lowest_latency', 'reliability_first'],
55
- default: 'balanced',
54
+ description: 'Optimization mode',
55
+ options: ['maximum', 'balanced', 'max_savings', 'lowest_latency', 'reliability_first'],
56
+ default: 'maximum',
56
57
  }),
57
58
  'key-name': core_1.Flags.string({ description: 'Name for the Byte key', default: 'cli' }),
58
59
  'skip-provider': core_1.Flags.boolean({ description: 'Skip connecting a provider' }),
@@ -65,6 +66,35 @@ class Setup extends base_1.BaseCommand {
65
66
  if (this.jsonEnabled() || !(0, tty_1.interactive)()) {
66
67
  return this.runNonInteractive(flags, profile, base);
67
68
  }
69
+ // Full-screen Ink wizard when the terminal supports it. Any failure inside the island
70
+ // returns {status:'fallback'} and we drop through to the clack flow below — no regression.
71
+ const tui = require('../lib/tui');
72
+ if (tui.useInk({ json: this.jsonEnabled() })) {
73
+ const ports = this.buildWizardPorts(flags, profile, base);
74
+ const res = await tui.renderWizardIsland({
75
+ plainColor: Boolean(process.env.NO_COLOR),
76
+ ascii: tui.computeAscii(),
77
+ version: this.config.version,
78
+ initial: { signedIn: Boolean((0, credentials_1.getToken)(profile)), email: undefined, byteKey: (0, credentials_1.getByteKey)(profile) },
79
+ ports,
80
+ providerPresets: providers_1.PROVIDER_PRESETS,
81
+ presetCards: presets_1.PRESET_CARDS,
82
+ });
83
+ if (res.status === 'cancelled')
84
+ return this.exit(130);
85
+ if (res.status === 'done') {
86
+ if (res.summary.connect) {
87
+ await this.config.runCommand('integrate', [
88
+ res.summary.connect,
89
+ '--write',
90
+ '--yes',
91
+ ...(res.summary.model ? ['--model', res.summary.model] : []),
92
+ ]);
93
+ }
94
+ return res.summary;
95
+ }
96
+ // status === 'fallback' → continue to the clack flow.
97
+ }
68
98
  await ui.intro(ui.banner());
69
99
  // 1) Sign in
70
100
  let token = (0, credentials_1.getToken)(profile);
@@ -82,37 +112,46 @@ class Setup extends base_1.BaseCommand {
82
112
  await (0, auth_1.deviceLogin)({ baseUrl: base, profile });
83
113
  }
84
114
  // 2) Connect a provider
85
- let chosenAlias;
115
+ let provider = { status: 'skipped' };
86
116
  if (!flags['skip-provider']) {
87
117
  const existing = await api.providersList().catch(() => ({ connections: [] }));
88
118
  const conns = existing.connections ?? (Array.isArray(existing) ? existing : []);
89
119
  let addNew = true;
90
120
  if (conns.length) {
91
- addNew = (await ui.select({
92
- message: 'Provider',
93
- options: [
94
- { value: 'existing', label: `Use existing connection (${conns.length})` },
95
- { value: 'new', label: 'Connect a new provider' },
96
- ],
97
- })) === 'new';
121
+ addNew =
122
+ (await ui.select({
123
+ message: 'Provider',
124
+ options: [
125
+ { value: 'existing', label: `Use existing connection (${conns.length})` },
126
+ { value: 'new', label: 'Connect a new provider' },
127
+ ],
128
+ })) === 'new';
129
+ if (!addNew)
130
+ provider = { status: 'connected' };
98
131
  }
99
132
  if (addNew) {
100
- chosenAlias = await this.connectProvider(api, flags);
133
+ provider = await this.connectProvider(api, flags);
101
134
  }
102
135
  }
103
- // 3) Create a Byte key
136
+ // 3) Choose the optimization mode
137
+ const preset = await ui.select({
138
+ message: 'Optimization mode',
139
+ options: presets_1.PRESET_CARDS.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
140
+ initialValue: presets_1.PRESET_CARDS.some((c) => c.value === flags.preset) ? flags.preset : 'maximum',
141
+ });
142
+ // 4) Create a Byte key
104
143
  let byteKey = (0, credentials_1.getByteKey)(profile);
105
144
  if (!byteKey) {
106
145
  const name = await ui.text({ message: 'Name this Byte key', defaultValue: flags['key-name'] || 'cli' });
107
- const res = await api.keysCreate({ name: name || 'cli', preset: flags.preset });
108
- const created = String(res.key);
109
- (0, credentials_1.setByteKey)(profile, created);
110
- await ui.revealSecret('Your Byte API key', created);
146
+ const res = await api.keysCreate({ name: name || 'cli', preset });
147
+ byteKey = String(res.key);
148
+ (0, credentials_1.setByteKey)(profile, byteKey);
149
+ await ui.revealSecret('Your Byte API key', byteKey);
111
150
  }
112
- // 4) Optimization preset
113
- await api.optPatch({ default_mode: flags.preset }).catch(() => undefined);
114
- await ui.log.success(`Optimizations: ${flags.preset} preset applied`);
115
- // 5) Connect a tool
151
+ // 5) Apply the optimization mode to the org
152
+ await api.optPatch({ default_mode: preset }).catch(() => undefined);
153
+ await ui.log.success(`Optimizations: ${preset} mode applied — every layer this mode enables now runs on each request.`);
154
+ // 6) Connect a tool
116
155
  const connect = await ui.select({
117
156
  message: 'Connect a coding tool now?',
118
157
  options: [
@@ -126,11 +165,76 @@ class Setup extends base_1.BaseCommand {
126
165
  ],
127
166
  });
128
167
  if (connect !== 'skip') {
129
- const argv = [connect, '--write', '--yes', ...(chosenAlias ? ['--model', chosenAlias] : [])];
168
+ const argv = [connect, '--write', '--yes', ...(provider.alias ? ['--model', provider.alias] : [])];
130
169
  await this.config.runCommand('integrate', argv);
131
170
  }
132
- await ui.outro(`${ui.theme.ok('Setup complete.')} Next: ${ui.theme.accent('byte run "hello from byte"')} · ${ui.theme.accent('byte usage')}`);
133
- return { status: 'setup_complete', profile, model: chosenAlias };
171
+ // Accurate close-out never claim a provider is wired when it is not.
172
+ const tips = [`${ui.theme.accent('byte run "hello from byte"')}`, `${ui.theme.accent('byte usage')}`];
173
+ if (provider.status === 'saved') {
174
+ await ui.note(`Your provider is saved but no model loaded yet. Fix the key/URL, then run ${ui.theme.accent('byte providers test')}.`, 'One thing left');
175
+ }
176
+ else if (provider.status === 'skipped' && !flags['skip-provider']) {
177
+ tips.unshift(`${ui.theme.accent('byte providers add')}`);
178
+ }
179
+ await ui.outro(`${ui.theme.ok('Setup complete.')} Next: ${tips.join(' · ')}`);
180
+ return { status: 'setup_complete', profile, model: provider.alias, provider: provider.status, preset };
181
+ }
182
+ // Adapts the existing auth + API surface into the WizardPorts the Ink island calls. Every port
183
+ // wraps a call the clack flow already uses, so both paths hit identical backend behavior.
184
+ buildWizardPorts(flags, profile, base) {
185
+ const api = this.api(flags);
186
+ return {
187
+ signIn: async () => {
188
+ const r = await (0, auth_1.deviceLogin)({ baseUrl: base, profile });
189
+ return { email: r.email };
190
+ },
191
+ listProviders: async () => {
192
+ const existing = await api.providersList().catch(() => ({ connections: [] }));
193
+ return existing.connections ?? (Array.isArray(existing) ? existing : []);
194
+ },
195
+ connectProvider: async (a) => {
196
+ const res = await api.providersAdd({
197
+ api_key: a.apiKey,
198
+ gateway_mode: flags.mode || a.mode,
199
+ base_url: a.baseUrl,
200
+ auto_fetch: true,
201
+ });
202
+ const fetchResult = res.fetch_result ?? {};
203
+ const models = res.models ?? fetchResult.models ?? [];
204
+ const connId = res.id ?? res.connection?.id;
205
+ const failed = fetchResult.status === 'error' || Boolean(res.fetch_error) || Boolean(fetchResult.last_error);
206
+ const status = failed && !models.length ? 'saved' : 'connected';
207
+ return {
208
+ status,
209
+ models,
210
+ connId,
211
+ error: failed ? String(fetchResult.last_error || res.fetch_error || 'Provider models could not be loaded.') : undefined,
212
+ };
213
+ },
214
+ rotateAndTest: async (connId, apiKey) => {
215
+ try {
216
+ await api.providersRotate(connId, apiKey);
217
+ const test = await api.providersTest(connId);
218
+ const models = test.models ?? [];
219
+ return { models, error: models.length ? undefined : 'No models returned.' };
220
+ }
221
+ catch (err) {
222
+ return { models: [], error: err?.message ?? String(err) };
223
+ }
224
+ },
225
+ enableModel: async (id) => {
226
+ await api.modelEnable(id).catch(() => undefined);
227
+ },
228
+ createByteKey: async (name, preset) => {
229
+ const res = await api.keysCreate({ name: name || 'cli', preset });
230
+ const key = String(res.key);
231
+ (0, credentials_1.setByteKey)(profile, key);
232
+ return { key };
233
+ },
234
+ applyPreset: async (preset) => {
235
+ await api.optPatch({ default_mode: preset }).catch(() => undefined);
236
+ },
237
+ };
134
238
  }
135
239
  async connectProvider(api, flags) {
136
240
  const providerId = await ui.select({
@@ -151,27 +255,72 @@ class Setup extends base_1.BaseCommand {
151
255
  const key = await ui.password({ message: `${preset.label} API key`, validate: (v) => (v.trim() ? undefined : 'Required') });
152
256
  const spin = await ui.spinner();
153
257
  spin.start(`Connecting ${preset.label}…`);
258
+ let res;
154
259
  try {
155
- const res = await api.providersAdd({ api_key: key, gateway_mode: flags.mode || preset.gateway_mode, base_url: baseUrl, auto_fetch: true });
156
- const models = res.fetch_result?.models ?? res.models ?? [];
157
- spin.stop(`Connected ${preset.label} — ${models.length} model(s) imported.`);
158
- if (models.length) {
159
- return ui.select({
160
- message: 'Default model',
161
- options: models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })),
162
- });
163
- }
260
+ res = await api.providersAdd({ api_key: key, gateway_mode: flags.mode || preset.gateway_mode, base_url: baseUrl, auto_fetch: true });
164
261
  }
165
262
  catch (err) {
166
- spin.stop('Provider connection failed.');
263
+ spin.stop('Could not save the provider connection.');
167
264
  await ui.log.error(err?.message ?? String(err));
265
+ return { status: 'failed' };
266
+ }
267
+ const connId = res.id ?? res.connection?.id;
268
+ let fetchResult = res.fetch_result ?? {};
269
+ let models = res.models ?? fetchResult.models ?? [];
270
+ // Retry / keep / skip loop — a failed model import never silently passes as success.
271
+ for (;;) {
272
+ const failed = fetchResult.status === 'error' || Boolean(res.fetch_error) || Boolean(fetchResult.last_error);
273
+ if (!failed || models.length) {
274
+ spin.stop(`Connected ${preset.label} — ${models.length} model(s) imported.`);
275
+ break;
276
+ }
277
+ spin.stop(`Saved ${preset.label}, but its models could not be loaded.`);
278
+ await ui.log.warn(String(fetchResult.last_error || res.fetch_error || 'The provider URL or key looks unreachable right now.'));
279
+ const choice = await ui.select({
280
+ message: 'What would you like to do?',
281
+ options: [
282
+ { value: 'retry', label: 'Re-enter the key and retry' },
283
+ { value: 'keep', label: 'Keep it — I will pick a model later' },
284
+ { value: 'skip', label: 'Skip the provider for now' },
285
+ ],
286
+ });
287
+ if (choice !== 'retry' || !connId)
288
+ return { status: choice === 'skip' ? 'skipped' : 'saved' };
289
+ const retryKey = await ui.password({ message: `${preset.label} API key`, validate: (v) => (v.trim() ? undefined : 'Required') });
290
+ spin.start('Retrying…');
291
+ try {
292
+ await api.providersRotate(connId, retryKey);
293
+ const test = await api.providersTest(connId);
294
+ models = test.models ?? [];
295
+ fetchResult = { status: models.length ? 'ok' : 'error', last_error: models.length ? null : 'No models returned.' };
296
+ res = { ...res, fetch_error: undefined };
297
+ }
298
+ catch (err) {
299
+ models = [];
300
+ fetchResult = { status: 'error', last_error: err?.message ?? String(err) };
301
+ res = { ...res, fetch_error: undefined };
302
+ }
168
303
  }
169
- return undefined;
304
+ if (models.length) {
305
+ const alias = await ui.select({
306
+ message: 'Default model',
307
+ options: models.slice(0, 50).map((m) => ({ value: m.byte_alias, label: m.byte_alias, hint: m.model_id })),
308
+ });
309
+ // Enable the chosen model so the gateway can route to it right away. Imported models
310
+ // start disabled — without this the first `byte run` would hit BYTE_ALIAS_NOT_CONFIGURED.
311
+ const picked = models.find((m) => m.byte_alias === alias);
312
+ if (picked?.id)
313
+ await api.modelEnable(picked.id).catch(() => undefined);
314
+ return { alias, status: 'connected' };
315
+ }
316
+ return { status: 'saved' };
170
317
  }
171
318
  async runNonInteractive(flags, profile, base) {
172
319
  this.requireToken(flags);
173
320
  const api = this.api(flags);
174
321
  let imported = 0;
322
+ let providerStatus = flags['skip-provider'] ? 'skipped' : 'skipped';
323
+ let providerError;
175
324
  let chosenAlias;
176
325
  const key = flags['provider-key'];
177
326
  if (key && !flags['skip-provider']) {
@@ -182,20 +331,42 @@ class Setup extends base_1.BaseCommand {
182
331
  return this.error('Provide --provider <preset> or --provider-base-url for the upstream provider.', { exit: 2 });
183
332
  }
184
333
  const res = await api.providersAdd({ api_key: key, gateway_mode: mode, base_url: baseUrl, auto_fetch: true });
185
- imported = res.fetch_result?.imported ?? res.imported ?? 0;
186
- chosenAlias = res.fetch_result?.models?.[0]?.byte_alias;
334
+ const fetchResult = res.fetch_result ?? {};
335
+ imported = fetchResult.imported ?? res.imported ?? 0;
336
+ const firstModel = res.models?.[0];
337
+ chosenAlias = firstModel?.byte_alias;
338
+ // Enable the first model so the gateway works immediately (imported models start disabled).
339
+ if (firstModel?.id)
340
+ await api.modelEnable(firstModel.id).catch(() => undefined);
341
+ if (fetchResult.status === 'error' || res.fetch_error) {
342
+ providerStatus = 'saved';
343
+ providerError = String(fetchResult.last_error || res.fetch_error);
344
+ }
345
+ else {
346
+ providerStatus = 'connected';
347
+ }
187
348
  }
188
349
  await api.optPatch({ default_mode: flags.preset }).catch(() => undefined);
189
350
  const keyRes = await api.keysCreate({ name: flags['key-name'] || 'cli', preset: flags.preset });
190
351
  (0, credentials_1.setByteKey)(profile, keyRes.key);
191
352
  if (flags.connect) {
192
- await this.config.runCommand('integrate', [flags.connect, '--write', '--yes', ...(chosenAlias ? ['--model', chosenAlias] : [])]);
353
+ const argv = [flags.connect, '--write', '--yes', ...(chosenAlias ? ['--model', chosenAlias] : [])];
354
+ await this.config.runCommand('integrate', argv);
193
355
  }
194
356
  if (!this.jsonEnabled()) {
195
357
  this.log(`Byte key: ${keyRes.key}`);
196
358
  this.log(`Provider models imported: ${imported}`);
359
+ if (providerStatus === 'saved')
360
+ this.log(`Provider saved but models not loaded: ${providerError ?? 'unreachable'}. Run \`byte providers test\`.`);
197
361
  }
198
- return { byte_key: keyRes.key, provider_models_imported: imported, preset: flags.preset, model: chosenAlias };
362
+ return {
363
+ byte_key: keyRes.key,
364
+ provider_models_imported: imported,
365
+ provider: providerStatus,
366
+ provider_error: providerError,
367
+ preset: flags.preset,
368
+ model: chosenAlias,
369
+ };
199
370
  }
200
371
  }
201
372
  exports.default = Setup;
@@ -16,16 +16,40 @@ class Usage extends base_1.BaseCommand {
16
16
  return res;
17
17
  const points = res.series ?? res.points ?? (Array.isArray(res) ? res : []);
18
18
  if (!points.length) {
19
- this.log('No usage recorded yet.');
19
+ this.log('No usage recorded yet. Send a request through your Byte key, then check back.');
20
20
  return res;
21
21
  }
22
- this.log((0, output_1.renderTable)(['Bucket', 'Requests', 'Tokens', 'Cost', 'Savings'], points.map((p) => [
23
- p.bucket ?? p.date ?? p.t ?? '-',
24
- p.requests ?? p.total_requests ?? '-',
25
- p.tokens ?? p.total_tokens ?? '-',
26
- (0, output_1.fmtUsd)(p.cost_usd ?? p.cost_byte_usd),
27
- (0, output_1.fmtUsd)(p.savings_usd),
28
- ])));
22
+ const num = (v) => {
23
+ const n = typeof v === 'number' ? v : Number(v);
24
+ return Number.isFinite(n) ? n : 0;
25
+ };
26
+ const tokensOf = (p) => num(p.tokens_in) + num(p.tokens_out);
27
+ const rows = points.map((p) => [
28
+ (0, output_1.fmtBucket)(p.timestamp ?? p.bucket ?? p.date),
29
+ (0, output_1.fmtNum)(num(p.requests)),
30
+ (0, output_1.fmtNum)(num(p.cached)),
31
+ (0, output_1.fmtNum)(tokensOf(p)),
32
+ (0, output_1.fmtUsd)(num(p.cost_byte_usd ?? p.cost_usd)),
33
+ (0, output_1.fmtUsd)(num(p.savings_usd)),
34
+ ]);
35
+ const totals = {
36
+ requests: points.reduce((a, p) => a + num(p.requests), 0),
37
+ cached: points.reduce((a, p) => a + num(p.cached), 0),
38
+ tokens: points.reduce((a, p) => a + tokensOf(p), 0),
39
+ cost: points.reduce((a, p) => a + num(p.cost_byte_usd ?? p.cost_usd), 0),
40
+ savings: points.reduce((a, p) => a + num(p.savings_usd), 0),
41
+ raw: points.reduce((a, p) => a + num(p.cost_raw_usd), 0),
42
+ };
43
+ rows.push(['Total', (0, output_1.fmtNum)(totals.requests), (0, output_1.fmtNum)(totals.cached), (0, output_1.fmtNum)(totals.tokens), (0, output_1.fmtUsd)(totals.cost), (0, output_1.fmtUsd)(totals.savings)]);
44
+ this.log((0, output_1.renderTable)(['Bucket', 'Requests', 'Cached', 'Tokens', 'Cost', 'Savings'], rows));
45
+ const plain = Boolean(process.env.NO_COLOR) || process.env.BYTE_PLAIN === '1';
46
+ const spark = (0, output_1.sparkline)(points.map((p) => num(p.savings_usd)), plain);
47
+ if (spark)
48
+ this.log(`\nSavings trend ${spark}`);
49
+ const savedPct = totals.raw > 0 ? (0, output_1.pct)(totals.savings / totals.raw) : null;
50
+ const hitRate = totals.requests > 0 ? (0, output_1.pct)(totals.cached / totals.requests) : null;
51
+ this.log(`Saved ${(0, output_1.fmtUsd)(totals.savings)}${savedPct ? ` (${savedPct})` : ''} across ${(0, output_1.fmtNum)(totals.requests)} requests` +
52
+ `${hitRate ? `, cache hit rate ${hitRate}` : ''}.`);
29
53
  return res;
30
54
  }
31
55
  }
@@ -33,13 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ const api_1 = require("../../lib/api");
36
37
  const config_1 = require("../../lib/config");
37
38
  const credentials_1 = require("../../lib/credentials");
39
+ const home_1 = require("../../lib/home");
40
+ const output_1 = require("../../lib/output");
38
41
  const tty_1 = require("../../lib/tty");
39
42
  const ui = __importStar(require("../../lib/ui"));
40
43
  // oclif runs init hooks before it would print help. Bare `byte` (no command) in a real
41
- // terminal opens an interactive home menu; anything else (a command, a pipe, CI, --help)
42
- // returns and lets oclif behave normally.
44
+ // terminal opens the live home dashboard + an interactive menu; anything else (a command,
45
+ // a pipe, CI, --help) returns and lets oclif behave normally.
43
46
  const hook = async function (opts) {
44
47
  if (opts.id !== undefined)
45
48
  return;
@@ -50,9 +53,42 @@ const hook = async function (opts) {
50
53
  return;
51
54
  const profile = (0, config_1.profileName)();
52
55
  const prof = (0, config_1.getProfile)(profile);
53
- const signedIn = Boolean((0, credentials_1.getToken)(profile));
54
- const status = signedIn ? ui.theme.muted(`signed in · ${prof.email ?? prof.org_name ?? profile}`) : ui.theme.muted('not signed in');
55
- await ui.intro(`${ui.banner()} ${status}`);
56
+ const token = (0, credentials_1.getToken)(profile);
57
+ const signedIn = Boolean(token);
58
+ const byteKey = (0, credentials_1.getByteKey)(profile);
59
+ // Live, branded home dashboard. Every cell is best-effort: an unreachable service or a
60
+ // signed-out profile degrades to a dim placeholder rather than blocking the screen.
61
+ let data = { savingsSeries: [], savedTotal: 0, tokensTotal: 0, byteKey, gateway: 'unknown' };
62
+ if (signedIn) {
63
+ const api = new api_1.ByteApi(prof.base_url || config_1.DEFAULT_BASE_URL, token);
64
+ const loaded = await (0, home_1.loadHomeData)(api, byteKey, profile).catch(() => undefined);
65
+ if (loaded)
66
+ data = { ...data, ...loaded };
67
+ }
68
+ // Full-screen Ink home dashboard + quick-actions menu, when the terminal supports it. Any
69
+ // failure inside the island returns {status:'fallback'} and we drop straight through to the
70
+ // existing clack dashboard below — nothing regresses.
71
+ const tui = require('../../lib/tui');
72
+ if (tui.useInk()) {
73
+ const res = await tui.renderHomeIsland({
74
+ signedIn,
75
+ data: { ...data, gateway: data.gateway ?? 'unknown', byteKeyMasked: (0, output_1.maskKey)(byteKey) },
76
+ plainColor: Boolean(process.env.NO_COLOR),
77
+ ascii: tui.computeAscii(),
78
+ version: this.config.version,
79
+ });
80
+ if (res.status === 'exit') {
81
+ await this.exit(0);
82
+ return;
83
+ }
84
+ if (res.status === 'action') {
85
+ await this.config.runCommand(res.commandId, res.argv);
86
+ await this.exit(0);
87
+ return;
88
+ }
89
+ // status === 'fallback' → continue to the clack dashboard.
90
+ }
91
+ this.log(`\n${(0, home_1.renderHome)(data, signedIn)}\n`);
56
92
  const choice = await ui.select({
57
93
  message: 'What would you like to do?',
58
94
  initialValue: signedIn ? 'setup' : 'login',
package/dist/lib/api.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare class ByteApi {
22
22
  providersAdd(body: Record<string, unknown>): Promise<any>;
23
23
  providersRotate(id: number, apiKey: string): Promise<any>;
24
24
  providersTest(id: number): Promise<any>;
25
+ modelEnable(id: number): Promise<any>;
25
26
  optShow(): Promise<any>;
26
27
  optPatch(body: Record<string, unknown>): Promise<any>;
27
28
  usage(granularity: string): Promise<any>;
package/dist/lib/api.js CHANGED
@@ -94,6 +94,12 @@ class ByteApi {
94
94
  providersTest(id) {
95
95
  return this.request('POST', `/api/v1/model-connections/${id}/fetch-models`, { body: {} });
96
96
  }
97
+ // Enable an imported provider model so the gateway can route to it. Imported models
98
+ // arrive disabled; without enabling at least one, the gateway returns
99
+ // BYTE_ALIAS_NOT_CONFIGURED. Setup/providers-add call this on the model the user picks.
100
+ modelEnable(id) {
101
+ return this.request('PATCH', `/api/v1/models/${id}`, { body: { enabled: true } });
102
+ }
97
103
  // --- optimizations ---
98
104
  optShow() {
99
105
  return this.request('GET', '/api/v1/optimizations');
@@ -0,0 +1,8 @@
1
+ import { ByteError } from './errors';
2
+ export interface Friendly {
3
+ issue: string;
4
+ why: string;
5
+ next: string;
6
+ }
7
+ export declare function friendlyError(err: ByteError): Friendly;
8
+ export declare function renderErrorPanel(f: Friendly): string;