@bytevion/cli 0.4.1 → 0.5.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 (42) hide show
  1. package/dist/commands/connect.d.ts +12 -0
  2. package/dist/commands/connect.js +64 -0
  3. package/dist/commands/dash.d.ts +9 -0
  4. package/dist/commands/dash.js +120 -0
  5. package/dist/commands/integrate.d.ts +3 -0
  6. package/dist/commands/integrate.js +58 -6
  7. package/dist/commands/keys/rotate.d.ts +3 -0
  8. package/dist/commands/keys/rotate.js +9 -1
  9. package/dist/commands/mcp/add.d.ts +13 -0
  10. package/dist/commands/mcp/add.js +31 -0
  11. package/dist/commands/mcp/list.d.ts +5 -0
  12. package/dist/commands/mcp/list.js +22 -0
  13. package/dist/commands/mcp/remove.d.ts +11 -0
  14. package/dist/commands/mcp/remove.js +19 -0
  15. package/dist/commands/mcp/test.d.ts +11 -0
  16. package/dist/commands/mcp/test.js +20 -0
  17. package/dist/commands/providers/compare.d.ts +13 -0
  18. package/dist/commands/providers/compare.js +52 -0
  19. package/dist/commands/sync.d.ts +10 -0
  20. package/dist/commands/sync.js +39 -0
  21. package/dist/lib/api.d.ts +7 -0
  22. package/dist/lib/api.js +24 -0
  23. package/dist/lib/config-formats.d.ts +3 -0
  24. package/dist/lib/config-formats.js +88 -0
  25. package/dist/lib/detect.d.ts +9 -0
  26. package/dist/lib/detect.js +83 -0
  27. package/dist/lib/friendly.js +10 -0
  28. package/dist/lib/integrations.d.ts +1 -0
  29. package/dist/lib/integrations.js +33 -4
  30. package/dist/lib/paths.d.ts +2 -0
  31. package/dist/lib/paths.js +13 -0
  32. package/dist/lib/tui.d.ts +42 -0
  33. package/dist/lib/tui.js +10 -0
  34. package/dist/lib/wiring.d.ts +10 -0
  35. package/dist/lib/wiring.js +53 -0
  36. package/dist/tui/components/Dash.d.ts +16 -0
  37. package/dist/tui/components/Dash.js +53 -0
  38. package/dist/tui/contract.d.ts +42 -0
  39. package/dist/tui/index.d.ts +2 -1
  40. package/dist/tui/index.js +34 -0
  41. package/oclif.manifest.json +585 -96
  42. package/package.json +4 -1
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../base");
5
+ const wiring_1 = require("../lib/wiring");
6
+ class Sync extends base_1.BaseCommand {
7
+ static description = 'Re-apply your current Byte key/model to every wired coding agent (run after rotating a key or switching model).';
8
+ static examples = ['<%= config.bin %> sync', '<%= config.bin %> sync --model byte-2-deepseek-v4-flash'];
9
+ static flags = {
10
+ model: core_1.Flags.string({ description: 'Model id (byte alias) to wire (defaults to the last wired model)' }),
11
+ yes: core_1.Flags.boolean({ char: 'y', description: 'Skip confirmation prompts' }),
12
+ };
13
+ async run() {
14
+ const { flags } = await this.parse(Sync);
15
+ this.requireByteKey(flags);
16
+ const profile = this.profileFrom(flags);
17
+ const wiring = (0, wiring_1.getWiring)(profile);
18
+ const tools = wiring.tools || [];
19
+ if (!tools.length) {
20
+ if (!this.jsonEnabled())
21
+ this.log('No wired tools yet. Run `byte connect` first.');
22
+ return { synced: [] };
23
+ }
24
+ const model = flags.model || wiring.model;
25
+ if (flags.model)
26
+ (0, wiring_1.setSelectedModel)(profile, flags.model);
27
+ const synced = [];
28
+ for (const key of tools) {
29
+ const argv = [key, '--write', '--yes', ...(model ? ['--model', model] : [])];
30
+ // eslint-disable-next-line no-await-in-loop
31
+ await this.config.runCommand('integrate', argv);
32
+ synced.push(key);
33
+ }
34
+ if (!this.jsonEnabled())
35
+ this.log(`Synced ${synced.length} tool(s): ${synced.join(', ')}.`);
36
+ return { synced, model };
37
+ }
38
+ }
39
+ exports.default = Sync;
package/dist/lib/api.d.ts CHANGED
@@ -23,10 +23,17 @@ export declare class ByteApi {
23
23
  providersRotate(id: number, apiKey: string): Promise<any>;
24
24
  providersTest(id: number): Promise<any>;
25
25
  modelEnable(id: number): Promise<any>;
26
+ modelsList(): Promise<any>;
27
+ modelCompare(id: number, on: boolean): Promise<any>;
28
+ mcpServers(): Promise<any>;
29
+ mcpAdd(body: Record<string, unknown>): Promise<any>;
30
+ mcpRemove(id: number): Promise<any>;
31
+ mcpTest(id: number): Promise<any>;
26
32
  optShow(): Promise<any>;
27
33
  optPatch(body: Record<string, unknown>): Promise<any>;
28
34
  usage(granularity: string): Promise<any>;
29
35
  dashboard(): Promise<any>;
36
+ requests(): Promise<any>;
30
37
  cacheStats(): Promise<any>;
31
38
  costs(): Promise<any>;
32
39
  health(): Promise<any>;
package/dist/lib/api.js CHANGED
@@ -100,6 +100,27 @@ class ByteApi {
100
100
  modelEnable(id) {
101
101
  return this.request('PATCH', `/api/v1/models/${id}`, { body: { enabled: true } });
102
102
  }
103
+ modelsList() {
104
+ return this.request('GET', '/api/v1/models');
105
+ }
106
+ // Toggle direct-vs-Byte comparison eligibility. Two priced models with compare on
107
+ // form the auto-pair `byte compare` needs; one is enough for `byte compare --model <alias>`.
108
+ modelCompare(id, on) {
109
+ return this.request('PATCH', `/api/v1/models/${id}`, { body: { compare_enabled: on } });
110
+ }
111
+ // --- MCP servers (Byte-hosted) ---
112
+ mcpServers() {
113
+ return this.request('GET', '/api/v1/mcp/servers');
114
+ }
115
+ mcpAdd(body) {
116
+ return this.request('POST', '/api/v1/mcp/servers', { body });
117
+ }
118
+ mcpRemove(id) {
119
+ return this.request('DELETE', `/api/v1/mcp/servers/${id}`);
120
+ }
121
+ mcpTest(id) {
122
+ return this.request('POST', `/api/v1/mcp/servers/${id}/test`, { body: {} });
123
+ }
103
124
  // --- optimizations ---
104
125
  optShow() {
105
126
  return this.request('GET', '/api/v1/optimizations');
@@ -114,6 +135,9 @@ class ByteApi {
114
135
  dashboard() {
115
136
  return this.request('GET', '/api/v1/dashboard/summary');
116
137
  }
138
+ requests() {
139
+ return this.request('GET', '/api/v1/requests');
140
+ }
117
141
  cacheStats() {
118
142
  return this.request('GET', '/api/v1/cache/stats');
119
143
  }
@@ -0,0 +1,3 @@
1
+ export type SettingsFormat = 'json' | 'toml';
2
+ export declare function parseConfig(format: SettingsFormat, text: string): any;
3
+ export declare function stringifyConfig(format: SettingsFormat, value: any): string;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseConfig = parseConfig;
4
+ exports.stringifyConfig = stringifyConfig;
5
+ function parseConfig(format, text) {
6
+ if (format === 'toml')
7
+ return parseToml(text);
8
+ return text.trim() ? JSON.parse(text) : {};
9
+ }
10
+ function stringifyConfig(format, value) {
11
+ if (format === 'toml')
12
+ return stringifyToml(value);
13
+ return `${JSON.stringify(value, null, 2)}\n`;
14
+ }
15
+ function parseTomlValue(raw) {
16
+ const v = raw.trim();
17
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
18
+ return v.slice(1, -1);
19
+ if (v === 'true')
20
+ return true;
21
+ if (v === 'false')
22
+ return false;
23
+ if (/^-?\d+$/.test(v))
24
+ return Number(v);
25
+ if (/^-?\d*\.\d+$/.test(v))
26
+ return Number(v);
27
+ if (v.startsWith('[') || v.startsWith('{'))
28
+ throw new Error('BYTE_TOML_UNSUPPORTED');
29
+ return v;
30
+ }
31
+ function parseToml(text) {
32
+ const root = {};
33
+ let current = root;
34
+ for (const line of text.split(/\r?\n/)) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith('#'))
37
+ continue;
38
+ const section = trimmed.match(/^\[([^\]]+)\]$/);
39
+ if (section) {
40
+ current = root;
41
+ for (const part of section[1].split('.').map((p) => p.trim())) {
42
+ current[part] = current[part] && typeof current[part] === 'object' ? current[part] : {};
43
+ current = current[part];
44
+ }
45
+ continue;
46
+ }
47
+ const eq = trimmed.indexOf('=');
48
+ if (eq === -1)
49
+ throw new Error('BYTE_TOML_UNSUPPORTED');
50
+ current[trimmed.slice(0, eq).trim()] = parseTomlValue(trimmed.slice(eq + 1));
51
+ }
52
+ return root;
53
+ }
54
+ function formatTomlValue(value) {
55
+ if (typeof value === 'boolean')
56
+ return value ? 'true' : 'false';
57
+ if (typeof value === 'number')
58
+ return String(value);
59
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
60
+ }
61
+ function stringifyToml(obj) {
62
+ const scalars = [];
63
+ const tables = [];
64
+ const emitTable = (path, table) => {
65
+ const lines = [`[${path}]`];
66
+ const nested = [];
67
+ for (const [key, value] of Object.entries(table)) {
68
+ if (value && typeof value === 'object' && !Array.isArray(value))
69
+ nested.push([key, value]);
70
+ else
71
+ lines.push(`${key} = ${formatTomlValue(value)}`);
72
+ }
73
+ tables.push(lines.join('\n'));
74
+ for (const [key, value] of nested)
75
+ emitTable(`${path}.${key}`, value);
76
+ };
77
+ for (const [key, value] of Object.entries(obj)) {
78
+ if (value && typeof value === 'object' && !Array.isArray(value))
79
+ emitTable(key, value);
80
+ else
81
+ scalars.push(`${key} = ${formatTomlValue(value)}`);
82
+ }
83
+ const parts = [...scalars];
84
+ if (scalars.length && tables.length)
85
+ parts.push('');
86
+ parts.push(...tables);
87
+ return `${parts.join('\n')}\n`;
88
+ }
@@ -0,0 +1,9 @@
1
+ export interface DetectedTool {
2
+ key: string;
3
+ title: string;
4
+ installed: boolean;
5
+ reason: 'binary' | 'config' | 'both' | 'none';
6
+ canAutoWrite: boolean;
7
+ configPath?: string;
8
+ }
9
+ export declare function detectTools(): DetectedTool[];
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectTools = detectTools;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_os_1 = require("node:os");
6
+ const node_path_1 = require("node:path");
7
+ const integrations_1 = require("./integrations");
8
+ // Per-tool detection hints: binaries we look for on PATH, and config dirs/files whose
9
+ // presence also implies the tool is installed. Keys must match integrations.ts entries.
10
+ const PROBES = {
11
+ opencode: { bins: ['opencode'], configs: [(0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'opencode')] },
12
+ 'claude-code': { bins: ['claude'], configs: [(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude')] },
13
+ codex: { bins: ['codex'], configs: [process.env.CODEX_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex')] },
14
+ cursor: { bins: ['cursor'], configs: [] },
15
+ cline: { bins: [], configs: [] },
16
+ aider: { bins: ['aider'], configs: [(0, node_path_1.join)((0, node_os_1.homedir)(), '.aider.conf.yml')] },
17
+ continue: { bins: ['cn'], configs: [(0, node_path_1.join)((0, node_os_1.homedir)(), '.continue')] },
18
+ zed: { bins: ['zed', 'zeditor'], configs: [(0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'zed')] },
19
+ goose: { bins: ['goose'], configs: [] },
20
+ 'gemini-cli': { bins: ['gemini'], configs: [] },
21
+ 'qwen-code': { bins: ['qwen'], configs: [] },
22
+ };
23
+ function exists(path) {
24
+ try {
25
+ return (0, node_fs_1.existsSync)(path);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function binaryOnPath(name) {
32
+ const dirs = (process.env.PATH || '').split(node_path_1.delimiter).filter(Boolean);
33
+ const exts = process.platform === 'win32' ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.PS1').split(';').filter(Boolean) : [''];
34
+ for (const dir of dirs) {
35
+ if (process.platform !== 'win32' && exists((0, node_path_1.join)(dir, name)))
36
+ return true;
37
+ for (const ext of exts) {
38
+ if (exists((0, node_path_1.join)(dir, name + ext)))
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ function hasWriter(key) {
45
+ const integ = (0, integrations_1.findIntegration)(key);
46
+ if (!integ)
47
+ return false;
48
+ try {
49
+ return Boolean(integ.build({ baseUrl: '', byteKey: '', modelAlias: '' }).settingsFile);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ // Inspect the local machine for installed coding agents (PATH binaries + known config
56
+ // dirs). Pure filesystem + env reads — no process spawning, sandbox-safe and fast.
57
+ function detectTools() {
58
+ const known = new Set((0, integrations_1.listIntegrations)().map((i) => i.key));
59
+ const out = [];
60
+ for (const [key, probe] of Object.entries(PROBES)) {
61
+ if (!known.has(key))
62
+ continue;
63
+ const integ = (0, integrations_1.findIntegration)(key);
64
+ if (!integ)
65
+ continue;
66
+ const hasBin = probe.bins.some(binaryOnPath);
67
+ const cfgPath = probe.configs.find(exists);
68
+ const hasCfg = Boolean(cfgPath);
69
+ if (!hasBin && !hasCfg) {
70
+ out.push({ key, title: integ.title, installed: false, reason: 'none', canAutoWrite: hasWriter(key) });
71
+ continue;
72
+ }
73
+ out.push({
74
+ key,
75
+ title: integ.title,
76
+ installed: true,
77
+ reason: hasBin && hasCfg ? 'both' : hasBin ? 'binary' : 'config',
78
+ canAutoWrite: hasWriter(key),
79
+ configPath: cfgPath,
80
+ });
81
+ }
82
+ return out;
83
+ }
@@ -33,6 +33,16 @@ function friendlyError(err) {
33
33
  why: 'The provider URL or key looks unreachable right now — your connection is still saved.',
34
34
  next: 'byte providers test',
35
35
  },
36
+ BYTE_AUTO_PAIR_INCOMPLETE: {
37
+ issue: 'Compare needs a baseline↔Byte model pair.',
38
+ why: 'No two priced models have comparison enabled yet, so there is nothing to compare against.',
39
+ next: 'byte providers compare --all',
40
+ },
41
+ BYTE_MODEL_COMPARE_DISABLED: {
42
+ issue: 'Comparison is turned off for this model.',
43
+ why: 'A model must have comparison enabled to run the direct-vs-Byte lanes.',
44
+ next: 'byte providers compare <alias>',
45
+ },
36
46
  BYTE_DEVICE_DENIED: {
37
47
  issue: 'Sign-in was declined in the browser.',
38
48
  why: 'The device approval was rejected or closed.',
@@ -6,6 +6,7 @@ export interface IntegrationContext {
6
6
  }
7
7
  export interface SettingsFile {
8
8
  path: string;
9
+ format?: 'json' | 'toml';
9
10
  apply: (existing: any) => any;
10
11
  }
11
12
  export interface IntegrationResult {
@@ -4,6 +4,7 @@ exports.listIntegrations = listIntegrations;
4
4
  exports.findIntegration = findIntegration;
5
5
  const node_os_1 = require("node:os");
6
6
  const node_path_1 = require("node:path");
7
+ const paths_1 = require("./paths");
7
8
  const openaiEnv = (ctx) => [
8
9
  ['OPENAI_BASE_URL', `${ctx.baseUrl}/v1`],
9
10
  ['OPENAI_API_KEY', ctx.byteKey],
@@ -39,8 +40,21 @@ const INTEGRATIONS = [
39
40
  title: 'Codex CLI',
40
41
  compat: 'openai',
41
42
  build: (ctx) => ({
42
- env: openaiEnv(ctx),
43
- instructions: ['Codex CLI uses OPENAI_BASE_URL + OPENAI_API_KEY (or ~/.codex/config.toml). Set the env vars below, then run codex.'],
43
+ env: [['BYTE_API_KEY', ctx.byteKey]],
44
+ instructions: [
45
+ `Wires the "byte" provider in ${(0, paths_1.codexConfigPath)()} (model: ${ctx.modelAlias}).`,
46
+ 'Export BYTE_API_KEY (above) so Codex reads it via env_key.',
47
+ ],
48
+ settingsFile: {
49
+ path: (0, paths_1.codexConfigPath)(),
50
+ format: 'toml',
51
+ apply: (existing) => {
52
+ const cfg = existing && typeof existing === 'object' ? existing : {};
53
+ const providers = { ...(cfg.model_providers ?? {}) };
54
+ providers.byte = { ...(providers.byte ?? {}), name: 'Byte', base_url: `${ctx.baseUrl}/v1`, wire_api: 'chat', env_key: 'BYTE_API_KEY' };
55
+ return { ...cfg, model_provider: 'byte', model: ctx.modelAlias, model_providers: providers };
56
+ },
57
+ },
44
58
  }),
45
59
  },
46
60
  {
@@ -138,8 +152,23 @@ const INTEGRATIONS = [
138
152
  title: 'Zed',
139
153
  compat: 'openai',
140
154
  build: (ctx) => ({
141
- env: [],
142
- instructions: ['Zed → settings.json, add a custom LLM provider with:', ` "api_url": "${ctx.baseUrl}/v1"`, ` API key set via Zed’s key prompt; model ${ctx.modelAlias}`],
155
+ env: [['BYTE_API_KEY', ctx.byteKey]],
156
+ instructions: [
157
+ `Wires an OpenAI-compatible "byte" provider in ${(0, paths_1.zedSettingsPath)()} (model ${ctx.modelAlias}).`,
158
+ 'Enter the BYTE_API_KEY (above) when Zed prompts for the provider key.',
159
+ ],
160
+ settingsFile: {
161
+ path: (0, paths_1.zedSettingsPath)(),
162
+ format: 'json',
163
+ apply: (existing) => {
164
+ const cfg = existing && typeof existing === 'object' ? existing : {};
165
+ const languageModels = { ...(cfg.language_models ?? {}) };
166
+ const openaiCompatible = { ...(languageModels.openai_compatible ?? {}) };
167
+ openaiCompatible.byte = { ...(openaiCompatible.byte ?? {}), api_url: `${ctx.baseUrl}/v1`, available_models: [{ name: ctx.modelAlias }] };
168
+ languageModels.openai_compatible = openaiCompatible;
169
+ return { ...cfg, language_models: languageModels };
170
+ },
171
+ },
143
172
  }),
144
173
  },
145
174
  {
@@ -1,3 +1,5 @@
1
1
  export declare function byteDir(): string;
2
2
  export declare function configPath(): string;
3
3
  export declare function credentialsPath(): string;
4
+ export declare function codexConfigPath(): string;
5
+ export declare function zedSettingsPath(): string;
package/dist/lib/paths.js CHANGED
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.byteDir = byteDir;
4
4
  exports.configPath = configPath;
5
5
  exports.credentialsPath = credentialsPath;
6
+ exports.codexConfigPath = codexConfigPath;
7
+ exports.zedSettingsPath = zedSettingsPath;
6
8
  const node_fs_1 = require("node:fs");
7
9
  const node_os_1 = require("node:os");
8
10
  const node_path_1 = require("node:path");
@@ -17,3 +19,14 @@ function configPath() {
17
19
  function credentialsPath() {
18
20
  return (0, node_path_1.join)(byteDir(), 'credentials.json');
19
21
  }
22
+ function xdgConfigHome() {
23
+ return process.env.XDG_CONFIG_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), '.config');
24
+ }
25
+ // Canonical config locations for the coding agents Byte auto-wires. Centralised here so
26
+ // detection (detect.ts) and the writers (integrations.ts) never disagree on a path.
27
+ function codexConfigPath() {
28
+ return (0, node_path_1.join)(process.env.CODEX_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex'), 'config.toml');
29
+ }
30
+ function zedSettingsPath() {
31
+ return (0, node_path_1.join)(xdgConfigHome(), 'zed', 'settings.json');
32
+ }
package/dist/lib/tui.d.ts CHANGED
@@ -103,3 +103,45 @@ export declare function useInk(opts?: {
103
103
  export declare function computeAscii(): boolean;
104
104
  export declare function renderHomeIsland(props: HomeIslandProps): Promise<HomeResult>;
105
105
  export declare function renderWizardIsland(props: WizardIslandProps): Promise<WizardResult>;
106
+ export interface DashRequest {
107
+ model: string;
108
+ status: string;
109
+ latencyMs: number;
110
+ tokens: number;
111
+ cache?: string;
112
+ }
113
+ export interface DashLayer {
114
+ label: string;
115
+ active: boolean;
116
+ }
117
+ export interface DashData {
118
+ email?: string;
119
+ org?: string;
120
+ gateway: 'healthy' | 'down' | 'unknown';
121
+ mode?: string;
122
+ requests: number;
123
+ cacheHitRate: number;
124
+ savedTotal: number;
125
+ tokensTotal: number;
126
+ costTotal: number;
127
+ savingsSeries: number[];
128
+ providers: number;
129
+ models: number;
130
+ recent: DashRequest[];
131
+ layers: DashLayer[];
132
+ }
133
+ export interface DashIslandProps {
134
+ data: DashData;
135
+ ports: {
136
+ refresh(): Promise<DashData>;
137
+ };
138
+ plainColor: boolean;
139
+ ascii: boolean;
140
+ version: string;
141
+ }
142
+ export type DashResult = {
143
+ status: 'fallback';
144
+ } | {
145
+ status: 'exit';
146
+ };
147
+ export declare function renderDashIsland(props: DashIslandProps): Promise<DashResult>;
package/dist/lib/tui.js CHANGED
@@ -7,6 +7,7 @@ exports.useInk = useInk;
7
7
  exports.computeAscii = computeAscii;
8
8
  exports.renderHomeIsland = renderHomeIsland;
9
9
  exports.renderWizardIsland = renderWizardIsland;
10
+ exports.renderDashIsland = renderDashIsland;
10
11
  const node_path_1 = __importDefault(require("node:path"));
11
12
  const node_url_1 = require("node:url");
12
13
  // CJS→ESM bridge for the Ink island. This file stays under src/lib (CommonJS) and is the ONLY
@@ -60,3 +61,12 @@ async function renderWizardIsland(props) {
60
61
  return { status: 'fallback' };
61
62
  }
62
63
  }
64
+ async function renderDashIsland(props) {
65
+ try {
66
+ const mod = await importESM(islandUrl());
67
+ return (await mod.renderDash(props));
68
+ }
69
+ catch {
70
+ return { status: 'fallback' };
71
+ }
72
+ }
@@ -0,0 +1,10 @@
1
+ export interface Wiring {
2
+ model?: string;
3
+ preset?: string;
4
+ tools: string[];
5
+ }
6
+ export declare function getWiring(profile: string): Wiring;
7
+ export declare function setWiring(profile: string, patch: Partial<Wiring>): void;
8
+ export declare function addWiredTool(profile: string, key: string): void;
9
+ export declare function setSelectedModel(profile: string, model: string): void;
10
+ export declare function setSelectedPreset(profile: string, preset: string): void;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getWiring = getWiring;
4
+ exports.setWiring = setWiring;
5
+ exports.addWiredTool = addWiredTool;
6
+ exports.setSelectedModel = setSelectedModel;
7
+ exports.setSelectedPreset = setSelectedPreset;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const paths_1 = require("./paths");
11
+ function wiringPath() {
12
+ return (0, node_path_1.join)((0, paths_1.byteDir)(), 'wiring.json');
13
+ }
14
+ function load() {
15
+ try {
16
+ const data = JSON.parse((0, node_fs_1.readFileSync)(wiringPath(), 'utf8'));
17
+ return data && typeof data === 'object' ? data : {};
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ function persist(store) {
24
+ const path = wiringPath();
25
+ const tmp = `${path}.tmp`;
26
+ (0, node_fs_1.writeFileSync)(tmp, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
27
+ (0, node_fs_1.renameSync)(tmp, path);
28
+ }
29
+ function getWiring(profile) {
30
+ return load()[profile] || { tools: [] };
31
+ }
32
+ function setWiring(profile, patch) {
33
+ const store = load();
34
+ const current = store[profile] || { tools: [] };
35
+ store[profile] = { ...current, ...patch };
36
+ persist(store);
37
+ }
38
+ function addWiredTool(profile, key) {
39
+ const store = load();
40
+ const current = store[profile] || { tools: [] };
41
+ if (!current.tools.includes(key))
42
+ current.tools = [...current.tools, key];
43
+ store[profile] = current;
44
+ persist(store);
45
+ }
46
+ function setSelectedModel(profile, model) {
47
+ if (model)
48
+ setWiring(profile, { model });
49
+ }
50
+ function setSelectedPreset(profile, preset) {
51
+ if (preset)
52
+ setWiring(profile, { preset });
53
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import type { DashData } from '../contract.js';
3
+ interface DashProps {
4
+ data: DashData;
5
+ ports: {
6
+ refresh(): Promise<DashData>;
7
+ };
8
+ ascii: boolean;
9
+ plainColor: boolean;
10
+ version: string;
11
+ onDone: (result: {
12
+ status: 'fallback' | 'exit';
13
+ }) => void;
14
+ }
15
+ export declare function Dash({ data: initial, ports, ascii, plainColor, version, onDone }: DashProps): React.ReactElement;
16
+ export {};
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import React from 'react';
4
+ import { glyphs, gradient, tones } from '../theme.js';
5
+ import { Panel } from './Panel.js';
6
+ function Spark({ series, ascii, plainColor }) {
7
+ const ramp = glyphs(ascii).spark;
8
+ const values = series.length ? series : [0];
9
+ const max = Math.max(...values, 1);
10
+ const cols = gradient(values.length);
11
+ return (_jsx(Box, { children: values.map((value, i) => {
12
+ const idx = Math.min(ramp.length - 1, Math.max(0, Math.round((value / max) * (ramp.length - 1))));
13
+ return (_jsx(Text, { color: plainColor ? undefined : cols[i], children: ramp[idx] }, i));
14
+ }) }));
15
+ }
16
+ // Dense live monitor: a brand/health header, a one-line metric strip with a savings sparkline,
17
+ // a Recent-requests panel beside an Optimizations panel, and a key-hint footer. Polls the
18
+ // injected refresh port every few seconds; q / Esc / Ctrl-C exits cleanly.
19
+ export function Dash({ data: initial, ports, ascii, plainColor, version, onDone }) {
20
+ const { exit } = useApp();
21
+ const [data, setData] = React.useState(initial);
22
+ const [tick, setTick] = React.useState(0);
23
+ const t = tones(plainColor);
24
+ const g = glyphs(ascii);
25
+ React.useEffect(() => {
26
+ let alive = true;
27
+ const timer = setInterval(() => {
28
+ ports
29
+ .refresh()
30
+ .then((next) => {
31
+ if (alive) {
32
+ setData(next);
33
+ setTick((n) => n + 1);
34
+ }
35
+ })
36
+ .catch(() => undefined);
37
+ }, 2500);
38
+ return () => {
39
+ alive = false;
40
+ clearInterval(timer);
41
+ };
42
+ }, [ports]);
43
+ useInput((input, key) => {
44
+ if (input === 'q' || key.escape || (key.ctrl && input === 'c')) {
45
+ onDone({ status: 'exit' });
46
+ exit();
47
+ }
48
+ });
49
+ const dot = data.gateway === 'healthy' ? t.ok : data.gateway === 'down' ? t.bad : t.muted;
50
+ const recent = data.recent.slice(0, 8);
51
+ const activeCount = data.layers.filter((l) => l.active).length;
52
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: t.brandMid, bold: true, children: [g.brand.join(''), " byte dash "] }), _jsxs(Text, { color: t.muted, children: ["v", version, " "] }), _jsxs(Text, { color: dot, children: [g.filled, " ", data.gateway] }), _jsxs(Text, { color: t.muted, children: [" ", data.email ?? '', data.org ? ` (${data.org})` : ''] })] }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: t.muted, children: "mode " }), _jsx(Text, { color: t.accent, children: data.mode ?? 'maximum' }), _jsx(Text, { color: t.muted, children: " saved " }), _jsxs(Text, { color: t.ok, children: ["$", data.savedTotal.toFixed(4)] }), _jsx(Text, { color: t.muted, children: " req " }), _jsx(Text, { children: data.requests }), _jsx(Text, { color: t.muted, children: " cache " }), _jsxs(Text, { color: t.accent, children: [Math.round(data.cacheHitRate * 100), "%"] }), _jsx(Text, { color: t.muted, children: " tok " }), _jsx(Text, { children: data.tokensTotal }), _jsx(Text, { color: t.muted, children: " " }), _jsx(Spark, { series: data.savingsSeries, ascii: ascii, plainColor: plainColor })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: "56%", marginRight: 1, children: _jsx(Panel, { title: "Recent requests", ascii: ascii, plainColor: plainColor, children: recent.length === 0 ? (_jsx(Text, { color: t.muted, children: "no requests yet" })) : (recent.map((r, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: r.status === 'ok' ? t.ok : t.bad, children: [g.bullet, " "] }), _jsx(Text, { children: r.model.padEnd(22).slice(0, 22) }), _jsxs(Text, { color: t.muted, children: [" ", String(r.latencyMs).padStart(5), "ms "] }), _jsxs(Text, { color: t.muted, children: [String(r.tokens).padStart(6), "tok "] }), _jsx(Text, { color: t.accent, children: r.cache ?? '' })] }, i)))) }) }), _jsx(Box, { width: "42%", children: _jsx(Panel, { title: `Optimizations ${activeCount}/${data.layers.length}`, tone: "ok", ascii: ascii, plainColor: plainColor, children: data.layers.slice(0, 12).map((l, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: l.active ? t.ok : t.muted, children: [l.active ? g.filled : g.empty, " "] }), _jsx(Text, { color: l.active ? undefined : t.muted, children: l.label })] }, i))) }) })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: t.muted, children: ["providers ", data.providers, " \u00B7 models ", data.models, " \u00B7 refresh #", tick, " "] }), _jsx(Text, { color: t.accent, children: "q" }), _jsx(Text, { color: t.muted, children: " quit" })] })] }));
53
+ }
@@ -97,3 +97,45 @@ export type WizardResult = {
97
97
  status: 'done';
98
98
  summary: WizardSummary;
99
99
  };
100
+ export interface DashRequest {
101
+ model: string;
102
+ status: string;
103
+ latencyMs: number;
104
+ tokens: number;
105
+ cache?: string;
106
+ }
107
+ export interface DashLayer {
108
+ label: string;
109
+ active: boolean;
110
+ }
111
+ export interface DashData {
112
+ email?: string;
113
+ org?: string;
114
+ gateway: 'healthy' | 'down' | 'unknown';
115
+ mode?: string;
116
+ requests: number;
117
+ cacheHitRate: number;
118
+ savedTotal: number;
119
+ tokensTotal: number;
120
+ costTotal: number;
121
+ savingsSeries: number[];
122
+ providers: number;
123
+ models: number;
124
+ recent: DashRequest[];
125
+ layers: DashLayer[];
126
+ }
127
+ export interface DashPorts {
128
+ refresh(): Promise<DashData>;
129
+ }
130
+ export interface DashIslandProps {
131
+ data: DashData;
132
+ ports: DashPorts;
133
+ plainColor: boolean;
134
+ ascii: boolean;
135
+ version: string;
136
+ }
137
+ export type DashResult = {
138
+ status: 'fallback';
139
+ } | {
140
+ status: 'exit';
141
+ };
@@ -1,3 +1,4 @@
1
- import type { HomeIslandProps, HomeResult, WizardIslandProps, WizardResult } from './contract.js';
1
+ import type { DashIslandProps, DashResult, HomeIslandProps, HomeResult, WizardIslandProps, WizardResult } from './contract.js';
2
2
  export declare function renderHome(props: HomeIslandProps): Promise<HomeResult>;
3
3
  export declare function renderWizard(props: WizardIslandProps): Promise<WizardResult>;
4
+ export declare function renderDash(props: DashIslandProps): Promise<DashResult>;