blyuecode 0.3.6 → 0.3.8
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/dist/core/agent.js +0 -1
- package/dist/core/diff.js +0 -3
- package/dist/core/freeModels.js +1 -4
- package/dist/core/mcp.js +0 -45
- package/dist/core/mcpRegistry.js +0 -20
- package/dist/core/modelUsage.js +0 -5
- package/dist/core/models.js +0 -19
- package/dist/core/permission.js +0 -2
- package/dist/core/provider.js +0 -3
- package/dist/core/redact.js +0 -11
- package/dist/core/session.js +3 -15
- package/dist/ui/Banner.js +0 -1
- package/dist/ui/Chat.js +0 -9
- package/dist/ui/MakerMark.js +0 -2
- package/dist/ui/Mascot.js +0 -9
- package/dist/ui/MascotPrompt.js +1 -3
- package/dist/ui/McpPanel.js +0 -1
- package/dist/ui/ModelPicker.js +1 -2
- package/dist/ui/RocketBar.js +1 -1
- package/dist/ui/Settings.js +153 -12
- package/dist/ui/Setup.js +0 -1
- package/dist/ui/SleepZs.js +0 -2
- package/dist/ui/Spinner.js +0 -1
- package/dist/ui/activity.js +0 -4
- package/dist/ui/animColor.js +0 -3
- package/dist/ui/pastel.js +1 -10
- package/dist/ui/slowTick.js +1 -4
- package/dist/ui/useIdle.js +0 -5
- package/dist/ui/useSlowColor.js +0 -4
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
package/dist/core/diff.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// Simple LCS-based line diff. Good enough for previews.
|
|
2
1
|
export function lineDiff(oldText, newText) {
|
|
3
2
|
const a = oldText.length ? oldText.split('\n') : [];
|
|
4
3
|
const b = newText.length ? newText.split('\n') : [];
|
|
5
4
|
const m = a.length;
|
|
6
5
|
const n = b.length;
|
|
7
|
-
// LCS table
|
|
8
6
|
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
9
7
|
for (let i = m - 1; i >= 0; i--) {
|
|
10
8
|
for (let j = n - 1; j >= 0; j--) {
|
|
@@ -38,7 +36,6 @@ export function lineDiff(oldText, newText) {
|
|
|
38
36
|
out.push({ type: 'add', text: b[j++] });
|
|
39
37
|
return out;
|
|
40
38
|
}
|
|
41
|
-
// Compact display: only show changed regions with context
|
|
42
39
|
export function compactDiff(diff, context = 2) {
|
|
43
40
|
const keep = new Set();
|
|
44
41
|
diff.forEach((d, idx) => {
|
package/dist/core/freeModels.js
CHANGED
|
@@ -3,8 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
const URL = 'https://openrouter.ai/api/v1/models';
|
|
5
5
|
const CACHE = path.join(os.homedir(), '.blyue', 'models-cache.json');
|
|
6
|
-
const TTL_MS = 5 * 60 * 1000;
|
|
7
|
-
// Fallback if API is down or first-run offline
|
|
6
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
8
7
|
const FALLBACK = [
|
|
9
8
|
{ id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Llama 3.3 70B', context: 131072 },
|
|
10
9
|
{ id: 'google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash', context: 1048576 },
|
|
@@ -12,7 +11,6 @@ const FALLBACK = [
|
|
|
12
11
|
{ id: 'qwen/qwen-2.5-coder-32b-instruct:free', name: 'Qwen 2.5 Coder 32B', context: 32768 },
|
|
13
12
|
];
|
|
14
13
|
export async function fetchFreeModels(apiKey) {
|
|
15
|
-
// Try cache first
|
|
16
14
|
try {
|
|
17
15
|
const raw = fs.readFileSync(CACHE, 'utf8');
|
|
18
16
|
const cached = JSON.parse(raw);
|
|
@@ -44,7 +42,6 @@ export async function fetchFreeModels(apiKey) {
|
|
|
44
42
|
context: m.context_length || 0,
|
|
45
43
|
}))
|
|
46
44
|
.sort((a, b) => b.context - a.context);
|
|
47
|
-
// Save cache
|
|
48
45
|
fs.mkdirSync(path.dirname(CACHE), { recursive: true });
|
|
49
46
|
fs.writeFileSync(CACHE, JSON.stringify({ ts: Date.now(), models }));
|
|
50
47
|
return models.length > 0 ? models : FALLBACK;
|
package/dist/core/mcp.js
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
// Minimal MCP client — stdio + HTTP/SSE transports, JSON-RPC 2.0.
|
|
2
|
-
// Spec: https://spec.modelcontextprotocol.io/
|
|
3
|
-
//
|
|
4
|
-
// No external dependencies. Uses Node 20's global fetch for SSE.
|
|
5
|
-
//
|
|
6
|
-
// Usage:
|
|
7
|
-
// const client = new McpClient(name, config);
|
|
8
|
-
// await client.connect();
|
|
9
|
-
// const tools = await client.listTools();
|
|
10
|
-
// const result = await client.callTool('read_file', { path: '...' });
|
|
11
|
-
// await client.close();
|
|
12
1
|
import { spawn } from 'child_process';
|
|
13
2
|
import { EventEmitter } from 'events';
|
|
14
|
-
// ──────────────────────────────────────────────────────────────
|
|
15
|
-
// Stdio transport — newline-delimited JSON-RPC over child stdin/stdout
|
|
16
|
-
// ──────────────────────────────────────────────────────────────
|
|
17
3
|
class StdioTransport extends EventEmitter {
|
|
18
4
|
proc;
|
|
19
5
|
buf = '';
|
|
@@ -39,16 +25,10 @@ class StdioTransport extends EventEmitter {
|
|
|
39
25
|
this.emit('message', msg);
|
|
40
26
|
}
|
|
41
27
|
catch {
|
|
42
|
-
// Not JSON — ignore (some servers log to stdout, which is rude
|
|
43
|
-
// but happens; stderr is the correct channel for logs).
|
|
44
28
|
}
|
|
45
29
|
}
|
|
46
30
|
});
|
|
47
31
|
this.proc.stderr?.on('data', (chunk) => {
|
|
48
|
-
// Servers log readiness banners and diagnostics to stderr — that's
|
|
49
|
-
// the MCP spec's designated log channel. Never treat stderr as an
|
|
50
|
-
// error: it would kill the in-flight initialize request. Keep the
|
|
51
|
-
// last few KB around for debugging, but do nothing else with it.
|
|
52
32
|
this.lastStderr = (this.lastStderr + chunk.toString('utf8')).slice(-4096);
|
|
53
33
|
});
|
|
54
34
|
this.proc.on('error', (err) => this.emit('error', err));
|
|
@@ -64,26 +44,15 @@ class StdioTransport extends EventEmitter {
|
|
|
64
44
|
try {
|
|
65
45
|
this.proc.stdin?.end();
|
|
66
46
|
this.proc.kill('SIGTERM');
|
|
67
|
-
// SIGKILL fallback if it doesn't die within 2s.
|
|
68
47
|
setTimeout(() => {
|
|
69
48
|
if (this.proc.exitCode === null)
|
|
70
49
|
this.proc.kill('SIGKILL');
|
|
71
50
|
}, 2000).unref?.();
|
|
72
51
|
}
|
|
73
52
|
catch {
|
|
74
|
-
// already dead
|
|
75
53
|
}
|
|
76
54
|
}
|
|
77
55
|
}
|
|
78
|
-
// ──────────────────────────────────────────────────────────────
|
|
79
|
-
// SSE transport — POST for sends, GET text/event-stream for receives
|
|
80
|
-
//
|
|
81
|
-
// MCP's HTTP+SSE transport (2024-11-05 spec):
|
|
82
|
-
// 1. GET url -> text/event-stream, server sends an `endpoint` event
|
|
83
|
-
// with the POST URL to use for requests.
|
|
84
|
-
// 2. POST JSON-RPC requests to that endpoint; responses come back on
|
|
85
|
-
// the SSE stream as `message` events.
|
|
86
|
-
// ──────────────────────────────────────────────────────────────
|
|
87
56
|
class SseTransport extends EventEmitter {
|
|
88
57
|
cfg;
|
|
89
58
|
postUrl = null;
|
|
@@ -141,8 +110,6 @@ class SseTransport extends EventEmitter {
|
|
|
141
110
|
if (!data)
|
|
142
111
|
return;
|
|
143
112
|
if (event === 'endpoint') {
|
|
144
|
-
// Server tells us where to POST. Relative URLs are resolved against
|
|
145
|
-
// the SSE endpoint's origin.
|
|
146
113
|
try {
|
|
147
114
|
this.postUrl = new URL(data, this.cfg.url).toString();
|
|
148
115
|
}
|
|
@@ -157,7 +124,6 @@ class SseTransport extends EventEmitter {
|
|
|
157
124
|
this.emit('message', msg);
|
|
158
125
|
}
|
|
159
126
|
catch {
|
|
160
|
-
// ignore malformed
|
|
161
127
|
}
|
|
162
128
|
}
|
|
163
129
|
}
|
|
@@ -179,9 +145,6 @@ class SseTransport extends EventEmitter {
|
|
|
179
145
|
this.abort?.abort();
|
|
180
146
|
}
|
|
181
147
|
}
|
|
182
|
-
// ──────────────────────────────────────────────────────────────
|
|
183
|
-
// Client
|
|
184
|
-
// ──────────────────────────────────────────────────────────────
|
|
185
148
|
export class McpClient {
|
|
186
149
|
name;
|
|
187
150
|
cfg;
|
|
@@ -210,7 +173,6 @@ export class McpClient {
|
|
|
210
173
|
: new SseTransport(this.cfg);
|
|
211
174
|
this.transport.on('message', (msg) => this.handleMessage(msg));
|
|
212
175
|
this.transport.on('error', (err) => {
|
|
213
|
-
// Route transport errors to any in-flight request so they don't hang.
|
|
214
176
|
for (const [, p] of this.pending) {
|
|
215
177
|
clearTimeout(p.timer);
|
|
216
178
|
p.reject(err);
|
|
@@ -220,14 +182,12 @@ export class McpClient {
|
|
|
220
182
|
this.transport.on('close', () => {
|
|
221
183
|
this.connected = false;
|
|
222
184
|
});
|
|
223
|
-
// Handshake
|
|
224
185
|
const result = (await this.request('initialize', {
|
|
225
186
|
protocolVersion: '2024-11-05',
|
|
226
187
|
capabilities: { tools: {} },
|
|
227
188
|
clientInfo: { name: 'blyue', version: '0.2.0' },
|
|
228
189
|
}));
|
|
229
190
|
this.serverInfo = result?.serverInfo ?? null;
|
|
230
|
-
// Spec requires this notification after initialize.
|
|
231
191
|
this.notify('notifications/initialized', {});
|
|
232
192
|
this.connected = true;
|
|
233
193
|
}
|
|
@@ -256,7 +216,6 @@ export class McpClient {
|
|
|
256
216
|
this.connected = false;
|
|
257
217
|
this.cachedTools = null;
|
|
258
218
|
}
|
|
259
|
-
// ────────────────────────────────────────────
|
|
260
219
|
handleMessage(msg) {
|
|
261
220
|
if ('id' in msg && msg.id !== undefined) {
|
|
262
221
|
const p = this.pending.get(msg.id);
|
|
@@ -272,7 +231,6 @@ export class McpClient {
|
|
|
272
231
|
}
|
|
273
232
|
return;
|
|
274
233
|
}
|
|
275
|
-
// Notifications from server (e.g. tools/list_changed) — currently ignored.
|
|
276
234
|
}
|
|
277
235
|
request(method, params) {
|
|
278
236
|
if (!this.transport)
|
|
@@ -301,9 +259,6 @@ export class McpClient {
|
|
|
301
259
|
this.transport.send({ jsonrpc: '2.0', method, params });
|
|
302
260
|
}
|
|
303
261
|
}
|
|
304
|
-
// ──────────────────────────────────────────────────────────────
|
|
305
|
-
// Config load / save
|
|
306
|
-
// ──────────────────────────────────────────────────────────────
|
|
307
262
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
308
263
|
import { homedir } from 'os';
|
|
309
264
|
import { join, dirname } from 'path';
|
package/dist/core/mcpRegistry.js
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
// MCP registry — holds all connected MCP clients as a module-level
|
|
2
|
-
// singleton so the tools layer can merge their tool defs into the LLM's
|
|
3
|
-
// tool list without threading state through the agent.
|
|
4
|
-
//
|
|
5
|
-
// Lifecycle:
|
|
6
|
-
// 1. index.tsx calls initMcpRegistry() once after config load
|
|
7
|
-
// 2. tools.ts calls getMcpToolDefsSync() every agent-loop iteration
|
|
8
|
-
// 3. agent calls executeTool() → falls through to executeMcpTool()
|
|
9
|
-
// for names prefixed mcp__<server>__<tool>
|
|
10
1
|
import { McpClient, loadMcpConfig, } from './mcp.js';
|
|
11
2
|
import { permissions } from './permission.js';
|
|
12
3
|
const servers = new Map();
|
|
13
4
|
let cachedDefs = [];
|
|
14
5
|
let initPromise = null;
|
|
15
6
|
let initialized = false;
|
|
16
|
-
/**
|
|
17
|
-
* Called once on app startup, after config is loaded.
|
|
18
|
-
* Never throws — a bad MCP server is logged and skipped so the TUI
|
|
19
|
-
* still boots. (Failing closed here would make a typo in mcp.json
|
|
20
|
-
* brick the whole app.)
|
|
21
|
-
*/
|
|
22
7
|
export async function initMcpRegistry() {
|
|
23
8
|
if (initPromise)
|
|
24
9
|
return initPromise;
|
|
@@ -79,7 +64,6 @@ function rebuildDefs() {
|
|
|
79
64
|
}
|
|
80
65
|
cachedDefs = defs;
|
|
81
66
|
}
|
|
82
|
-
/** Synchronous — safe to call every iteration of the agent loop. */
|
|
83
67
|
export function getMcpToolDefsSync() {
|
|
84
68
|
return cachedDefs;
|
|
85
69
|
}
|
|
@@ -95,10 +79,6 @@ export function parseMcpToolName(fullName) {
|
|
|
95
79
|
return null;
|
|
96
80
|
return { server: rest.slice(0, idx), tool: rest.slice(idx + 2) };
|
|
97
81
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Executes an MCP tool call. Routes through the permission broker
|
|
100
|
-
* unless the server is marked autoAllow:true in mcp.json.
|
|
101
|
-
*/
|
|
102
82
|
export async function executeMcpTool(fullName, args) {
|
|
103
83
|
const parsed = parseMcpToolName(fullName);
|
|
104
84
|
if (!parsed)
|
package/dist/core/modelUsage.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// Tracks which models the user has picked, so the picker can surface a
|
|
2
|
-
// RECENT section. Stored at ~/.blyue/model-usage.json. Bounded — one
|
|
3
|
-
// entry per unique model ever picked.
|
|
4
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
5
2
|
import { homedir } from 'os';
|
|
6
3
|
import { join, dirname } from 'path';
|
|
@@ -29,7 +26,6 @@ function save(m) {
|
|
|
29
26
|
writeFileSync(p, JSON.stringify(m, null, 2), 'utf8');
|
|
30
27
|
cache = m;
|
|
31
28
|
}
|
|
32
|
-
/** Call when the user picks a model (Ctrl+L or /model). */
|
|
33
29
|
export function recordModelUse(id) {
|
|
34
30
|
if (!id)
|
|
35
31
|
return;
|
|
@@ -38,7 +34,6 @@ export function recordModelUse(id) {
|
|
|
38
34
|
m[id] = { count: prev.count + 1, lastUsed: Date.now() };
|
|
39
35
|
save(m);
|
|
40
36
|
}
|
|
41
|
-
/** Top N most-recently-used model IDs, newest first. */
|
|
42
37
|
export function getRecentModels(limit = 5) {
|
|
43
38
|
const m = load();
|
|
44
39
|
return Object.entries(m)
|
package/dist/core/models.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
// Universal model fetcher — works with any OpenAI-compatible /v1/models
|
|
2
|
-
// endpoint: OpenAI, OpenRouter, Groq, xAI (Grok), Ollama, LM Studio,
|
|
3
|
-
// llama.cpp, vLLM, together, deepseek, etc.
|
|
4
1
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
5
2
|
let cache = null;
|
|
6
3
|
function currentBaseUrl() {
|
|
7
4
|
const raw = process.env.OPENAI_BASE_URL ||
|
|
8
5
|
process.env.OPENAI_API_BASE ||
|
|
9
6
|
'https://api.openai.com/v1';
|
|
10
|
-
// Strip trailing slash for consistent concat.
|
|
11
7
|
return raw.replace(/\/+$/, '');
|
|
12
8
|
}
|
|
13
9
|
function currentApiKey() {
|
|
@@ -29,7 +25,6 @@ function providerLabel(baseUrl) {
|
|
|
29
25
|
return 'Together';
|
|
30
26
|
if (u.includes('localhost') || u.includes('127.0.0.1'))
|
|
31
27
|
return 'local';
|
|
32
|
-
// Fall back to the hostname.
|
|
33
28
|
try {
|
|
34
29
|
return new URL(baseUrl).host;
|
|
35
30
|
}
|
|
@@ -40,10 +35,6 @@ function providerLabel(baseUrl) {
|
|
|
40
35
|
export function currentProviderLabel() {
|
|
41
36
|
return providerLabel(currentBaseUrl());
|
|
42
37
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Non-chat models we should hide from the picker. Conservative — better
|
|
45
|
-
* to show a couple of extras than to hide something the user wants.
|
|
46
|
-
*/
|
|
47
38
|
function isChatModel(id) {
|
|
48
39
|
const l = id.toLowerCase();
|
|
49
40
|
const block = [
|
|
@@ -74,11 +65,6 @@ function parseNumber(v) {
|
|
|
74
65
|
const n = typeof v === 'string' ? parseFloat(v) : v;
|
|
75
66
|
return Number.isFinite(n) ? n : undefined;
|
|
76
67
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Fetches the model list from the current provider. Cached for 5 minutes
|
|
79
|
-
* per base URL, so reopening /model is instant but "new models" surface
|
|
80
|
-
* within one cache window.
|
|
81
|
-
*/
|
|
82
68
|
export async function fetchModels(opts) {
|
|
83
69
|
const baseUrl = currentBaseUrl();
|
|
84
70
|
const url = baseUrl + '/models';
|
|
@@ -129,8 +115,6 @@ export async function fetchModels(opts) {
|
|
|
129
115
|
})
|
|
130
116
|
.filter((m) => m !== null)
|
|
131
117
|
.filter((m) => isChatModel(m.id));
|
|
132
|
-
// Sort: free first, then alphabetical. (OpenRouter returns them in
|
|
133
|
-
// arbitrary order; this makes the useful ones surface at the top.)
|
|
134
118
|
models.sort((a, b) => {
|
|
135
119
|
if (!!a.isFree !== !!b.isFree)
|
|
136
120
|
return a.isFree ? -1 : 1;
|
|
@@ -142,9 +126,6 @@ export async function fetchModels(opts) {
|
|
|
142
126
|
export function clearModelCache() {
|
|
143
127
|
cache = null;
|
|
144
128
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Returns the current model ID — from config/env.
|
|
147
|
-
*/
|
|
148
129
|
export function currentModelId() {
|
|
149
130
|
return process.env.MODEL || '';
|
|
150
131
|
}
|
package/dist/core/permission.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
class PermissionBroker {
|
|
2
2
|
pending = null;
|
|
3
|
-
// Session-scoped "always allow" set (cleared on restart)
|
|
4
3
|
alwaysAllow = new Set();
|
|
5
4
|
listeners = new Set();
|
|
6
5
|
nextId = 0;
|
|
7
6
|
subscribe(fn) {
|
|
8
7
|
this.listeners.add(fn);
|
|
9
|
-
// Immediately notify with current state
|
|
10
8
|
fn(this.pending?.req ?? null);
|
|
11
9
|
return () => this.listeners.delete(fn);
|
|
12
10
|
}
|
package/dist/core/provider.js
CHANGED
|
@@ -51,7 +51,6 @@ export class Provider {
|
|
|
51
51
|
}
|
|
52
52
|
throw new Error(raw);
|
|
53
53
|
}
|
|
54
|
-
// Non-streaming call (used by /compact)
|
|
55
54
|
async chat(messages, tools = []) {
|
|
56
55
|
const { messages: safeMessages } = this.sanitize(messages);
|
|
57
56
|
try {
|
|
@@ -75,7 +74,6 @@ export class Provider {
|
|
|
75
74
|
this.handleError(e);
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
|
-
// Streaming call — yields deltas then a final summary
|
|
79
77
|
async *chatStream(messages, tools) {
|
|
80
78
|
const { messages: safeMessages, redactedCount } = this.sanitize(messages);
|
|
81
79
|
try {
|
|
@@ -135,7 +133,6 @@ export class Provider {
|
|
|
135
133
|
this.handleError(e);
|
|
136
134
|
}
|
|
137
135
|
}
|
|
138
|
-
// Summarize helper for /compact
|
|
139
136
|
async summarize(messages) {
|
|
140
137
|
const res = await this.chat(messages, []);
|
|
141
138
|
return res.content || '';
|
package/dist/core/redact.js
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
const PATTERNS = [
|
|
2
|
-
// OpenAI / OpenRouter
|
|
3
2
|
[/sk-or-v1-[a-zA-Z0-9]{32,}/g, 'sk-or-v1-***REDACTED***'],
|
|
4
3
|
[/sk-[a-zA-Z0-9]{20,}/g, 'sk-***REDACTED***'],
|
|
5
|
-
// Anthropic
|
|
6
4
|
[/sk-ant-[a-zA-Z0-9\-_]{20,}/g, 'sk-ant-***REDACTED***'],
|
|
7
|
-
// GitHub
|
|
8
5
|
[/ghp_[a-zA-Z0-9]{36,}/g, 'ghp_***REDACTED***'],
|
|
9
6
|
[/gho_[a-zA-Z0-9]{36,}/g, 'gho_***REDACTED***'],
|
|
10
7
|
[/ghs_[a-zA-Z0-9]{36,}/g, 'ghs_***REDACTED***'],
|
|
11
8
|
[/github_pat_[a-zA-Z0-9_]{60,}/g, 'github_pat_***REDACTED***'],
|
|
12
|
-
// AWS
|
|
13
9
|
[/AKIA[0-9A-Z]{16}/g, 'AKIA***REDACTED***'],
|
|
14
10
|
[/aws_secret_access_key\s*=\s*[A-Za-z0-9/+=]{40}/gi, 'aws_secret_access_key=***REDACTED***'],
|
|
15
|
-
// Google
|
|
16
11
|
[/AIza[0-9A-Za-z_\-]{35}/g, 'AIza***REDACTED***'],
|
|
17
|
-
// Slack
|
|
18
12
|
[/xox[baprs]-[a-zA-Z0-9-]{10,}/g, 'xox***REDACTED***'],
|
|
19
|
-
// Stripe
|
|
20
13
|
[/sk_live_[a-zA-Z0-9]{24,}/g, 'sk_live_***REDACTED***'],
|
|
21
14
|
[/pk_live_[a-zA-Z0-9]{24,}/g, 'pk_live_***REDACTED***'],
|
|
22
|
-
// JWT
|
|
23
15
|
[/eyJ[a-zA-Z0-9_\-]{10,}\.eyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}/g, '***REDACTED JWT***'],
|
|
24
|
-
// Bearer tokens
|
|
25
16
|
[/Bearer\s+[a-zA-Z0-9_\-\.]{20,}/g, 'Bearer ***REDACTED***'],
|
|
26
|
-
// PEM keys
|
|
27
17
|
[/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '-----***REDACTED KEY***-----'],
|
|
28
|
-
// Generic env-style secrets (password=, token=, secret=)
|
|
29
18
|
[/(password|passwd|pwd|secret|token|api_key|apikey)\s*[:=]\s*["']?([A-Za-z0-9_\-\.]{16,})["']?/gi,
|
|
30
19
|
'$1=***REDACTED***'],
|
|
31
20
|
];
|
package/dist/core/session.js
CHANGED
|
@@ -21,25 +21,17 @@ export function loadSession(cwd = process.cwd()) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
const MAX_SESSION_BYTES = Number(process.env.BLYUE_MAX_SESSION_BYTES) || 5 * 1024 * 1024;
|
|
25
|
-
const MAX_DIR_BYTES = Number(process.env.BLYUE_MAX_SESSIONS_DIR_BYTES) || 50 * 1024 * 1024;
|
|
26
|
-
/**
|
|
27
|
-
* Trim the in-memory messages array so a session file never grows past
|
|
28
|
-
* MAX_SESSION_BYTES. Drops oldest non-system messages first, keeping a
|
|
29
|
-
* marker so the user (and the model) knows history was clipped.
|
|
30
|
-
*/
|
|
24
|
+
const MAX_SESSION_BYTES = Number(process.env.BLYUE_MAX_SESSION_BYTES) || 5 * 1024 * 1024;
|
|
25
|
+
const MAX_DIR_BYTES = Number(process.env.BLYUE_MAX_SESSIONS_DIR_BYTES) || 50 * 1024 * 1024;
|
|
31
26
|
function trimToBudget(messages) {
|
|
32
27
|
const size = (arr) => Buffer.byteLength(JSON.stringify(arr, null, 2), 'utf8');
|
|
33
28
|
if (size(messages) <= MAX_SESSION_BYTES)
|
|
34
29
|
return messages;
|
|
35
|
-
// Keep the most recent N messages, dropping from the front. Stop as
|
|
36
|
-
// soon as we're under budget; always keep at least the last 6.
|
|
37
30
|
let arr = messages.slice();
|
|
38
31
|
while (arr.length > 6 && size(arr) > MAX_SESSION_BYTES) {
|
|
39
32
|
arr.shift();
|
|
40
33
|
}
|
|
41
34
|
if (size(arr) > MAX_SESSION_BYTES) {
|
|
42
|
-
// Even the tail is too big — hard-truncate the largest text fields.
|
|
43
35
|
arr = arr.map((m) => {
|
|
44
36
|
const c = m.content;
|
|
45
37
|
if (typeof c === 'string' && c.length > 8000) {
|
|
@@ -53,10 +45,6 @@ function trimToBudget(messages) {
|
|
|
53
45
|
...arr,
|
|
54
46
|
];
|
|
55
47
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Evict oldest session files (never the active one) until the sessions
|
|
58
|
-
* directory is under MAX_DIR_BYTES.
|
|
59
|
-
*/
|
|
60
48
|
function pruneDirectory(activeFile) {
|
|
61
49
|
if (!fs.existsSync(SESSIONS_DIR))
|
|
62
50
|
return;
|
|
@@ -74,7 +62,7 @@ function pruneDirectory(activeFile) {
|
|
|
74
62
|
}
|
|
75
63
|
if (total <= MAX_DIR_BYTES)
|
|
76
64
|
return;
|
|
77
|
-
stats.sort((a, b) => a.mtime - b.mtime);
|
|
65
|
+
stats.sort((a, b) => a.mtime - b.mtime);
|
|
78
66
|
for (const { file, size } of stats) {
|
|
79
67
|
if (total <= MAX_DIR_BYTES)
|
|
80
68
|
break;
|
package/dist/ui/Banner.js
CHANGED
|
@@ -75,7 +75,6 @@ export const Banner = () => {
|
|
|
75
75
|
const pad = ' '.repeat(leftPad);
|
|
76
76
|
const [animate] = useState(() => shouldAnimate());
|
|
77
77
|
const [t, setT] = useState(0);
|
|
78
|
-
// Slow pastel drift — 24s full cycle, matches the rest of the TUI.
|
|
79
78
|
useEffect(() => {
|
|
80
79
|
if (!animate)
|
|
81
80
|
return;
|
package/dist/ui/Chat.js
CHANGED
|
@@ -70,7 +70,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
70
70
|
const [permReq, setPermReq] = useState(null);
|
|
71
71
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
72
72
|
const [agent, setAgent] = useState(() => new Agent());
|
|
73
|
-
// Mascot state
|
|
74
73
|
const [mascotState, setMascotState] = useState(undefined);
|
|
75
74
|
const [elapsedSec, setElapsedSec] = useState(0);
|
|
76
75
|
const startRef = useRef(null);
|
|
@@ -78,7 +77,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
78
77
|
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
79
78
|
return unsub;
|
|
80
79
|
}, []);
|
|
81
|
-
// Track elapsed time during loading
|
|
82
80
|
useEffect(() => {
|
|
83
81
|
if (!loading) {
|
|
84
82
|
startRef.current = null;
|
|
@@ -93,7 +91,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
93
91
|
}, 500);
|
|
94
92
|
return () => clearInterval(t);
|
|
95
93
|
}, [loading]);
|
|
96
|
-
// Flash a temporary mascot state
|
|
97
94
|
const flash = (state, ms = 1200) => {
|
|
98
95
|
setMascotState(state);
|
|
99
96
|
setTimeout(() => setMascotState(undefined), ms);
|
|
@@ -118,7 +115,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
118
115
|
if (!text.trim() || loading)
|
|
119
116
|
return;
|
|
120
117
|
const trimmed = text.trim();
|
|
121
|
-
// ---- slash commands ----
|
|
122
118
|
if (trimmed === '/model' || trimmed === '/models') {
|
|
123
119
|
setInput('');
|
|
124
120
|
setOverlay('model');
|
|
@@ -187,12 +183,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
187
183
|
]);
|
|
188
184
|
return;
|
|
189
185
|
}
|
|
190
|
-
// ---- normal run ----
|
|
191
186
|
setInput('');
|
|
192
187
|
setHistory((h) => [...h, mkItem('user', trimmed)]);
|
|
193
188
|
setLoading(true);
|
|
194
189
|
setStreaming('');
|
|
195
|
-
// Jump animation on submit
|
|
196
190
|
flash('jump', 400);
|
|
197
191
|
let textBuf = '';
|
|
198
192
|
let toolBuf = '';
|
|
@@ -201,7 +195,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
201
195
|
if (ev.type === 'text') {
|
|
202
196
|
textBuf += ev.delta;
|
|
203
197
|
setStreaming(textBuf);
|
|
204
|
-
// Clear any tool mascot while text streams
|
|
205
198
|
setMascotState(undefined);
|
|
206
199
|
}
|
|
207
200
|
else if (ev.type === 'tool_start') {
|
|
@@ -211,7 +204,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
211
204
|
}
|
|
212
205
|
toolBuf = '\n[Calling ' + ev.name + '...]\n';
|
|
213
206
|
setStreaming(toolBuf);
|
|
214
|
-
// React to the specific tool
|
|
215
207
|
setMascotState(toolToState(ev.name));
|
|
216
208
|
}
|
|
217
209
|
else if (ev.type === 'compact') {
|
|
@@ -224,7 +216,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
224
216
|
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
225
217
|
toolBuf = '';
|
|
226
218
|
setStreaming('');
|
|
227
|
-
// Brief pulse before next step
|
|
228
219
|
flash('success', 500);
|
|
229
220
|
}
|
|
230
221
|
}
|
package/dist/ui/MakerMark.js
CHANGED
|
@@ -2,10 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Mascot } from './Mascot.js';
|
|
4
4
|
export const MakerMark = ({ color, narrow }) => {
|
|
5
|
-
// Phone / narrow: single line
|
|
6
5
|
if (narrow) {
|
|
7
6
|
return (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: "\u2584\u2588\u2584 " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" })] }));
|
|
8
7
|
}
|
|
9
|
-
// Wide: mascot on left, wordmark vertically centered
|
|
10
8
|
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Mascot, { color: color }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: color, bold: true, children: "L U M E N" }), _jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })] }));
|
|
11
9
|
};
|
package/dist/ui/Mascot.js
CHANGED
|
@@ -2,11 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
// ─────────────────────────────────────────────
|
|
6
|
-
// LUMEN mascot — gradient body, solid eyes
|
|
7
|
-
// Light at top fades to dark at bottom
|
|
8
|
-
// (shading = light emitting downward)
|
|
9
|
-
// ─────────────────────────────────────────────
|
|
10
5
|
const BODY_TOP = ' ▄▄▄▄▄▄▄ ';
|
|
11
6
|
const BODY_LIGHT = '░░░░░░░░░';
|
|
12
7
|
const BODY_FACE_A = '░ ● ● ░';
|
|
@@ -27,7 +22,6 @@ function shouldAnimate() {
|
|
|
27
22
|
catch { }
|
|
28
23
|
return true;
|
|
29
24
|
}
|
|
30
|
-
// Full mascot — 9 wide × 6 tall
|
|
31
25
|
export const Mascot = ({ color }) => {
|
|
32
26
|
const [pulse, setPulse] = useState(0);
|
|
33
27
|
const [animate] = useState(() => shouldAnimate());
|
|
@@ -37,11 +31,8 @@ export const Mascot = ({ color }) => {
|
|
|
37
31
|
const t = setInterval(() => setPulse((p) => (p + 1) % 3), 800);
|
|
38
32
|
return () => clearInterval(t);
|
|
39
33
|
}, [animate]);
|
|
40
|
-
// Subtle pulse: alternate shade intensity on the middle row
|
|
41
34
|
const mid = pulse === 0 ? BODY_MED : pulse === 1 ? '▒▒▒▒▒▒▒▒▒' : '░░▒░░▒░░';
|
|
42
35
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: color, bold: true, children: BODY_TOP }), _jsx(Text, { color: color, bold: true, children: BODY_LIGHT }), _jsx(Text, { color: color, bold: true, children: BODY_FACE_A }), _jsx(Text, { color: color, bold: true, children: BODY_FACE_B }), _jsx(Text, { color: color, bold: true, children: mid }), _jsx(Text, { color: color, bold: true, children: BODY_DARK }), _jsx(Text, { color: color, bold: true, children: BODY_BOT })] }));
|
|
43
36
|
};
|
|
44
|
-
// Compact — one line, for input bar & prompt
|
|
45
37
|
export const MascotInline = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584 " }));
|
|
46
|
-
// Tiny — just the face, for very narrow terminals
|
|
47
38
|
export const MascotMini = ({ color }) => (_jsx(Text, { color: color, bold: true, children: "\u2584\u2588\u2584 " }));
|
package/dist/ui/MascotPrompt.js
CHANGED
|
@@ -48,8 +48,6 @@ export function toolToState(tool) {
|
|
|
48
48
|
return 'thinking';
|
|
49
49
|
}
|
|
50
50
|
const BlankRows = () => (_jsx(_Fragment, { children: Array.from({ length: TOP_ROWS }, (_, i) => (_jsx(Text, { children: " " }, i))) }));
|
|
51
|
-
// States whose color is semantic (meaning-bearing) — these stay their
|
|
52
|
-
// theme colors. Everything else drifts through the pastel palette.
|
|
53
51
|
function isSemantic(state) {
|
|
54
52
|
return (state === 'error' || state === 'denied' ||
|
|
55
53
|
state === 'success' || state === 'granted' ||
|
|
@@ -60,7 +58,7 @@ function semanticColor(state) {
|
|
|
60
58
|
return theme.error;
|
|
61
59
|
if (state === 'success' || state === 'granted')
|
|
62
60
|
return theme.success;
|
|
63
|
-
return theme.warning;
|
|
61
|
+
return theme.warning;
|
|
64
62
|
}
|
|
65
63
|
export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
66
64
|
const frames = FACES[state];
|
package/dist/ui/McpPanel.js
CHANGED
|
@@ -11,7 +11,6 @@ export const McpPanel = ({ onClose }) => {
|
|
|
11
11
|
const [servers, setServers] = useState(() => listMcpServers());
|
|
12
12
|
const [reloading, setReloading] = useState(false);
|
|
13
13
|
const [msg, setMsg] = useState(null);
|
|
14
|
-
// Refresh list on mount in case registry changed since last render.
|
|
15
14
|
useEffect(() => {
|
|
16
15
|
setServers(listMcpServers());
|
|
17
16
|
}, []);
|
package/dist/ui/ModelPicker.js
CHANGED
|
@@ -57,7 +57,7 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
57
57
|
const paid = [];
|
|
58
58
|
for (const m of matched) {
|
|
59
59
|
if (seen.has(m.id))
|
|
60
|
-
continue;
|
|
60
|
+
continue;
|
|
61
61
|
if (m.isFree)
|
|
62
62
|
free.push(m);
|
|
63
63
|
else
|
|
@@ -86,7 +86,6 @@ export const ModelPicker = ({ current, onSelect, onCancel }) => {
|
|
|
86
86
|
pushSection('PAID', paid);
|
|
87
87
|
}
|
|
88
88
|
else {
|
|
89
|
-
// 'all' — show recent first, then free, then paid
|
|
90
89
|
pushSection('RECENT', recent);
|
|
91
90
|
pushSection('FREE', free);
|
|
92
91
|
pushSection('PAID', paid);
|
package/dist/ui/RocketBar.js
CHANGED
|
@@ -26,7 +26,7 @@ function shouldAnimate() {
|
|
|
26
26
|
export const RocketBar = ({ label, loading, width }) => {
|
|
27
27
|
const [animate] = useState(() => shouldAnimate());
|
|
28
28
|
const [pos, setPos] = useState(0);
|
|
29
|
-
const hue = useSlowColor();
|
|
29
|
+
const hue = useSlowColor();
|
|
30
30
|
const fixed = 5 + label.length;
|
|
31
31
|
const midLen = Math.max(0, width - fixed);
|
|
32
32
|
useEffect(() => {
|
package/dist/ui/Settings.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Box, Text, useInput, useStdout } from 'ink';
|
|
|
4
4
|
import { KeyInput } from './KeyInput.js';
|
|
5
5
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
6
6
|
import { detectOllama } from '../core/localCheck.js';
|
|
7
|
+
import { loadMcpConfig, saveMcpConfig } from '../core/mcp.js';
|
|
8
|
+
import { initMcpRegistry, closeAllMcp, listMcpServers, } from '../core/mcpRegistry.js';
|
|
7
9
|
import { theme } from './theme.js';
|
|
8
10
|
export const Settings = ({ onClose, onModelChange }) => {
|
|
9
11
|
const { stdout } = useStdout();
|
|
@@ -15,6 +17,14 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
15
17
|
const [testResult, setTestResult] = useState(null);
|
|
16
18
|
const [ollamaModels, setOllamaModels] = useState([]);
|
|
17
19
|
const [ollamaChecked, setOllamaChecked] = useState(false);
|
|
20
|
+
const [mcpCfg, setMcpCfg] = useState(() => loadMcpConfig());
|
|
21
|
+
const [mcpCursor, setMcpCursor] = useState(0);
|
|
22
|
+
const [mcpDraftName, setMcpDraftName] = useState('');
|
|
23
|
+
const [mcpDraftCommand, setMcpDraftCommand] = useState('');
|
|
24
|
+
const [mcpDraftArgs, setMcpDraftArgs] = useState('');
|
|
25
|
+
const [mcpDraftAutoAllow, setMcpDraftAutoAllow] = useState(false);
|
|
26
|
+
const [mcpConfirmDel, setMcpConfirmDel] = useState(null);
|
|
27
|
+
const [mcpStatus, setMcpStatus] = useState(null);
|
|
18
28
|
useEffect(() => {
|
|
19
29
|
(async () => {
|
|
20
30
|
const models = await detectOllama();
|
|
@@ -27,6 +37,22 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
27
37
|
applyConfig(next);
|
|
28
38
|
setCfg(next);
|
|
29
39
|
};
|
|
40
|
+
const persistMcp = (next) => {
|
|
41
|
+
saveMcpConfig(next);
|
|
42
|
+
setMcpCfg(next);
|
|
43
|
+
};
|
|
44
|
+
const reloadMcp = async (msg) => {
|
|
45
|
+
setMcpStatus('reloading…');
|
|
46
|
+
try {
|
|
47
|
+
await closeAllMcp();
|
|
48
|
+
await initMcpRegistry();
|
|
49
|
+
setMcpStatus(msg);
|
|
50
|
+
setTimeout(() => setMcpStatus(null), 2500);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setMcpStatus('✗ ' + err.message);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
30
56
|
const mainItems = [
|
|
31
57
|
{ label: 'API key', hint: cfg.apiKey ? '•••' + cfg.apiKey.slice(-4) : 'not set', action: () => setScreen('apikey') },
|
|
32
58
|
{ label: 'Base URL', hint: cfg.baseUrl || 'default (OpenAI)', action: () => setScreen('baseurl') },
|
|
@@ -38,9 +64,16 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
38
64
|
: 'not configured',
|
|
39
65
|
action: () => setScreen('n8n'),
|
|
40
66
|
},
|
|
67
|
+
{
|
|
68
|
+
label: 'MCP servers',
|
|
69
|
+
hint: (() => {
|
|
70
|
+
const n = Object.keys(mcpCfg.servers || {}).length;
|
|
71
|
+
return n === 0 ? 'none configured' : `${n} server${n === 1 ? '' : 's'}`;
|
|
72
|
+
})(),
|
|
73
|
+
action: () => { setMcpCursor(0); setScreen('mcp'); },
|
|
74
|
+
},
|
|
41
75
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
42
76
|
];
|
|
43
|
-
// ---- Main menu navigation ----
|
|
44
77
|
useInput((_, key) => {
|
|
45
78
|
if (key.escape)
|
|
46
79
|
return onClose();
|
|
@@ -51,7 +84,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
51
84
|
if (key.return)
|
|
52
85
|
mainItems[cursor].action();
|
|
53
86
|
}, { isActive: screen === 'main' });
|
|
54
|
-
// ---- n8n submenu ----
|
|
55
87
|
const n8nItems = [
|
|
56
88
|
{ label: 'URL', hint: cfg.n8nUrl || 'not set', action: () => { setDraft(cfg.n8nUrl || ''); setScreen('n8n-url'); } },
|
|
57
89
|
{ label: 'API key', hint: cfg.n8nApiKey ? '•••' + cfg.n8nApiKey.slice(-4) : 'not set', action: () => { setDraft(cfg.n8nApiKey || ''); setScreen('n8n-key'); } },
|
|
@@ -86,16 +118,109 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
86
118
|
if (key.return)
|
|
87
119
|
n8nItems[n8nCursor].action();
|
|
88
120
|
}, { isActive: screen === 'n8n' });
|
|
89
|
-
// ---- n8n test ----
|
|
90
121
|
useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
|
|
91
|
-
|
|
122
|
+
const serverNames = Object.keys(mcpCfg.servers || {});
|
|
123
|
+
const mcpItems = [
|
|
124
|
+
{ label: '+ Add server', hint: 'new MCP server', action: () => {
|
|
125
|
+
setMcpDraftName('');
|
|
126
|
+
setMcpDraftCommand('');
|
|
127
|
+
setMcpDraftArgs('');
|
|
128
|
+
setMcpDraftAutoAllow(false);
|
|
129
|
+
setScreen('mcp-add-name');
|
|
130
|
+
} },
|
|
131
|
+
...serverNames.map((name) => {
|
|
132
|
+
const s = mcpCfg.servers[name];
|
|
133
|
+
const status = listMcpServers().find((x) => x.name === name);
|
|
134
|
+
const connected = status && !status.error;
|
|
135
|
+
const toolCount = status?.toolCount ?? 0;
|
|
136
|
+
const cmd = 'command' in s ? `${s.command} ${(s.args || []).join(' ')}` : s.url;
|
|
137
|
+
const autoTag = s.autoAllow ? ' [auto]' : '';
|
|
138
|
+
return {
|
|
139
|
+
label: name,
|
|
140
|
+
hint: connected
|
|
141
|
+
? `● ${toolCount} tools${autoTag} · delete with [d]`
|
|
142
|
+
: `✗ ${cmd.slice(0, 30)}`,
|
|
143
|
+
action: () => {
|
|
144
|
+
setMcpConfirmDel(name);
|
|
145
|
+
setScreen('mcp-confirm-del');
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
{ label: 'Reload registry', hint: 'reconnect all servers', action: () => { void reloadMcp('✓ reloaded'); } },
|
|
150
|
+
{ label: 'Back', hint: 'esc', action: () => setScreen('main') },
|
|
151
|
+
];
|
|
152
|
+
useInput((input, key) => {
|
|
153
|
+
if (key.escape)
|
|
154
|
+
return setScreen('main');
|
|
155
|
+
if (key.upArrow)
|
|
156
|
+
setMcpCursor((c) => (c - 1 + mcpItems.length) % mcpItems.length);
|
|
157
|
+
if (key.downArrow)
|
|
158
|
+
setMcpCursor((c) => (c + 1) % mcpItems.length);
|
|
159
|
+
if (input === 'd' || input === 'D') {
|
|
160
|
+
const item = mcpItems[mcpCursor];
|
|
161
|
+
if (item && serverNames.includes(item.label)) {
|
|
162
|
+
setMcpConfirmDel(item.label);
|
|
163
|
+
setScreen('mcp-confirm-del');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (key.return)
|
|
167
|
+
mcpItems[mcpCursor].action();
|
|
168
|
+
}, { isActive: screen === 'mcp' });
|
|
169
|
+
useInput((input, key) => {
|
|
170
|
+
if (input === 'y' || input === 'Y') {
|
|
171
|
+
if (mcpConfirmDel) {
|
|
172
|
+
const next = { ...mcpCfg, servers: { ...mcpCfg.servers } };
|
|
173
|
+
delete next.servers[mcpConfirmDel];
|
|
174
|
+
persistMcp(next);
|
|
175
|
+
void reloadMcp(`✓ removed ${mcpConfirmDel}`);
|
|
176
|
+
}
|
|
177
|
+
setMcpConfirmDel(null);
|
|
178
|
+
setScreen('mcp');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
182
|
+
setMcpConfirmDel(null);
|
|
183
|
+
setScreen('mcp');
|
|
184
|
+
}
|
|
185
|
+
}, { isActive: screen === 'mcp-confirm-del' });
|
|
186
|
+
useInput((input, key) => {
|
|
187
|
+
if (key.escape)
|
|
188
|
+
return setScreen('mcp');
|
|
189
|
+
if (input === 'y' || input === 'Y') {
|
|
190
|
+
finishAdd(true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (input === 'n' || input === 'N' || key.return) {
|
|
194
|
+
finishAdd(false);
|
|
195
|
+
}
|
|
196
|
+
}, { isActive: screen === 'mcp-add-autollw' });
|
|
197
|
+
const finishAdd = (autoAllow) => {
|
|
198
|
+
if (!mcpDraftName.trim() || !mcpDraftCommand.trim()) {
|
|
199
|
+
setScreen('mcp');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const args = mcpDraftArgs
|
|
203
|
+
.split(/\s+/)
|
|
204
|
+
.map((s) => s.trim())
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
const next = {
|
|
207
|
+
servers: {
|
|
208
|
+
...(mcpCfg.servers || {}),
|
|
209
|
+
[mcpDraftName.trim()]: {
|
|
210
|
+
command: mcpDraftCommand.trim(),
|
|
211
|
+
args,
|
|
212
|
+
autoAllow,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
persistMcp(next);
|
|
217
|
+
void reloadMcp(`✓ added ${mcpDraftName.trim()}`);
|
|
218
|
+
setScreen('mcp');
|
|
219
|
+
};
|
|
92
220
|
const inner = width - 4;
|
|
93
221
|
const top = (label) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(Math.max(1, inner - label.length - 3)) + '╮' })] }));
|
|
94
222
|
const bot = (_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }));
|
|
95
|
-
// ⚠️ Changed from <Text> to <Box flexDirection="row"> — callers pass a
|
|
96
|
-
// <Box> containing a prompt + <KeyInput>, and Ink forbids Box-in-Text.
|
|
97
223
|
const row = (content) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.muted, children: '│ ' }), content] }));
|
|
98
|
-
// ---- Main screen ----
|
|
99
224
|
if (screen === 'main') {
|
|
100
225
|
const baseUrlIsLocal = (cfg.baseUrl || '').includes('localhost') ||
|
|
101
226
|
(cfg.baseUrl || '').includes('127.0.0.1');
|
|
@@ -104,7 +229,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
104
229
|
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: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
105
230
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc close" })] }));
|
|
106
231
|
}
|
|
107
|
-
// ---- API key / Base URL editors ----
|
|
108
232
|
if (screen === 'apikey' || screen === 'baseurl') {
|
|
109
233
|
const isKey = screen === 'apikey';
|
|
110
234
|
const title = isKey ? 'api key' : 'base url';
|
|
@@ -115,7 +239,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
115
239
|
};
|
|
116
240
|
return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Paste your new API key' : 'e.g. http://localhost:11434/v1' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'sk-...' : 'https://...', masked: isKey, onSubmit: () => setScreen('main') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
117
241
|
}
|
|
118
|
-
// ---- Model editor ----
|
|
119
242
|
if (screen === 'model') {
|
|
120
243
|
const isLocal = (cfg.baseUrl || '').includes('localhost');
|
|
121
244
|
return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children: isLocal && ollamaModels.length > 0
|
|
@@ -125,14 +248,12 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
125
248
|
setScreen('main');
|
|
126
249
|
} })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
127
250
|
}
|
|
128
|
-
// ---- n8n submenu ----
|
|
129
251
|
if (screen === 'n8n') {
|
|
130
252
|
return (_jsxs(Box, { flexDirection: "column", children: [top('n8n integration'), n8nItems.map((it, i) => {
|
|
131
253
|
const active = i === n8nCursor;
|
|
132
254
|
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: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
133
255
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc back" })] }));
|
|
134
256
|
}
|
|
135
|
-
// ---- n8n URL / key editors ----
|
|
136
257
|
if (screen === 'n8n-url' || screen === 'n8n-key') {
|
|
137
258
|
const isKey = screen === 'n8n-key';
|
|
138
259
|
const title = isKey ? 'n8n api key' : 'n8n url';
|
|
@@ -143,9 +264,29 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
143
264
|
};
|
|
144
265
|
return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Settings → n8n API → create key' : 'e.g. http://localhost:5678' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'n8n_api_...' : 'http://localhost:5678', masked: isKey, onSubmit: () => setScreen('n8n') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
145
266
|
}
|
|
146
|
-
// ---- n8n test result ----
|
|
147
267
|
if (screen === 'n8n-test') {
|
|
148
268
|
return (_jsxs(Box, { flexDirection: "column", children: [top('n8n test'), row(_jsx(Text, { color: testResult?.startsWith('✓') ? theme.success : theme.muted, children: testResult || 'testing…' })), bot, _jsx(Text, { color: theme.muted, children: "press any key to go back" })] }));
|
|
149
269
|
}
|
|
270
|
+
if (screen === 'mcp') {
|
|
271
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('mcp servers'), row(_jsx(Text, { color: theme.muted, children: "config: ~/.blyue/mcp.json" })), row(_jsxs(Text, { color: theme.muted, children: [serverNames.length, " server", serverNames.length === 1 ? '' : 's', mcpStatus ? ' · ' : '', mcpStatus && (_jsx(Text, { color: mcpStatus.startsWith('✗') ? theme.error : theme.success, children: mcpStatus }))] })), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), mcpItems.map((it, i) => {
|
|
272
|
+
const active = i === mcpCursor;
|
|
273
|
+
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: it.label.padEnd(22) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
274
|
+
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 [d] delete \u00B7 esc back" })] }));
|
|
275
|
+
}
|
|
276
|
+
if (screen === 'mcp-confirm-del') {
|
|
277
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('delete server'), row(_jsxs(Text, { color: theme.warning, children: ["Delete MCP server \"", mcpConfirmDel, "\"?"] })), row(_jsx(Text, { color: theme.muted, children: "This removes it from mcp.json." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no" })] })] }));
|
|
278
|
+
}
|
|
279
|
+
if (screen === 'mcp-add-name') {
|
|
280
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 1/4'), row(_jsx(Text, { color: theme.muted, children: "Name (unique, e.g. \"filesystem\")" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftName, onChange: setMcpDraftName, placeholder: "filesystem", onSubmit: () => setScreen('mcp-add-command') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
281
|
+
}
|
|
282
|
+
if (screen === 'mcp-add-command') {
|
|
283
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 2/4'), row(_jsx(Text, { color: theme.muted, children: "Command (binary to run)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftCommand, onChange: setMcpDraftCommand, placeholder: "npx", onSubmit: () => setScreen('mcp-add-args') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
284
|
+
}
|
|
285
|
+
if (screen === 'mcp-add-args') {
|
|
286
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 3/4'), row(_jsx(Text, { color: theme.muted, children: "Args (space-separated, can be empty)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftArgs, onChange: setMcpDraftArgs, placeholder: "-y @modelcontextprotocol/server-filesystem /home/user", onSubmit: () => setScreen('mcp-add-autollw') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
287
|
+
}
|
|
288
|
+
if (screen === 'mcp-add-autollw') {
|
|
289
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 4/4'), row(_jsx(Text, { color: theme.muted, children: "Auto-allow calls to this server (skip permission prompts)?" })), row(_jsx(Text, { color: theme.warning, children: "Only enable for servers you fully trust." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no (recommended)" })] })] }));
|
|
290
|
+
}
|
|
150
291
|
return null;
|
|
151
292
|
};
|
package/dist/ui/Setup.js
CHANGED
|
@@ -91,7 +91,6 @@ export const Setup = ({ onDone }) => {
|
|
|
91
91
|
setStep('provider');
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
-
// Header: single line hint
|
|
95
94
|
const Hint = ({ children }) => (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.muted, children: children }) }));
|
|
96
95
|
if (step === 'provider') {
|
|
97
96
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Hint, { children: "Select a provider \u2191\u2193 Enter" }), PROVIDERS.map((p, i) => {
|
package/dist/ui/SleepZs.js
CHANGED
|
@@ -2,8 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { theme } from './theme.js';
|
|
5
|
-
// Two rows, always exactly two rows — the mascot container reserves this
|
|
6
|
-
// space whether a z is visible or not, so Ink never reflows.
|
|
7
5
|
const FRAMES = [
|
|
8
6
|
[' ', ' '],
|
|
9
7
|
[' ', ' z '],
|
package/dist/ui/Spinner.js
CHANGED
|
@@ -6,7 +6,6 @@ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
|
|
|
6
6
|
export const Spinner = ({ label = 'Thinking' }) => {
|
|
7
7
|
const [i, setI] = useState(0);
|
|
8
8
|
useEffect(() => {
|
|
9
|
-
// 200ms instead of 90ms → 5 fps not 11 fps
|
|
10
9
|
const t = setInterval(() => setI((v) => (v + 1) % FRAMES.length), 200);
|
|
11
10
|
return () => clearInterval(t);
|
|
12
11
|
}, []);
|
package/dist/ui/activity.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// Tiny global activity bus — avoids prop-drilling an `activeSignal` number
|
|
2
|
-
// through every component. Chat.tsx calls bumpActivity() from useInput;
|
|
3
|
-
// MascotPrompt subscribes via useIdle().
|
|
4
1
|
let counter = 0;
|
|
5
2
|
const listeners = new Set();
|
|
6
|
-
/** Call this from any real user input handler (keystroke, submit). */
|
|
7
3
|
export function bumpActivity() {
|
|
8
4
|
counter = (counter + 1) % Number.MAX_SAFE_INTEGER;
|
|
9
5
|
for (const l of listeners)
|
package/dist/ui/animColor.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// HSL → hex, no dependencies
|
|
2
1
|
function hslToHex(h, s, l) {
|
|
3
2
|
h = ((h % 360) + 360) % 360;
|
|
4
3
|
s /= 100;
|
|
@@ -11,9 +10,7 @@ function hslToHex(h, s, l) {
|
|
|
11
10
|
};
|
|
12
11
|
return `#${f(0)}${f(8)}${f(4)}`;
|
|
13
12
|
}
|
|
14
|
-
/** Returns a hex color at position t (0..1) along a soft palette loop. */
|
|
15
13
|
export function hueAt(t, lightness = 65, saturation = 70) {
|
|
16
|
-
// Weighted loop: cyan → blue → purple → pink → cyan
|
|
17
14
|
const hue = 180 + Math.sin(t * Math.PI * 2) * 120;
|
|
18
15
|
return hslToHex(hue, saturation, lightness);
|
|
19
16
|
}
|
package/dist/ui/pastel.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// Pastel/faded palette — Claude-Code-inspired soft muting.
|
|
2
|
-
// All colors sit at ~76% lightness / ~48% saturation, so they read as
|
|
3
|
-
// "faded" on both dark and light terminals.
|
|
4
1
|
function hslToHex(h, s, l) {
|
|
5
2
|
s /= 100;
|
|
6
3
|
l /= 100;
|
|
@@ -13,18 +10,12 @@ function hslToHex(h, s, l) {
|
|
|
13
10
|
const toHex = (x) => x.toString(16).padStart(2, '0');
|
|
14
11
|
return '#' + toHex(f(0)) + toHex(f(8)) + toHex(f(4));
|
|
15
12
|
}
|
|
16
|
-
/** Full-hue pastel cycle. t in [0, 1). */
|
|
17
13
|
export function pastelAt(t) {
|
|
18
14
|
const h = (((t % 1) + 1) % 1) * 360;
|
|
19
15
|
return hslToHex(h, 48, 76);
|
|
20
16
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Warm-only variant — rose → peach → gold → sage → back. If the full
|
|
23
|
-
* wheel feels too rainbow-y for your taste, swap pastelAt -> pastelWarmAt
|
|
24
|
-
* in the four call sites below.
|
|
25
|
-
*/
|
|
26
17
|
export function pastelWarmAt(t) {
|
|
27
18
|
const t2 = ((t % 1) + 1) % 1;
|
|
28
|
-
const h = (320 + t2 * 110) % 360;
|
|
19
|
+
const h = (320 + t2 * 110) % 360;
|
|
29
20
|
return hslToHex(h, 52, 76);
|
|
30
21
|
}
|
package/dist/ui/slowTick.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// fade in sync. One interval, N subscribers, no per-component drift.
|
|
3
|
-
const PERIOD_MS = 24_000; // full pastel cycle
|
|
1
|
+
const PERIOD_MS = 24_000;
|
|
4
2
|
const TICK_MS = 120;
|
|
5
3
|
let t = 0;
|
|
6
4
|
const listeners = new Set();
|
|
@@ -13,7 +11,6 @@ function start() {
|
|
|
13
11
|
for (const l of listeners)
|
|
14
12
|
l();
|
|
15
13
|
}, TICK_MS);
|
|
16
|
-
// Don't keep the Node process alive just to animate colors.
|
|
17
14
|
const anyInt = interval;
|
|
18
15
|
anyInt.unref?.();
|
|
19
16
|
}
|
package/dist/ui/useIdle.js
CHANGED
|
@@ -27,11 +27,6 @@ export function useIdle({ drowsyAfterMs = 120_000, sleepAfterMs = 300_000, disab
|
|
|
27
27
|
}, [active, drowsyAfterMs, sleepAfterMs, disabled]);
|
|
28
28
|
return stage;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Idle thresholds.
|
|
32
|
-
* BLYUE_FAST_IDLE=1 -> 3s / 6s (dev testing only)
|
|
33
|
-
* default -> 2 min drowsy / 5 min sleep
|
|
34
|
-
*/
|
|
35
30
|
export function defaultIdleThresholds() {
|
|
36
31
|
if (process.env.BLYUE_FAST_IDLE === '1') {
|
|
37
32
|
return { drowsyAfterMs: 3_000, sleepAfterMs: 6_000 };
|
package/dist/ui/useSlowColor.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
2
|
import { subscribeSlowTick, getSlowTick } from './slowTick.js';
|
|
3
3
|
import { pastelAt, pastelWarmAt } from './pastel.js';
|
|
4
|
-
/**
|
|
5
|
-
* Returns a pastel hex color that drifts slowly and stays in sync with
|
|
6
|
-
* every other component that uses this hook.
|
|
7
|
-
*/
|
|
8
4
|
export function useSlowColor(warm = false) {
|
|
9
5
|
const t = useSyncExternalStore(subscribeSlowTick, getSlowTick, getSlowTick);
|
|
10
6
|
return warm ? pastelWarmAt(t) : pastelAt(t);
|