@bytevion/cli 0.1.0 → 0.3.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.
- package/README.md +66 -0
- package/dist/base.js +8 -1
- package/dist/commands/compare.d.ts +12 -0
- package/dist/commands/compare.js +104 -0
- package/dist/commands/integrate.d.ts +1 -0
- package/dist/commands/integrate.js +76 -28
- package/dist/commands/login.js +40 -59
- package/dist/commands/opt/preset.js +8 -5
- package/dist/commands/opt/show.js +20 -6
- package/dist/commands/providers/add.d.ts +3 -1
- package/dist/commands/providers/add.js +102 -20
- package/dist/commands/providers/rotate.js +39 -4
- package/dist/commands/setup.d.ts +19 -0
- package/dist/commands/setup.js +277 -0
- package/dist/commands/usage.js +32 -8
- package/dist/hooks/init/home.d.ts +3 -0
- package/dist/hooks/init/home.js +107 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +60 -0
- package/dist/lib/auth.d.ts +9 -0
- package/dist/lib/auth.js +100 -0
- package/dist/lib/friendly.d.ts +8 -0
- package/dist/lib/friendly.js +86 -0
- package/dist/lib/home.d.ts +16 -0
- package/dist/lib/home.js +97 -0
- package/dist/lib/integrations.d.ts +1 -0
- package/dist/lib/integrations.js +198 -61
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.js +43 -0
- package/dist/lib/presets.d.ts +9 -0
- package/dist/lib/presets.js +40 -0
- package/dist/lib/providers.d.ts +13 -0
- package/dist/lib/providers.js +39 -0
- package/dist/lib/tty.d.ts +1 -0
- package/dist/lib/tty.js +11 -0
- package/dist/lib/ui.d.ts +60 -0
- package/dist/lib/ui.js +122 -0
- package/dist/lib/util.d.ts +1 -2
- package/dist/lib/util.js +11 -79
- package/oclif.manifest.json +215 -16
- package/package.json +76 -59
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const api_1 = require("../../lib/api");
|
|
37
|
+
const config_1 = require("../../lib/config");
|
|
38
|
+
const credentials_1 = require("../../lib/credentials");
|
|
39
|
+
const home_1 = require("../../lib/home");
|
|
40
|
+
const tty_1 = require("../../lib/tty");
|
|
41
|
+
const ui = __importStar(require("../../lib/ui"));
|
|
42
|
+
// oclif runs init hooks before it would print help. Bare `byte` (no command) in a real
|
|
43
|
+
// terminal opens the live home dashboard + an interactive menu; anything else (a command,
|
|
44
|
+
// a pipe, CI, --help) returns and lets oclif behave normally.
|
|
45
|
+
const hook = async function (opts) {
|
|
46
|
+
if (opts.id !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
const argv = opts.argv ?? [];
|
|
49
|
+
if (argv.length > 0)
|
|
50
|
+
return;
|
|
51
|
+
if (!(0, tty_1.interactive)())
|
|
52
|
+
return;
|
|
53
|
+
const profile = (0, config_1.profileName)();
|
|
54
|
+
const prof = (0, config_1.getProfile)(profile);
|
|
55
|
+
const token = (0, credentials_1.getToken)(profile);
|
|
56
|
+
const signedIn = Boolean(token);
|
|
57
|
+
const byteKey = (0, credentials_1.getByteKey)(profile);
|
|
58
|
+
// Live, branded home dashboard. Every cell is best-effort: an unreachable service or a
|
|
59
|
+
// signed-out profile degrades to a dim placeholder rather than blocking the screen.
|
|
60
|
+
let data = { savingsSeries: [], savedTotal: 0, tokensTotal: 0, byteKey, gateway: 'unknown' };
|
|
61
|
+
if (signedIn) {
|
|
62
|
+
const api = new api_1.ByteApi(prof.base_url || config_1.DEFAULT_BASE_URL, token);
|
|
63
|
+
const loaded = await (0, home_1.loadHomeData)(api, byteKey, profile).catch(() => undefined);
|
|
64
|
+
if (loaded)
|
|
65
|
+
data = { ...data, ...loaded };
|
|
66
|
+
}
|
|
67
|
+
this.log(`\n${(0, home_1.renderHome)(data, signedIn)}\n`);
|
|
68
|
+
const choice = await ui.select({
|
|
69
|
+
message: 'What would you like to do?',
|
|
70
|
+
initialValue: signedIn ? 'setup' : 'login',
|
|
71
|
+
options: [
|
|
72
|
+
{ value: 'setup', label: 'Set up Byte', hint: 'guided' },
|
|
73
|
+
{ value: 'providers:add', label: 'Connect a provider' },
|
|
74
|
+
{ value: 'keys:create', label: 'Create a Byte key' },
|
|
75
|
+
{ value: 'integrate', label: 'Connect a tool' },
|
|
76
|
+
{ value: 'run', label: 'Run a test prompt' },
|
|
77
|
+
{ value: 'compare', label: 'Compare direct vs Byte', hint: 'prove the win' },
|
|
78
|
+
{ value: 'usage', label: 'View usage' },
|
|
79
|
+
{ value: signedIn ? 'logout' : 'login', label: signedIn ? 'Sign out' : 'Sign in' },
|
|
80
|
+
{ value: 'exit', label: 'Exit' },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
if (choice === 'exit') {
|
|
84
|
+
await ui.outro('Bye.');
|
|
85
|
+
await this.exit(0);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let cmdId = choice;
|
|
89
|
+
let cmdArgv = [];
|
|
90
|
+
if (choice === 'run') {
|
|
91
|
+
const prompt = await ui.text({ message: 'Prompt', placeholder: 'hello from byte', defaultValue: 'hello from byte' });
|
|
92
|
+
cmdId = 'run';
|
|
93
|
+
cmdArgv = [prompt || 'hello from byte'];
|
|
94
|
+
}
|
|
95
|
+
else if (choice === 'compare') {
|
|
96
|
+
const prompt = await ui.text({ message: 'Prompt to compare', placeholder: 'Summarize the latest changes', defaultValue: 'Summarize the latest changes' });
|
|
97
|
+
cmdId = 'compare';
|
|
98
|
+
cmdArgv = [prompt || 'Summarize the latest changes'];
|
|
99
|
+
}
|
|
100
|
+
else if (choice === 'keys:create') {
|
|
101
|
+
const name = await ui.text({ message: 'Key name', defaultValue: 'cli' });
|
|
102
|
+
cmdArgv = [name || 'cli'];
|
|
103
|
+
}
|
|
104
|
+
await this.config.runCommand(cmdId, cmdArgv);
|
|
105
|
+
await this.exit(0);
|
|
106
|
+
};
|
|
107
|
+
exports.default = hook;
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -36,5 +36,6 @@ export declare class ByteApi {
|
|
|
36
36
|
chat(byteKey: string, body: Record<string, unknown>): Promise<any>;
|
|
37
37
|
models(byteKey: string): Promise<any>;
|
|
38
38
|
probeMessages(byteKey: string): Promise<any>;
|
|
39
|
+
compareStream(body: Record<string, unknown>): AsyncGenerator<any>;
|
|
39
40
|
}
|
|
40
41
|
export {};
|
package/dist/lib/api.js
CHANGED
|
@@ -143,5 +143,65 @@ class ByteApi {
|
|
|
143
143
|
body: { model: 'byte-default', max_tokens: 1, messages: [{ role: 'user', content: 'ping' }] },
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
+
// --- compare (direct vs Byte) — Server-Sent Events ---
|
|
147
|
+
async *compareStream(body) {
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await fetch(`${this.baseUrl}/api/v1/compare/chat`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
Accept: 'text/event-stream',
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : 'network error';
|
|
162
|
+
throw new errors_1.ByteError(`Cannot reach ${this.baseUrl} (${message})`, 'BYTE_API_OFFLINE', 5, 0);
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const text = await res.text().catch(() => '');
|
|
166
|
+
let detail = res.statusText;
|
|
167
|
+
try {
|
|
168
|
+
detail = JSON.parse(text).detail ?? detail;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* keep statusText */
|
|
172
|
+
}
|
|
173
|
+
const code = typeof detail === 'string' ? detail : 'BYTE_HTTP_ERROR';
|
|
174
|
+
throw new errors_1.ByteError(typeof detail === 'string' ? detail : JSON.stringify(detail), code, (0, errors_1.exitForStatus)(res.status), res.status);
|
|
175
|
+
}
|
|
176
|
+
const reader = res.body?.getReader();
|
|
177
|
+
if (!reader)
|
|
178
|
+
return;
|
|
179
|
+
const decoder = new TextDecoder();
|
|
180
|
+
let buffer = '';
|
|
181
|
+
for (;;) {
|
|
182
|
+
const { value, done } = await reader.read();
|
|
183
|
+
if (done)
|
|
184
|
+
break;
|
|
185
|
+
buffer += decoder.decode(value, { stream: true });
|
|
186
|
+
const parts = buffer.split('\n\n');
|
|
187
|
+
buffer = parts.pop() ?? '';
|
|
188
|
+
for (const part of parts) {
|
|
189
|
+
for (const line of part.split('\n')) {
|
|
190
|
+
const trimmed = line.trim();
|
|
191
|
+
if (!trimmed.startsWith('data:'))
|
|
192
|
+
continue;
|
|
193
|
+
const raw = trimmed.slice(5).trim();
|
|
194
|
+
if (!raw || raw === '[DONE]')
|
|
195
|
+
continue;
|
|
196
|
+
try {
|
|
197
|
+
yield JSON.parse(raw);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* ignore non-JSON keepalives */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
146
206
|
}
|
|
147
207
|
exports.ByteApi = ByteApi;
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.deviceLogin = deviceLogin;
|
|
37
|
+
const node_os_1 = require("node:os");
|
|
38
|
+
const api_1 = require("./api");
|
|
39
|
+
const config_1 = require("./config");
|
|
40
|
+
const credentials_1 = require("./credentials");
|
|
41
|
+
const errors_1 = require("./errors");
|
|
42
|
+
const ui = __importStar(require("./ui"));
|
|
43
|
+
const util_1 = require("./util");
|
|
44
|
+
// Device-code login loop shared by `byte login` and the setup wizard. Prints the
|
|
45
|
+
// user code, opens the browser, and polls with backoff until approved/denied/expired.
|
|
46
|
+
async function deviceLogin(opts) {
|
|
47
|
+
const api = new api_1.ByteApi(opts.baseUrl);
|
|
48
|
+
const start = await api.deviceStart(`byte-cli@${(0, node_os_1.hostname)()}`);
|
|
49
|
+
const verifyUrl = start.verification_uri_complete || start.verification_uri;
|
|
50
|
+
await ui.note(`${ui.theme.bold(start.user_code)}\n\n${verifyUrl}`, 'Approve this device in your browser');
|
|
51
|
+
if (!opts.noBrowser) {
|
|
52
|
+
try {
|
|
53
|
+
await (0, util_1.openBrowser)(verifyUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// user can open the URL manually
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const spin = await ui.spinner();
|
|
60
|
+
spin.start('Waiting for approval…');
|
|
61
|
+
let interval = (Number(start.interval) || 5) * 1000;
|
|
62
|
+
const deadline = Date.now() + (Number(start.expires_in) || 600) * 1000;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
await (0, util_1.sleep)(interval);
|
|
65
|
+
try {
|
|
66
|
+
const tok = await api.deviceToken(start.device_code);
|
|
67
|
+
(0, credentials_1.setToken)(opts.profile, tok.access_token);
|
|
68
|
+
(0, config_1.setProfile)(opts.profile, {
|
|
69
|
+
base_url: opts.baseUrl,
|
|
70
|
+
dashboard_url: (0, config_1.getProfile)(opts.profile).dashboard_url || config_1.DEFAULT_DASHBOARD_URL,
|
|
71
|
+
org_id: tok.org?.id,
|
|
72
|
+
org_name: tok.org?.name,
|
|
73
|
+
email: tok.user?.email,
|
|
74
|
+
});
|
|
75
|
+
spin.stop('Signed in.');
|
|
76
|
+
return { email: tok.user?.email, org: tok.org?.name };
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const code = err instanceof errors_1.ByteError ? err.code : '';
|
|
80
|
+
if (code === 'authorization_pending')
|
|
81
|
+
continue;
|
|
82
|
+
if (code === 'slow_down') {
|
|
83
|
+
interval += 5000;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (code === 'access_denied') {
|
|
87
|
+
spin.stop('Authorization denied.');
|
|
88
|
+
throw new errors_1.ByteError('Authorization was denied in the dashboard.', 'BYTE_AUTH_DENIED', 3);
|
|
89
|
+
}
|
|
90
|
+
if (code === 'expired_token') {
|
|
91
|
+
spin.stop('Code expired.');
|
|
92
|
+
throw new errors_1.ByteError('The verification code expired. Run `byte login` again.', 'BYTE_AUTH_EXPIRED', 3);
|
|
93
|
+
}
|
|
94
|
+
spin.stop('Login failed.');
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
spin.stop('Timed out.');
|
|
99
|
+
throw new errors_1.ByteError('Login timed out. Run `byte login` again.', 'BYTE_AUTH_TIMEOUT', 3);
|
|
100
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.friendlyError = friendlyError;
|
|
7
|
+
exports.renderErrorPanel = renderErrorPanel;
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
// Translates a ByteError (HTTP status + BYTE_* / provider detail code) into plain
|
|
10
|
+
// language a first-time user can act on: what happened, why, and the exact next
|
|
11
|
+
// command. The goal is that no error ever leaves someone stuck.
|
|
12
|
+
function friendlyError(err) {
|
|
13
|
+
const code = (err.code || '').toUpperCase();
|
|
14
|
+
const status = err.status;
|
|
15
|
+
const byCode = {
|
|
16
|
+
BYTE_AUTH_REQUIRED: {
|
|
17
|
+
issue: 'You are not signed in yet.',
|
|
18
|
+
why: 'This command needs your Byte account.',
|
|
19
|
+
next: 'byte login',
|
|
20
|
+
},
|
|
21
|
+
BYTE_KEY_REQUIRED: {
|
|
22
|
+
issue: 'This profile has no Byte API key.',
|
|
23
|
+
why: 'Requests run through a Byte key so every optimization applies automatically.',
|
|
24
|
+
next: 'byte keys create',
|
|
25
|
+
},
|
|
26
|
+
BYTE_API_OFFLINE: {
|
|
27
|
+
issue: 'Could not reach the Byte service.',
|
|
28
|
+
why: 'You may be offline, or the base URL is pointed somewhere unexpected.',
|
|
29
|
+
next: 'byte doctor',
|
|
30
|
+
},
|
|
31
|
+
BYTE_MODEL_FETCH_FAILED: {
|
|
32
|
+
issue: 'Your provider was saved, but its model list could not be loaded.',
|
|
33
|
+
why: 'The provider URL or key looks unreachable right now — your connection is still saved.',
|
|
34
|
+
next: 'byte providers test',
|
|
35
|
+
},
|
|
36
|
+
BYTE_DEVICE_DENIED: {
|
|
37
|
+
issue: 'Sign-in was declined in the browser.',
|
|
38
|
+
why: 'The device approval was rejected or closed.',
|
|
39
|
+
next: 'byte login',
|
|
40
|
+
},
|
|
41
|
+
BYTE_DEVICE_EXPIRED: {
|
|
42
|
+
issue: 'The sign-in code expired.',
|
|
43
|
+
why: 'It was not approved in time.',
|
|
44
|
+
next: 'byte login',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
if (byCode[code])
|
|
48
|
+
return byCode[code];
|
|
49
|
+
if (status === 0) {
|
|
50
|
+
return { issue: 'Could not reach the Byte service.', why: 'You may be offline.', next: 'byte doctor' };
|
|
51
|
+
}
|
|
52
|
+
if (status === 401 || status === 403) {
|
|
53
|
+
return { issue: 'Your session or key was rejected.', why: 'It may have expired, been revoked, or be missing a scope.', next: 'byte login' };
|
|
54
|
+
}
|
|
55
|
+
if (status === 404) {
|
|
56
|
+
return {
|
|
57
|
+
issue: 'That route or model was not found.',
|
|
58
|
+
why: 'The model may not be wired to your key, or the surface (e.g. /v1/messages) is not enabled for it.',
|
|
59
|
+
next: 'byte doctor',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (status === 429) {
|
|
63
|
+
return { issue: 'Rate limit reached.', why: 'Too many requests landed in a short window.', next: 'wait a few seconds, then retry' };
|
|
64
|
+
}
|
|
65
|
+
if (status && status >= 500) {
|
|
66
|
+
return {
|
|
67
|
+
issue: 'The service hit an error finishing your request.',
|
|
68
|
+
why: 'This is on the Byte side, not your input — your key and provider are fine.',
|
|
69
|
+
next: 'byte doctor (then retry shortly)',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { issue: err.message || 'Something went wrong.', why: `Error code: ${err.code}`, next: 'byte doctor' };
|
|
73
|
+
}
|
|
74
|
+
// A compact, left-accent error panel. picocolors auto-disables under NO_COLOR, so this
|
|
75
|
+
// degrades to clean monochrome text in plain terminals.
|
|
76
|
+
function renderErrorPanel(f) {
|
|
77
|
+
const bar = picocolors_1.default.red('▌');
|
|
78
|
+
const lines = [
|
|
79
|
+
`${bar} ${picocolors_1.default.bold(picocolors_1.default.red('Byte hit a problem'))}`,
|
|
80
|
+
`${bar} ${f.issue}`,
|
|
81
|
+
`${bar}`,
|
|
82
|
+
`${bar} ${picocolors_1.default.dim('Why ')} ${f.why}`,
|
|
83
|
+
`${bar} ${picocolors_1.default.dim('Next')} ${picocolors_1.default.cyan(f.next)}`,
|
|
84
|
+
];
|
|
85
|
+
return `\n${lines.join('\n')}\n`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ByteApi } from './api';
|
|
2
|
+
export declare function wordmark(): string;
|
|
3
|
+
export interface HomeData {
|
|
4
|
+
email?: string;
|
|
5
|
+
org?: string;
|
|
6
|
+
providers?: number;
|
|
7
|
+
models?: number;
|
|
8
|
+
byteKey?: string;
|
|
9
|
+
gateway?: 'healthy' | 'down' | 'unknown';
|
|
10
|
+
savingsSeries: number[];
|
|
11
|
+
savedTotal: number;
|
|
12
|
+
tokensTotal: number;
|
|
13
|
+
hitRate?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadHomeData(api: ByteApi, byteKey: string | undefined, fallbackId: string): Promise<HomeData>;
|
|
16
|
+
export declare function renderHome(data: HomeData, signedIn: boolean): string;
|
package/dist/lib/home.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.wordmark = wordmark;
|
|
7
|
+
exports.loadHomeData = loadHomeData;
|
|
8
|
+
exports.renderHome = renderHome;
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const output_1 = require("./output");
|
|
11
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
12
|
+
const visLen = (s) => s.replace(ANSI, '').length;
|
|
13
|
+
const plainMode = () => Boolean(process.env.NO_COLOR) || process.env.BYTE_PLAIN === '1';
|
|
14
|
+
// Cyan→blue gradient wordmark. picocolors has no true gradient, so we step the letters
|
|
15
|
+
// across a cyan→blue ramp — a recognizable Byte signature that degrades to plain text.
|
|
16
|
+
function wordmark() {
|
|
17
|
+
const mark = `${picocolors_1.default.dim('›')}${picocolors_1.default.bold(picocolors_1.default.cyanBright('b'))}${picocolors_1.default.dim('_')}`;
|
|
18
|
+
if (plainMode())
|
|
19
|
+
return `${mark} BYTE`;
|
|
20
|
+
const ramp = [picocolors_1.default.cyanBright, picocolors_1.default.cyan, picocolors_1.default.blue, picocolors_1.default.blueBright];
|
|
21
|
+
const name = 'BYTE'
|
|
22
|
+
.split('')
|
|
23
|
+
.map((ch, i) => picocolors_1.default.bold(ramp[i % ramp.length](ch)))
|
|
24
|
+
.join('');
|
|
25
|
+
return `${mark} ${name}`;
|
|
26
|
+
}
|
|
27
|
+
function box(lines, pad = 2) {
|
|
28
|
+
const inner = Math.max(...lines.map(visLen));
|
|
29
|
+
const horiz = '─'.repeat(inner + pad * 2);
|
|
30
|
+
const body = lines.map((l) => `${picocolors_1.default.dim('│')}${' '.repeat(pad)}${l}${' '.repeat(inner - visLen(l) + pad)}${picocolors_1.default.dim('│')}`);
|
|
31
|
+
return [picocolors_1.default.dim(`╭${horiz}╮`), ...body, picocolors_1.default.dim(`╰${horiz}╯`)].join('\n');
|
|
32
|
+
}
|
|
33
|
+
function withTimeout(p, ms) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const timer = setTimeout(() => resolve(undefined), ms);
|
|
36
|
+
if (typeof timer.unref === 'function')
|
|
37
|
+
timer.unref();
|
|
38
|
+
p.then((v) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve(v);
|
|
41
|
+
}, () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve(undefined);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Fetches everything bare `byte` shows, in parallel, each guarded by a short timeout so a
|
|
48
|
+
// slow or unreachable service never blocks the home screen — missing cells just render dim.
|
|
49
|
+
async function loadHomeData(api, byteKey, fallbackId) {
|
|
50
|
+
const [me, provs, usage, health] = await Promise.all([
|
|
51
|
+
withTimeout(api.me(), 4000),
|
|
52
|
+
withTimeout(api.providersList(), 4000),
|
|
53
|
+
withTimeout(api.usage('day'), 4000),
|
|
54
|
+
withTimeout(api.health(), 3000),
|
|
55
|
+
]);
|
|
56
|
+
const conns = provs?.connections ?? (Array.isArray(provs) ? provs : []);
|
|
57
|
+
const models = conns.reduce((a, c) => a + (Number(c.models ?? c.model_count ?? 0) || 0), 0);
|
|
58
|
+
const series = usage?.series ?? [];
|
|
59
|
+
const num = (v) => {
|
|
60
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
61
|
+
return Number.isFinite(n) ? n : 0;
|
|
62
|
+
};
|
|
63
|
+
const requests = series.reduce((a, p) => a + num(p.requests), 0);
|
|
64
|
+
const cached = series.reduce((a, p) => a + num(p.cached), 0);
|
|
65
|
+
return {
|
|
66
|
+
email: me?.user?.email,
|
|
67
|
+
org: me?.org?.name ?? me?.user?.org_name,
|
|
68
|
+
providers: conns.length || undefined,
|
|
69
|
+
models: models || undefined,
|
|
70
|
+
byteKey,
|
|
71
|
+
gateway: health ? (health.status === 'ok' || health.ok ? 'healthy' : 'down') : 'unknown',
|
|
72
|
+
savingsSeries: series.map((p) => num(p.savings_usd)),
|
|
73
|
+
savedTotal: series.reduce((a, p) => a + num(p.savings_usd), 0),
|
|
74
|
+
tokensTotal: series.reduce((a, p) => a + num(p.tokens_in) + num(p.tokens_out), 0),
|
|
75
|
+
hitRate: requests > 0 ? cached / requests : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function renderHome(data, signedIn) {
|
|
79
|
+
const dim = picocolors_1.default.dim;
|
|
80
|
+
const label = (s) => dim(s.padEnd(10));
|
|
81
|
+
const dash = dim('—');
|
|
82
|
+
const gatewayDot = data.gateway === 'healthy' ? `${picocolors_1.default.green('●')} healthy` : data.gateway === 'down' ? `${picocolors_1.default.red('●')} unreachable` : dim('● unknown');
|
|
83
|
+
const lines = [
|
|
84
|
+
`${wordmark()}${' '.repeat(6)}${dim('optimization gateway')}`,
|
|
85
|
+
'',
|
|
86
|
+
`${label('Account')}${signedIn ? `${data.email ?? 'you'}${data.org ? ` ${dim('·')} ${data.org}` : ''}` : picocolors_1.default.yellow('not signed in')}`,
|
|
87
|
+
`${label('Providers')}${data.providers ? `${data.providers} connected${data.models ? ` ${dim('·')} ${data.models} models` : ''}` : dash}`,
|
|
88
|
+
`${label('Byte key')}${data.byteKey ? picocolors_1.default.cyan((0, output_1.maskKey)(data.byteKey)) : dash}`,
|
|
89
|
+
`${label('Gateway')}${gatewayDot}`,
|
|
90
|
+
];
|
|
91
|
+
if (data.savingsSeries.some((v) => v > 0) || data.savedTotal > 0) {
|
|
92
|
+
const spark = (0, output_1.sparkline)(data.savingsSeries, plainMode());
|
|
93
|
+
const summary = `${picocolors_1.default.green((0, output_1.fmtUsd)(data.savedTotal))} saved ${dim('·')} ${(0, output_1.fmtNum)(data.tokensTotal)} tokens${data.hitRate !== undefined ? ` ${dim('·')} ${(0, output_1.pct)(data.hitRate)} cache hits` : ''}`;
|
|
94
|
+
lines.push('', `${label('7-day')}${picocolors_1.default.cyan(spark)} ${summary}`);
|
|
95
|
+
}
|
|
96
|
+
return box(lines);
|
|
97
|
+
}
|