blyuecode 0.3.7 → 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 +0 -19
- 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
|
@@ -17,7 +17,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
17
17
|
const [testResult, setTestResult] = useState(null);
|
|
18
18
|
const [ollamaModels, setOllamaModels] = useState([]);
|
|
19
19
|
const [ollamaChecked, setOllamaChecked] = useState(false);
|
|
20
|
-
// MCP draft state
|
|
21
20
|
const [mcpCfg, setMcpCfg] = useState(() => loadMcpConfig());
|
|
22
21
|
const [mcpCursor, setMcpCursor] = useState(0);
|
|
23
22
|
const [mcpDraftName, setMcpDraftName] = useState('');
|
|
@@ -75,7 +74,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
75
74
|
},
|
|
76
75
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
77
76
|
];
|
|
78
|
-
// ---- Main menu ----
|
|
79
77
|
useInput((_, key) => {
|
|
80
78
|
if (key.escape)
|
|
81
79
|
return onClose();
|
|
@@ -86,7 +84,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
86
84
|
if (key.return)
|
|
87
85
|
mainItems[cursor].action();
|
|
88
86
|
}, { isActive: screen === 'main' });
|
|
89
|
-
// ---- n8n submenu ----
|
|
90
87
|
const n8nItems = [
|
|
91
88
|
{ label: 'URL', hint: cfg.n8nUrl || 'not set', action: () => { setDraft(cfg.n8nUrl || ''); setScreen('n8n-url'); } },
|
|
92
89
|
{ label: 'API key', hint: cfg.n8nApiKey ? '•••' + cfg.n8nApiKey.slice(-4) : 'not set', action: () => { setDraft(cfg.n8nApiKey || ''); setScreen('n8n-key'); } },
|
|
@@ -122,7 +119,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
122
119
|
n8nItems[n8nCursor].action();
|
|
123
120
|
}, { isActive: screen === 'n8n' });
|
|
124
121
|
useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
|
|
125
|
-
// ---- MCP submenu ----
|
|
126
122
|
const serverNames = Object.keys(mcpCfg.servers || {});
|
|
127
123
|
const mcpItems = [
|
|
128
124
|
{ label: '+ Add server', hint: 'new MCP server', action: () => {
|
|
@@ -170,7 +166,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
170
166
|
if (key.return)
|
|
171
167
|
mcpItems[mcpCursor].action();
|
|
172
168
|
}, { isActive: screen === 'mcp' });
|
|
173
|
-
// ---- MCP confirm delete ----
|
|
174
169
|
useInput((input, key) => {
|
|
175
170
|
if (input === 'y' || input === 'Y') {
|
|
176
171
|
if (mcpConfirmDel) {
|
|
@@ -188,7 +183,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
188
183
|
setScreen('mcp');
|
|
189
184
|
}
|
|
190
185
|
}, { isActive: screen === 'mcp-confirm-del' });
|
|
191
|
-
// ---- MCP add wizard: autoAllow toggle ----
|
|
192
186
|
useInput((input, key) => {
|
|
193
187
|
if (key.escape)
|
|
194
188
|
return setScreen('mcp');
|
|
@@ -223,12 +217,10 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
223
217
|
void reloadMcp(`✓ added ${mcpDraftName.trim()}`);
|
|
224
218
|
setScreen('mcp');
|
|
225
219
|
};
|
|
226
|
-
// ---- Frame helpers ----
|
|
227
220
|
const inner = width - 4;
|
|
228
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)) + '╮' })] }));
|
|
229
222
|
const bot = (_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }));
|
|
230
223
|
const row = (content) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.muted, children: '│ ' }), content] }));
|
|
231
|
-
// ---- Main screen ----
|
|
232
224
|
if (screen === 'main') {
|
|
233
225
|
const baseUrlIsLocal = (cfg.baseUrl || '').includes('localhost') ||
|
|
234
226
|
(cfg.baseUrl || '').includes('127.0.0.1');
|
|
@@ -237,7 +229,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
237
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));
|
|
238
230
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc close" })] }));
|
|
239
231
|
}
|
|
240
|
-
// ---- API key / Base URL ----
|
|
241
232
|
if (screen === 'apikey' || screen === 'baseurl') {
|
|
242
233
|
const isKey = screen === 'apikey';
|
|
243
234
|
const title = isKey ? 'api key' : 'base url';
|
|
@@ -248,7 +239,6 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
248
239
|
};
|
|
249
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" })] }));
|
|
250
241
|
}
|
|
251
|
-
// ---- Model ----
|
|
252
242
|
if (screen === 'model') {
|
|
253
243
|
const isLocal = (cfg.baseUrl || '').includes('localhost');
|
|
254
244
|
return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children: isLocal && ollamaModels.length > 0
|
|
@@ -258,14 +248,12 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
258
248
|
setScreen('main');
|
|
259
249
|
} })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
260
250
|
}
|
|
261
|
-
// ---- n8n submenu ----
|
|
262
251
|
if (screen === 'n8n') {
|
|
263
252
|
return (_jsxs(Box, { flexDirection: "column", children: [top('n8n integration'), n8nItems.map((it, i) => {
|
|
264
253
|
const active = i === n8nCursor;
|
|
265
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));
|
|
266
255
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc back" })] }));
|
|
267
256
|
}
|
|
268
|
-
// ---- n8n url/key ----
|
|
269
257
|
if (screen === 'n8n-url' || screen === 'n8n-key') {
|
|
270
258
|
const isKey = screen === 'n8n-key';
|
|
271
259
|
const title = isKey ? 'n8n api key' : 'n8n url';
|
|
@@ -276,34 +264,27 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
276
264
|
};
|
|
277
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" })] }));
|
|
278
266
|
}
|
|
279
|
-
// ---- n8n test ----
|
|
280
267
|
if (screen === 'n8n-test') {
|
|
281
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" })] }));
|
|
282
269
|
}
|
|
283
|
-
// ---- MCP submenu ----
|
|
284
270
|
if (screen === 'mcp') {
|
|
285
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) => {
|
|
286
272
|
const active = i === mcpCursor;
|
|
287
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));
|
|
288
274
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 [d] delete \u00B7 esc back" })] }));
|
|
289
275
|
}
|
|
290
|
-
// ---- MCP confirm delete ----
|
|
291
276
|
if (screen === 'mcp-confirm-del') {
|
|
292
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" })] })] }));
|
|
293
278
|
}
|
|
294
|
-
// ---- MCP add wizard: name ----
|
|
295
279
|
if (screen === 'mcp-add-name') {
|
|
296
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" })] }));
|
|
297
281
|
}
|
|
298
|
-
// ---- MCP add wizard: command ----
|
|
299
282
|
if (screen === 'mcp-add-command') {
|
|
300
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" })] }));
|
|
301
284
|
}
|
|
302
|
-
// ---- MCP add wizard: args ----
|
|
303
285
|
if (screen === 'mcp-add-args') {
|
|
304
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" })] }));
|
|
305
287
|
}
|
|
306
|
-
// ---- MCP add wizard: autoAllow ----
|
|
307
288
|
if (screen === 'mcp-add-autollw') {
|
|
308
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)" })] })] }));
|
|
309
290
|
}
|
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);
|