blyuecode 0.1.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 +10 -0
- package/dist/core/agent.js +53 -0
- package/dist/core/config.js +52 -0
- package/dist/core/freeModels.js +55 -0
- package/dist/core/provider.js +48 -0
- package/dist/core/tools.js +92 -0
- package/dist/index.js +20 -0
- package/dist/ui/Banner.js +47 -0
- package/dist/ui/Chat.js +72 -0
- package/dist/ui/InputBar.js +24 -0
- package/dist/ui/KeyInput.js +20 -0
- package/dist/ui/MakerMark.js +3 -0
- package/dist/ui/ModelPicker.js +57 -0
- package/dist/ui/Placeholder.js +21 -0
- package/dist/ui/RocketBar.js +32 -0
- package/dist/ui/Setup.js +121 -0
- package/dist/ui/Spinner.js +13 -0
- package/dist/ui/animColor.js +19 -0
- package/dist/ui/theme.js +8 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Provider } from './provider.js';
|
|
2
|
+
import { toolDefs, executeTool } from './tools.js';
|
|
3
|
+
export class Agent {
|
|
4
|
+
provider = null;
|
|
5
|
+
getProvider() {
|
|
6
|
+
if (!this.provider)
|
|
7
|
+
this.provider = new Provider();
|
|
8
|
+
return this.provider;
|
|
9
|
+
}
|
|
10
|
+
messages = [
|
|
11
|
+
{
|
|
12
|
+
role: 'system',
|
|
13
|
+
content: 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
14
|
+
'Use tools to read, write, and run commands. Always explain what you are doing. ' +
|
|
15
|
+
'Ask before destructive commands. Be concise.',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
async *run(userInput) {
|
|
19
|
+
this.messages.push({ role: 'user', content: userInput });
|
|
20
|
+
while (true) {
|
|
21
|
+
const res = await this.getProvider().chat(this.messages, toolDefs);
|
|
22
|
+
if (res.content)
|
|
23
|
+
yield res.content;
|
|
24
|
+
if (res.toolCalls.length === 0)
|
|
25
|
+
break;
|
|
26
|
+
this.messages.push({
|
|
27
|
+
role: 'assistant',
|
|
28
|
+
content: res.content,
|
|
29
|
+
tool_calls: res.toolCalls.map((tc) => ({
|
|
30
|
+
id: tc.id,
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
for (const tc of res.toolCalls) {
|
|
36
|
+
yield '\n[Calling ' + tc.name + '...]\n';
|
|
37
|
+
let args = {};
|
|
38
|
+
try {
|
|
39
|
+
args = JSON.parse(tc.arguments || '{}');
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
const result = await executeTool(tc.name, args);
|
|
43
|
+
this.messages.push({
|
|
44
|
+
role: 'tool',
|
|
45
|
+
tool_call_id: tc.id,
|
|
46
|
+
content: result,
|
|
47
|
+
});
|
|
48
|
+
const preview = result.length > 500 ? result.slice(0, 500) + '...' : result;
|
|
49
|
+
yield '[Result]: ' + preview + '\n';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const GLOBAL_DIR = path.join(os.homedir(), '.blyue');
|
|
5
|
+
const GLOBAL_ENV = path.join(GLOBAL_DIR, '.env');
|
|
6
|
+
// Anything matching this = not a real key
|
|
7
|
+
const PLACEHOLDER = /your[-_]?key|placeholder|^sk-your|^xxx|^sk-xxx|here$/i;
|
|
8
|
+
/** Only look at ~/.blyue/.env (global, works from any cwd) */
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
if (!fs.existsSync(GLOBAL_ENV))
|
|
11
|
+
return null;
|
|
12
|
+
const cfg = parseEnv(GLOBAL_ENV);
|
|
13
|
+
if (!cfg.apiKey)
|
|
14
|
+
return null;
|
|
15
|
+
if (PLACEHOLDER.test(cfg.apiKey))
|
|
16
|
+
return null;
|
|
17
|
+
return cfg;
|
|
18
|
+
}
|
|
19
|
+
function parseEnv(file) {
|
|
20
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
21
|
+
const map = {};
|
|
22
|
+
for (const line of text.split('\n')) {
|
|
23
|
+
const m = line.match(/^\s*([A-Z_]+)\s*=\s*(.*)\s*$/);
|
|
24
|
+
if (m)
|
|
25
|
+
map[m[1]] = m[2].trim();
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
apiKey: map.OPENAI_API_KEY || '',
|
|
29
|
+
baseUrl: map.OPENAI_BASE_URL || undefined,
|
|
30
|
+
model: map.MODEL || 'gpt-4o-mini',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function saveConfig(cfg) {
|
|
34
|
+
if (!fs.existsSync(GLOBAL_DIR))
|
|
35
|
+
fs.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
36
|
+
const lines = [
|
|
37
|
+
'# BLYUE config (auto-generated)',
|
|
38
|
+
`OPENAI_API_KEY=${cfg.apiKey}`,
|
|
39
|
+
];
|
|
40
|
+
if (cfg.baseUrl)
|
|
41
|
+
lines.push(`OPENAI_BASE_URL=${cfg.baseUrl}`);
|
|
42
|
+
lines.push(`MODEL=${cfg.model}`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
fs.writeFileSync(GLOBAL_ENV, lines.join('\n'), { mode: 0o600 });
|
|
45
|
+
return GLOBAL_ENV;
|
|
46
|
+
}
|
|
47
|
+
export function applyConfig(cfg) {
|
|
48
|
+
process.env.OPENAI_API_KEY = cfg.apiKey;
|
|
49
|
+
if (cfg.baseUrl)
|
|
50
|
+
process.env.OPENAI_BASE_URL = cfg.baseUrl;
|
|
51
|
+
process.env.MODEL = cfg.model;
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const URL = 'https://openrouter.ai/api/v1/models';
|
|
5
|
+
const CACHE = path.join(os.homedir(), '.blyue', 'models-cache.json');
|
|
6
|
+
const TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
// Fallback if API is down or first-run offline
|
|
8
|
+
const FALLBACK = [
|
|
9
|
+
{ id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Llama 3.3 70B', context: 131072 },
|
|
10
|
+
{ id: 'google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash', context: 1048576 },
|
|
11
|
+
{ id: 'deepseek/deepseek-chat-v3-0324:free', name: 'DeepSeek V3', context: 163840 },
|
|
12
|
+
{ id: 'qwen/qwen-2.5-coder-32b-instruct:free', name: 'Qwen 2.5 Coder 32B', context: 32768 },
|
|
13
|
+
];
|
|
14
|
+
export async function fetchFreeModels(apiKey) {
|
|
15
|
+
// Try cache first
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs.readFileSync(CACHE, 'utf8');
|
|
18
|
+
const cached = JSON.parse(raw);
|
|
19
|
+
if (Date.now() - cached.ts < TTL_MS)
|
|
20
|
+
return cached.models;
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
try {
|
|
24
|
+
const headers = {};
|
|
25
|
+
if (apiKey)
|
|
26
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
27
|
+
const res = await fetch(URL, { headers });
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`HTTP ${res.status}`);
|
|
30
|
+
const json = await res.json();
|
|
31
|
+
const models = (json.data || [])
|
|
32
|
+
.filter((m) => {
|
|
33
|
+
const p = m.pricing || {};
|
|
34
|
+
const isFree = parseFloat(p.prompt || '0') === 0 &&
|
|
35
|
+
parseFloat(p.completion || '0') === 0;
|
|
36
|
+
const outMods = (m.architecture?.output_modalities) || ['text'];
|
|
37
|
+
const isText = outMods.length === 1 && outMods[0] === 'text';
|
|
38
|
+
const notMeta = !m.id.startsWith('openrouter/');
|
|
39
|
+
return isFree && isText && notMeta;
|
|
40
|
+
})
|
|
41
|
+
.map((m) => ({
|
|
42
|
+
id: m.id,
|
|
43
|
+
name: m.name || m.id,
|
|
44
|
+
context: m.context_length || 0,
|
|
45
|
+
}))
|
|
46
|
+
.sort((a, b) => b.context - a.context);
|
|
47
|
+
// Save cache
|
|
48
|
+
fs.mkdirSync(path.dirname(CACHE), { recursive: true });
|
|
49
|
+
fs.writeFileSync(CACHE, JSON.stringify({ ts: Date.now(), models }));
|
|
50
|
+
return models.length > 0 ? models : FALLBACK;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return FALLBACK;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
export class Provider {
|
|
3
|
+
client;
|
|
4
|
+
model;
|
|
5
|
+
constructor() {
|
|
6
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
7
|
+
if (!apiKey)
|
|
8
|
+
throw new Error('OPENAI_API_KEY missing. Delete ~/.blyue/.env and restart.');
|
|
9
|
+
this.model = process.env.MODEL || 'gpt-4o-mini';
|
|
10
|
+
this.client = new OpenAI({
|
|
11
|
+
apiKey,
|
|
12
|
+
baseURL: process.env.OPENAI_BASE_URL,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async chat(messages, tools) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await this.client.chat.completions.create({
|
|
18
|
+
model: this.model,
|
|
19
|
+
messages: messages,
|
|
20
|
+
tools,
|
|
21
|
+
tool_choice: 'auto',
|
|
22
|
+
});
|
|
23
|
+
const msg = res.choices[0].message;
|
|
24
|
+
return {
|
|
25
|
+
content: msg.content,
|
|
26
|
+
toolCalls: (msg.tool_calls || []).map((tc) => ({
|
|
27
|
+
id: tc.id,
|
|
28
|
+
name: tc.function.name,
|
|
29
|
+
arguments: tc.function.arguments,
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const status = e?.status || e?.response?.status;
|
|
35
|
+
const raw = e?.error?.message || e?.message || String(e);
|
|
36
|
+
if (status === 401) {
|
|
37
|
+
throw new Error(`401 Unauthorized — bad API key.\n Fix: rm ~/.blyue/.env then run again.`);
|
|
38
|
+
}
|
|
39
|
+
if (status === 404) {
|
|
40
|
+
throw new Error(`404 Model unavailable: "${this.model}".\n Fix: rm ~/.blyue/.env then pick a different model.`);
|
|
41
|
+
}
|
|
42
|
+
if (status === 429) {
|
|
43
|
+
throw new Error(`429 Rate limit / quota. Try again or switch model.`);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(raw);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export const toolDefs = [
|
|
6
|
+
{
|
|
7
|
+
type: 'function',
|
|
8
|
+
function: {
|
|
9
|
+
name: 'read_file',
|
|
10
|
+
description: 'Read a file from disk',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
path: { type: 'string', description: 'File path' },
|
|
15
|
+
},
|
|
16
|
+
required: ['path'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'function',
|
|
22
|
+
function: {
|
|
23
|
+
name: 'write_file',
|
|
24
|
+
description: 'Write content to a file (overwrites existing)',
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
path: { type: 'string' },
|
|
29
|
+
content: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
required: ['path', 'content'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'function',
|
|
37
|
+
function: {
|
|
38
|
+
name: 'list_files',
|
|
39
|
+
description: 'List files in a directory',
|
|
40
|
+
parameters: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
path: { type: 'string', description: 'Directory path (default: .)' },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: {
|
|
51
|
+
name: 'run_command',
|
|
52
|
+
description: 'Run a shell command. Use carefully.',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
command: { type: 'string' },
|
|
57
|
+
},
|
|
58
|
+
required: ['command'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
export async function executeTool(name, args) {
|
|
64
|
+
try {
|
|
65
|
+
switch (name) {
|
|
66
|
+
case 'read_file':
|
|
67
|
+
return await fs.readFile(args.path, 'utf8');
|
|
68
|
+
case 'write_file':
|
|
69
|
+
await fs.writeFile(args.path, args.content, 'utf8');
|
|
70
|
+
return 'Wrote ' + args.path;
|
|
71
|
+
case 'list_files': {
|
|
72
|
+
const dir = args.path || '.';
|
|
73
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
74
|
+
return entries
|
|
75
|
+
.map((e) => (e.isDirectory() ? e.name + '/' : e.name))
|
|
76
|
+
.join('\n');
|
|
77
|
+
}
|
|
78
|
+
case 'run_command': {
|
|
79
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
82
|
+
});
|
|
83
|
+
return 'stdout:\n' + stdout + '\nstderr:\n' + stderr;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
return 'Unknown tool: ' + name;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return 'Error: ' + err.message + '\n' + (err.stdout || '') + (err.stderr || '');
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { render, Box } from 'ink';
|
|
5
|
+
import { Setup } from './ui/Setup.js';
|
|
6
|
+
import { Chat } from './ui/Chat.js';
|
|
7
|
+
import { Banner } from './ui/Banner.js';
|
|
8
|
+
import { loadConfig, applyConfig } from './core/config.js';
|
|
9
|
+
const App = () => {
|
|
10
|
+
const [ready, setReady] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const existing = loadConfig();
|
|
13
|
+
if (existing) {
|
|
14
|
+
applyConfig(existing);
|
|
15
|
+
setReady(true);
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Banner, {}), ready ? _jsx(Chat, {}) : _jsx(Setup, { onDone: () => setReady(true) })] }));
|
|
19
|
+
};
|
|
20
|
+
render(_jsx(App, {}));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { hueAt } from './animColor.js';
|
|
5
|
+
import { MakerMark } from './MakerMark.js';
|
|
6
|
+
// ---------- BLYUE ----------
|
|
7
|
+
const BLYUE_BIG = [
|
|
8
|
+
'██████╗ ██╗ ██╗ ██╗ ██╗ ██╗ ███████╗',
|
|
9
|
+
'██╔══██╗██║ ╚██╗ ██╔╝ ██║ ██║ ██╔════╝',
|
|
10
|
+
'██████╔╝██║ ╚████╔╝ ██║ ██║ █████╗ ',
|
|
11
|
+
'██╔══██╗██║ ╚██╔╝ ██║ ██║ ██╔══╝ ',
|
|
12
|
+
'██████╔╝███████╗ ██║ ╚██████╔╝ ███████╗',
|
|
13
|
+
'╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚══════╝',
|
|
14
|
+
];
|
|
15
|
+
const BLYUE_SMALL = [
|
|
16
|
+
' ___ _ _ _ _ _ ___ ',
|
|
17
|
+
'| _ )| | | | | | | | | __|',
|
|
18
|
+
'| _ \\| |__| |_| | |_| | _| ',
|
|
19
|
+
'|___/|____|\\___/ \\___/|___|',
|
|
20
|
+
];
|
|
21
|
+
// ---------- CODE ----------
|
|
22
|
+
const CODE_BIG = [
|
|
23
|
+
' ██████╗ ██████╗ ██████╗ ███████╗',
|
|
24
|
+
'██╔════╝██╔═══██╗██╔══██╗██╔════╝',
|
|
25
|
+
'██║ ██║ ██║██║ ██║█████╗ ',
|
|
26
|
+
'██║ ██║ ██║██║ ██║██╔══╝ ',
|
|
27
|
+
'╚██████╗╚██████╔╝██████╔╝███████╗',
|
|
28
|
+
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
|
|
29
|
+
];
|
|
30
|
+
const CODE_SMALL = [
|
|
31
|
+
'█▀▀ █▀█ █▀▄ █▀▀',
|
|
32
|
+
'█▄▄ █▄█ █▄▀ ██▄',
|
|
33
|
+
];
|
|
34
|
+
export const Banner = () => {
|
|
35
|
+
const { stdout } = useStdout();
|
|
36
|
+
const width = stdout?.columns ?? 80;
|
|
37
|
+
const wide = width >= 60;
|
|
38
|
+
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
39
|
+
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
40
|
+
const [t, setT] = useState(0);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const interval = setInterval(() => setT((v) => (v + 1 / 160) % 1), 50);
|
|
43
|
+
return () => clearInterval(interval);
|
|
44
|
+
}, []);
|
|
45
|
+
const brand = hueAt(t);
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MakerMark, { color: brand }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color: hueAt((t + i * 0.06) % 1), bold: true, children: line }, 'b' + i))), code.map((line, i) => (_jsx(Text, { color: hueAt((t + i * 0.06 + 0.35) % 1), bold: true, children: line }, 'c' + i))), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: "a coding agent \u00B7 v0.1.0" }), _jsx(Text, { children: " " })] }));
|
|
47
|
+
};
|
package/dist/ui/Chat.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { Agent } from '../core/agent.js';
|
|
5
|
+
import { InputBar } from './InputBar.js';
|
|
6
|
+
import { ModelPicker } from './ModelPicker.js';
|
|
7
|
+
import { theme } from './theme.js';
|
|
8
|
+
export const Chat = () => {
|
|
9
|
+
const { stdout } = useStdout();
|
|
10
|
+
const width = stdout?.columns ?? 80;
|
|
11
|
+
const [history, setHistory] = useState([]);
|
|
12
|
+
const [input, setInput] = useState('');
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
15
|
+
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
16
|
+
const [agent, setAgent] = useState(() => new Agent());
|
|
17
|
+
useInput((inputChar, key) => {
|
|
18
|
+
if (key.ctrl && inputChar === 'p') {
|
|
19
|
+
setModelPickerOpen(true);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const handleSubmit = async (text) => {
|
|
23
|
+
if (!text.trim() || loading)
|
|
24
|
+
return;
|
|
25
|
+
const trimmed = text.trim();
|
|
26
|
+
if (trimmed === '/model' || trimmed === '/models') {
|
|
27
|
+
setInput('');
|
|
28
|
+
setModelPickerOpen(true);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setInput('');
|
|
32
|
+
setHistory((h) => [...h, { kind: 'user', text: trimmed }]);
|
|
33
|
+
setLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
for await (const chunk of agent.run(trimmed)) {
|
|
36
|
+
const isTool = chunk.startsWith('\n[Calling') || chunk.startsWith('[Result]');
|
|
37
|
+
setHistory((h) => [...h, { kind: isTool ? 'tool' : 'ai', text: chunk }]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
setHistory((h) => [...h, { kind: 'error', text: e?.message || String(e) }]);
|
|
42
|
+
}
|
|
43
|
+
setLoading(false);
|
|
44
|
+
};
|
|
45
|
+
const handleModelSelect = (id) => {
|
|
46
|
+
process.env.MODEL = id;
|
|
47
|
+
setCurrentModel(id);
|
|
48
|
+
setModelPickerOpen(false);
|
|
49
|
+
setAgent(new Agent());
|
|
50
|
+
setHistory((h) => [...h, { kind: 'tool', text: `model → ${id}` }]);
|
|
51
|
+
};
|
|
52
|
+
if (modelPickerOpen) {
|
|
53
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(ModelPicker, { current: currentModel, onSelect: handleModelSelect, onCancel: () => setModelPickerOpen(false) }) }));
|
|
54
|
+
}
|
|
55
|
+
// Truncate model name so footer fits on one line
|
|
56
|
+
const maxModelLen = Math.max(12, width - 24);
|
|
57
|
+
const shortModel = currentModel.length > maxModelLen
|
|
58
|
+
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
59
|
+
: currentModel || 'unset';
|
|
60
|
+
return (_jsxs(Box, { flexDirection: "column", children: [history.map((item, i) => {
|
|
61
|
+
if (item.kind === 'user') {
|
|
62
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }, i));
|
|
63
|
+
}
|
|
64
|
+
if (item.kind === 'error') {
|
|
65
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.error, children: '❯ ' }), _jsx(Text, { color: theme.error, children: item.text })] }, i));
|
|
66
|
+
}
|
|
67
|
+
if (item.kind === 'tool') {
|
|
68
|
+
return _jsx(Text, { color: theme.muted, children: item.text.trim() }, i);
|
|
69
|
+
}
|
|
70
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: item.text }) }, i));
|
|
71
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading }) }), _jsxs(Text, { color: theme.muted, children: [' ', _jsx(Text, { color: theme.accent, children: "/model" }), ' · ', _jsx(Text, { color: theme.text, children: shortModel })] })] }));
|
|
72
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Placeholder } from './Placeholder.js';
|
|
6
|
+
import { RocketBar } from './RocketBar.js';
|
|
7
|
+
import { theme } from './theme.js';
|
|
8
|
+
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
9
|
+
export const InputBar = ({ value, onChange, onSubmit, loading }) => {
|
|
10
|
+
const { stdout } = useStdout();
|
|
11
|
+
const width = Math.max(24, stdout?.columns ?? 50);
|
|
12
|
+
const [dotI, setDotI] = useState(0);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!loading)
|
|
15
|
+
return;
|
|
16
|
+
const int = setInterval(() => setDotI((d) => (d + 1) % DOTS.length), 180);
|
|
17
|
+
return () => clearInterval(int);
|
|
18
|
+
}, [loading]);
|
|
19
|
+
const hasText = value.length > 0;
|
|
20
|
+
const label = loading ? 'thinking' : 'blyue';
|
|
21
|
+
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
22
|
+
const botLen = Math.max(0, width - 2 - status.length);
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(RocketBar, { label: label, loading: loading, width: width }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), hasText ? (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })) : (_jsxs(_Fragment, { children: [_jsx(Placeholder, {}), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(botLen) }), loading && _jsx(Text, { color: theme.accent, children: status }), _jsx(Text, { color: theme.muted, children: "\u256F" })] })] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from './theme.js';
|
|
4
|
+
export const KeyInput = ({ value, onChange, onSubmit, placeholder = '', masked = false }) => {
|
|
5
|
+
useInput((input, key) => {
|
|
6
|
+
if (key.return)
|
|
7
|
+
return onSubmit(value);
|
|
8
|
+
if (key.backspace || key.delete)
|
|
9
|
+
return onChange(value.slice(0, -1));
|
|
10
|
+
if (key.ctrl || key.meta || key.escape)
|
|
11
|
+
return;
|
|
12
|
+
if (input) {
|
|
13
|
+
const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
14
|
+
if (clean)
|
|
15
|
+
onChange(value + clean);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const shown = value ? (masked ? '•'.repeat(value.length) : value) : '';
|
|
19
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), value ? (_jsx(Text, { color: theme.text, children: shown })) : (_jsx(Text, { color: theme.muted, children: placeholder })), _jsx(Text, { color: theme.accent, children: "\u2588" })] }));
|
|
20
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
export const MakerMark = ({ color }) => (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: "\u25C6 " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" })] }));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { fetchFreeModels } from '../core/freeModels.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const [models, setModels] = useState([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [idx, setIdx] = useState(0);
|
|
12
|
+
const [filter, setFilter] = useState('');
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetchFreeModels(process.env.OPENAI_API_KEY).then((m) => {
|
|
15
|
+
setModels(m);
|
|
16
|
+
const found = m.findIndex((x) => x.id === current);
|
|
17
|
+
setIdx(found >= 0 ? found : 0);
|
|
18
|
+
setLoading(false);
|
|
19
|
+
});
|
|
20
|
+
}, [current]);
|
|
21
|
+
const filtered = filter
|
|
22
|
+
? models.filter((m) => m.id.toLowerCase().includes(filter.toLowerCase()))
|
|
23
|
+
: models;
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (key.escape)
|
|
26
|
+
return onCancel();
|
|
27
|
+
if (key.upArrow)
|
|
28
|
+
setIdx((i) => (i - 1 + filtered.length) % filtered.length);
|
|
29
|
+
if (key.downArrow)
|
|
30
|
+
setIdx((i) => (i + 1) % filtered.length);
|
|
31
|
+
if (key.return && filtered[idx])
|
|
32
|
+
onSelect(filtered[idx].id);
|
|
33
|
+
if (key.backspace || key.delete)
|
|
34
|
+
setFilter((f) => f.slice(0, -1));
|
|
35
|
+
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) {
|
|
36
|
+
const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
37
|
+
if (clean)
|
|
38
|
+
setFilter((f) => f + clean);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const inner = width - 4;
|
|
42
|
+
const maxVisible = 12;
|
|
43
|
+
const start = Math.max(0, Math.min(idx - 6, filtered.length - maxVisible));
|
|
44
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
45
|
+
const topLabel = filter ? `filter: ${filter}` : 'free models';
|
|
46
|
+
const topPad = Math.max(1, inner - topLabel.length - 4);
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: topLabel }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(topPad) + '╮' })] }), loading ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: "loading catalog\u2026" })] })) : visible.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.error, children: "no matches" })] })) : (visible.map((m, i) => {
|
|
48
|
+
const realIdx = start + i;
|
|
49
|
+
const active = realIdx === idx;
|
|
50
|
+
const isCurrent = m.id === current;
|
|
51
|
+
const ctx = m.context >= 1000 ? `${Math.round(m.context / 1000)}k` : `${m.context}`;
|
|
52
|
+
const label = m.id.length > inner - 10 ? m.id.slice(0, inner - 13) + '…' : m.id;
|
|
53
|
+
const right = `${ctx}`;
|
|
54
|
+
const pad = Math.max(1, inner - label.length - right.length - 4);
|
|
55
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: label }), _jsx(Text, { color: theme.muted, children: ' '.repeat(pad) }), _jsx(Text, { color: theme.muted, children: right }), isCurrent && _jsx(Text, { color: theme.accent, children: " \u2713" })] }, m.id));
|
|
56
|
+
})), _jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }) }), _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 type to filter \u00B7 \u21B5 select \u00B7 esc cancel" })] }));
|
|
57
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
const HINTS = [
|
|
6
|
+
'what do you want to build?',
|
|
7
|
+
'list files',
|
|
8
|
+
'read package.json',
|
|
9
|
+
'run tests',
|
|
10
|
+
'explain src/index.tsx',
|
|
11
|
+
'add a .gitignore',
|
|
12
|
+
'fix the bug in app.ts',
|
|
13
|
+
];
|
|
14
|
+
export const Placeholder = () => {
|
|
15
|
+
const [i, setI] = useState(0);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const t = setInterval(() => setI((v) => (v + 1) % HINTS.length), 3500);
|
|
18
|
+
return () => clearInterval(t);
|
|
19
|
+
}, []);
|
|
20
|
+
return _jsx(Text, { color: theme.muted, dimColor: true, children: HINTS[i] });
|
|
21
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
import { hueAt } from './animColor.js';
|
|
6
|
+
export const RocketBar = ({ label, loading, width }) => {
|
|
7
|
+
const [t, setT] = useState(0);
|
|
8
|
+
const [pos, setPos] = useState(0);
|
|
9
|
+
// Animated hue (matches banner)
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const int = setInterval(() => setT((v) => (v + 0.00625) % 1), 50);
|
|
12
|
+
return () => clearInterval(int);
|
|
13
|
+
}, []);
|
|
14
|
+
const fixed = 5 + label.length; // '╭─ ' + label + ' ' + '╮'
|
|
15
|
+
const midLen = Math.max(0, width - fixed);
|
|
16
|
+
// Rocket position animation
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!loading || midLen < 6)
|
|
19
|
+
return;
|
|
20
|
+
const track = midLen - 2;
|
|
21
|
+
const int = setInterval(() => setPos((p) => (p + 1) % track), 65);
|
|
22
|
+
return () => clearInterval(int);
|
|
23
|
+
}, [loading, midLen]);
|
|
24
|
+
const hue = hueAt(t);
|
|
25
|
+
if (!loading || midLen < 6) {
|
|
26
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: loading ? hue : theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(midLen) + '╮' })] }));
|
|
27
|
+
}
|
|
28
|
+
const p = Math.min(pos, Math.max(0, midLen - 3));
|
|
29
|
+
const before = '─'.repeat(p);
|
|
30
|
+
const after = '─'.repeat(Math.max(0, midLen - p - 3));
|
|
31
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: hue, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + before }), _jsx(Text, { color: theme.dim, dimColor: true, children: "\u00B7" }), _jsx(Text, { color: theme.muted, children: "\u00B7" }), _jsx(Text, { color: hue, bold: true, children: "\u27A4" }), _jsx(Text, { color: theme.muted, children: after + '╮' })] }));
|
|
32
|
+
};
|
package/dist/ui/Setup.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { saveConfig, applyConfig } from '../core/config.js';
|
|
6
|
+
import { theme } from './theme.js';
|
|
7
|
+
import { KeyInput } from './KeyInput.js';
|
|
8
|
+
const PROVIDERS = [
|
|
9
|
+
{
|
|
10
|
+
label: 'OpenAI',
|
|
11
|
+
models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4.1-mini', 'o4-mini'],
|
|
12
|
+
keyHint: 'sk-...',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: 'OpenRouter',
|
|
16
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
17
|
+
models: [
|
|
18
|
+
'meta-llama/llama-3.3-70b-instruct:free',
|
|
19
|
+
'google/gemini-2.0-flash-exp:free',
|
|
20
|
+
'deepseek/deepseek-chat-v3-0324:free',
|
|
21
|
+
'qwen/qwen-2.5-coder-32b-instruct:free',
|
|
22
|
+
],
|
|
23
|
+
keyHint: 'sk-or-...',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'Groq',
|
|
27
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
28
|
+
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
|
|
29
|
+
keyHint: 'gsk_...',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Ollama (local)',
|
|
33
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
34
|
+
models: ['llama3.2', 'qwen2.5-coder'],
|
|
35
|
+
keyHint: 'ollama',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'Custom endpoint',
|
|
39
|
+
baseUrl: '',
|
|
40
|
+
models: [''],
|
|
41
|
+
keyHint: 'sk-...',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
export const Setup = ({ onDone }) => {
|
|
45
|
+
const [step, setStep] = useState('provider');
|
|
46
|
+
const [pIdx, setPIdx] = useState(0);
|
|
47
|
+
const [mIdx, setMIdx] = useState(0);
|
|
48
|
+
const [apiKey, setApiKey] = useState('');
|
|
49
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
50
|
+
const [customModel, setCustomModel] = useState('');
|
|
51
|
+
const [error, setError] = useState(null);
|
|
52
|
+
const provider = PROVIDERS[pIdx];
|
|
53
|
+
const isCustom = provider.label === 'Custom endpoint';
|
|
54
|
+
useInput((_, key) => {
|
|
55
|
+
if (step === 'provider') {
|
|
56
|
+
if (key.upArrow)
|
|
57
|
+
setPIdx((i) => (i - 1 + PROVIDERS.length) % PROVIDERS.length);
|
|
58
|
+
if (key.downArrow)
|
|
59
|
+
setPIdx((i) => (i + 1) % PROVIDERS.length);
|
|
60
|
+
if (key.return) {
|
|
61
|
+
setMIdx(0);
|
|
62
|
+
setStep(isCustom ? 'baseurl' : 'apikey');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (step === 'model' && !isCustom) {
|
|
66
|
+
if (key.upArrow)
|
|
67
|
+
setMIdx((i) => (i - 1 + provider.models.length) % provider.models.length);
|
|
68
|
+
if (key.downArrow)
|
|
69
|
+
setMIdx((i) => (i + 1) % provider.models.length);
|
|
70
|
+
if (key.return)
|
|
71
|
+
finish(provider.models[mIdx]);
|
|
72
|
+
}
|
|
73
|
+
}, { isActive: step === 'provider' || (step === 'model' && !isCustom) });
|
|
74
|
+
const finish = (model) => {
|
|
75
|
+
if (!model)
|
|
76
|
+
return setError('Model name required');
|
|
77
|
+
const cfg = {
|
|
78
|
+
apiKey: apiKey.trim(),
|
|
79
|
+
baseUrl: baseUrl.trim() || provider.baseUrl || undefined,
|
|
80
|
+
model,
|
|
81
|
+
};
|
|
82
|
+
setStep('saving');
|
|
83
|
+
try {
|
|
84
|
+
saveConfig(cfg);
|
|
85
|
+
applyConfig(cfg);
|
|
86
|
+
setStep('done');
|
|
87
|
+
setTimeout(() => onDone(cfg), 400);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
setError('Save failed: ' + e.message);
|
|
91
|
+
setStep('provider');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// Header: single line hint
|
|
95
|
+
const Hint = ({ children }) => (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.muted, children: children }) }));
|
|
96
|
+
if (step === 'provider') {
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Select a provider \u2191\u2193 Enter" }), PROVIDERS.map((p, i) => {
|
|
98
|
+
const active = i === pIdx;
|
|
99
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, bold: true, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: p.label })] }, p.label));
|
|
100
|
+
}), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.error, children: error }) }))] }));
|
|
101
|
+
}
|
|
102
|
+
if (step === 'baseurl') {
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Base URL (e.g. http://localhost:8080/v1)" }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(TextInput, { value: baseUrl, onChange: setBaseUrl, placeholder: "http://...", onSubmit: (v) => { setBaseUrl(v); setStep('apikey'); } })] })] }));
|
|
104
|
+
}
|
|
105
|
+
if (step === 'apikey') {
|
|
106
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Hint, { children: ["API key \u00B7 ", provider.keyHint, " \u00B7 saved to ~/.blyue/.env"] }), _jsx(KeyInput, { value: apiKey, onChange: setApiKey, placeholder: provider.keyHint, masked: true, onSubmit: (v) => { setApiKey(v); setMIdx(0); setStep('model'); } })] }));
|
|
107
|
+
}
|
|
108
|
+
if (step === 'model') {
|
|
109
|
+
if (isCustom) {
|
|
110
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Model name (e.g. gpt-4o-mini)" }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(TextInput, { value: customModel, onChange: setCustomModel, placeholder: "gpt-4o-mini", onSubmit: (v) => finish(v) })] })] }));
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Select a model \u2191\u2193 Enter" }), provider.models.map((m, i) => {
|
|
113
|
+
const active = i === mIdx;
|
|
114
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, bold: true, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: m })] }, m));
|
|
115
|
+
})] }));
|
|
116
|
+
}
|
|
117
|
+
if (step === 'saving') {
|
|
118
|
+
return _jsx(Text, { color: theme.muted, children: "Saving\u2026" });
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.success, children: "\u2713 Configured" }), _jsx(Text, { color: theme.muted, children: "Starting\u2026" })] }));
|
|
121
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
6
|
+
export const Spinner = ({ label = 'Thinking' }) => {
|
|
7
|
+
const [i, setI] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), 90);
|
|
10
|
+
return () => clearInterval(t);
|
|
11
|
+
}, []);
|
|
12
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsxs(Text, { color: theme.muted, children: [FRAMES[i], " ", label, "\u2026"] })] }));
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// HSL → hex, no dependencies
|
|
2
|
+
function hslToHex(h, s, l) {
|
|
3
|
+
h = ((h % 360) + 360) % 360;
|
|
4
|
+
s /= 100;
|
|
5
|
+
l /= 100;
|
|
6
|
+
const k = (n) => (n + h / 30) % 12;
|
|
7
|
+
const a = s * Math.min(l, 1 - l);
|
|
8
|
+
const f = (n) => {
|
|
9
|
+
const c = l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
10
|
+
return Math.round(255 * c).toString(16).padStart(2, '0');
|
|
11
|
+
};
|
|
12
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
13
|
+
}
|
|
14
|
+
/** Returns a hex color at position t (0..1) along a soft palette loop. */
|
|
15
|
+
export function hueAt(t, lightness = 65, saturation = 70) {
|
|
16
|
+
// Weighted loop: cyan → blue → purple → pink → cyan
|
|
17
|
+
const hue = 180 + Math.sin(t * Math.PI * 2) * 120;
|
|
18
|
+
return hslToHex(hue, saturation, lightness);
|
|
19
|
+
}
|
package/dist/ui/theme.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blyuecode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BLYUE Code — a coding agent by Lumen",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"blyue": "./dist/index.js",
|
|
9
|
+
"blyuecode": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"dev": "tsc && node dist/index.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ink": "^5.0.1",
|
|
27
|
+
"ink-text-input": "^6.0.0",
|
|
28
|
+
"openai": "^4.67.3",
|
|
29
|
+
"react": "^18.3.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.7.5",
|
|
33
|
+
"@types/react": "^18.3.11",
|
|
34
|
+
"typescript": "^5.6.3"
|
|
35
|
+
}
|
|
36
|
+
}
|