blyuecode 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agent.js +13 -2
- 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/core/skills.js +88 -0
- package/dist/ui/Banner.js +0 -1
- package/dist/ui/Chat.js +53 -10
- 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/SkillsPanel.js +20 -0
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Provider } from './provider.js';
|
|
2
2
|
import { getToolDefs, executeTool } from './tools.js';
|
|
3
3
|
import { loadSession, saveSession, clearSession } from './session.js';
|
|
4
|
+
import { formatSkillForPrompt } from './skills.js';
|
|
4
5
|
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
5
6
|
'Use tools to read, write, and run commands. ' +
|
|
6
7
|
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
@@ -29,6 +30,16 @@ export class Agent {
|
|
|
29
30
|
}
|
|
30
31
|
catch { }
|
|
31
32
|
}
|
|
33
|
+
buildSystemMessage(skills) {
|
|
34
|
+
let content = SYSTEM_PROMPT;
|
|
35
|
+
if (skills.length > 0) {
|
|
36
|
+
content += '\n\n# Active skills\n';
|
|
37
|
+
for (const sk of skills) {
|
|
38
|
+
content += '\n' + formatSkillForPrompt(sk) + '\n';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { role: 'system', content };
|
|
42
|
+
}
|
|
32
43
|
historySize() {
|
|
33
44
|
return this.messages.length - 1;
|
|
34
45
|
}
|
|
@@ -63,7 +74,8 @@ export class Agent {
|
|
|
63
74
|
return null;
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
|
-
async *run(userInput) {
|
|
77
|
+
async *run(userInput, opts) {
|
|
78
|
+
this.messages[0] = this.buildSystemMessage(opts?.activeSkills ?? []);
|
|
67
79
|
this.messages.push({ role: 'user', content: userInput });
|
|
68
80
|
this.persist();
|
|
69
81
|
const compacted = await this.maybeAutoCompact();
|
|
@@ -124,7 +136,6 @@ export class Agent {
|
|
|
124
136
|
const before = this.messages.length;
|
|
125
137
|
const sys = this.messages[0];
|
|
126
138
|
const rest = this.messages.slice(1);
|
|
127
|
-
// Build a compact transcript
|
|
128
139
|
const transcript = rest
|
|
129
140
|
.map((m) => {
|
|
130
141
|
if (m.role === 'tool') {
|
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;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const SKILLS_DIR = path.join(os.homedir(), '.blyue', 'skills');
|
|
5
|
+
export function skillsDir() {
|
|
6
|
+
return SKILLS_DIR;
|
|
7
|
+
}
|
|
8
|
+
export function ensureSkillsDir() {
|
|
9
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
10
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function stripQuotes(s) {
|
|
14
|
+
return s.replace(/^["']|["']$/g, '').trim();
|
|
15
|
+
}
|
|
16
|
+
function parseFrontmatter(raw) {
|
|
17
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
18
|
+
if (!m)
|
|
19
|
+
return null;
|
|
20
|
+
const [, front, body] = m;
|
|
21
|
+
const meta = {};
|
|
22
|
+
for (const line of front.split(/\r?\n/)) {
|
|
23
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
24
|
+
continue;
|
|
25
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
26
|
+
if (!kv)
|
|
27
|
+
continue;
|
|
28
|
+
const [, key, rawVal] = kv;
|
|
29
|
+
if (key === 'triggers') {
|
|
30
|
+
const cleaned = rawVal.replace(/^\[|\]$/g, '');
|
|
31
|
+
meta.triggers = cleaned
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((s) => stripQuotes(s))
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
meta[key] = stripQuotes(rawVal);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { meta, body: body.trim() };
|
|
41
|
+
}
|
|
42
|
+
function loadOne(skillFile) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
45
|
+
const parsed = parseFrontmatter(raw);
|
|
46
|
+
if (!parsed)
|
|
47
|
+
return null;
|
|
48
|
+
const fallbackName = path.basename(path.dirname(skillFile));
|
|
49
|
+
return {
|
|
50
|
+
name: parsed.meta.name || fallbackName,
|
|
51
|
+
description: parsed.meta.description || '',
|
|
52
|
+
triggers: parsed.meta.triggers || [],
|
|
53
|
+
body: parsed.body,
|
|
54
|
+
path: skillFile,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function listSkills() {
|
|
62
|
+
if (!fs.existsSync(SKILLS_DIR))
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory())
|
|
67
|
+
continue;
|
|
68
|
+
const skillFile = path.join(SKILLS_DIR, entry.name, 'SKILL.md');
|
|
69
|
+
if (!fs.existsSync(skillFile))
|
|
70
|
+
continue;
|
|
71
|
+
const skill = loadOne(skillFile);
|
|
72
|
+
if (skill)
|
|
73
|
+
out.push(skill);
|
|
74
|
+
}
|
|
75
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
}
|
|
77
|
+
export function findSkill(name) {
|
|
78
|
+
const lc = name.toLowerCase();
|
|
79
|
+
return (listSkills().find((s) => s.name.toLowerCase() === lc) || null);
|
|
80
|
+
}
|
|
81
|
+
export function matchSkills(userInput) {
|
|
82
|
+
const lc = userInput.toLowerCase();
|
|
83
|
+
return listSkills().filter((s) => s.triggers.some((t) => t && lc.includes(t.toLowerCase())));
|
|
84
|
+
}
|
|
85
|
+
export function formatSkillForPrompt(skill) {
|
|
86
|
+
const desc = skill.description ? ` — ${skill.description}` : '';
|
|
87
|
+
return `## Skill: ${skill.name}${desc}\n${skill.body}`;
|
|
88
|
+
}
|
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
|
@@ -19,6 +19,8 @@ import { bumpActivity } from './activity.js';
|
|
|
19
19
|
import { theme } from './theme.js';
|
|
20
20
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
21
21
|
import { recordModelUse } from '../core/modelUsage.js';
|
|
22
|
+
import { SkillsPanel } from './SkillsPanel.js';
|
|
23
|
+
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
22
24
|
let nextId = 0;
|
|
23
25
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
24
26
|
const HistoryLine = ({ item }) => {
|
|
@@ -67,10 +69,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
67
69
|
const [loading, setLoading] = useState(false);
|
|
68
70
|
const [streaming, setStreaming] = useState('');
|
|
69
71
|
const [overlay, setOverlay] = useState('none');
|
|
72
|
+
const [pendingSkill, setPendingSkill] = useState(null);
|
|
70
73
|
const [permReq, setPermReq] = useState(null);
|
|
71
74
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
72
75
|
const [agent, setAgent] = useState(() => new Agent());
|
|
73
|
-
// Mascot state
|
|
74
76
|
const [mascotState, setMascotState] = useState(undefined);
|
|
75
77
|
const [elapsedSec, setElapsedSec] = useState(0);
|
|
76
78
|
const startRef = useRef(null);
|
|
@@ -78,7 +80,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
78
80
|
const unsub = permissions.subscribe((req) => setPermReq(req));
|
|
79
81
|
return unsub;
|
|
80
82
|
}, []);
|
|
81
|
-
// Track elapsed time during loading
|
|
82
83
|
useEffect(() => {
|
|
83
84
|
if (!loading) {
|
|
84
85
|
startRef.current = null;
|
|
@@ -93,7 +94,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
93
94
|
}, 500);
|
|
94
95
|
return () => clearInterval(t);
|
|
95
96
|
}, [loading]);
|
|
96
|
-
// Flash a temporary mascot state
|
|
97
97
|
const flash = (state, ms = 1200) => {
|
|
98
98
|
setMascotState(state);
|
|
99
99
|
setTimeout(() => setMascotState(undefined), ms);
|
|
@@ -108,6 +108,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
108
108
|
if (key.ctrl && inputChar === 'o')
|
|
109
109
|
setOverlay('settings');
|
|
110
110
|
});
|
|
111
|
+
useEffect(() => { try {
|
|
112
|
+
ensureSkillsDir();
|
|
113
|
+
}
|
|
114
|
+
catch { } }, []);
|
|
111
115
|
const flush = (kind, text) => {
|
|
112
116
|
if (!text)
|
|
113
117
|
return;
|
|
@@ -118,7 +122,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
118
122
|
if (!text.trim() || loading)
|
|
119
123
|
return;
|
|
120
124
|
const trimmed = text.trim();
|
|
121
|
-
// ---- slash commands ----
|
|
122
125
|
if (trimmed === '/model' || trimmed === '/models') {
|
|
123
126
|
setInput('');
|
|
124
127
|
setOverlay('model');
|
|
@@ -134,6 +137,31 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
134
137
|
setOverlay('mcp');
|
|
135
138
|
return;
|
|
136
139
|
}
|
|
140
|
+
if (trimmed === '/skills') {
|
|
141
|
+
setInput('');
|
|
142
|
+
setOverlay('skills');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (trimmed.startsWith('/skill ')) {
|
|
146
|
+
setInput('');
|
|
147
|
+
const name = trimmed.slice(7).trim();
|
|
148
|
+
const sk = findSkill(name);
|
|
149
|
+
if (!sk) {
|
|
150
|
+
const all = listSkills().map((x) => x.name).join(', ') || '(none)';
|
|
151
|
+
setHistory((h) => [
|
|
152
|
+
...h,
|
|
153
|
+
mkItem('error', 'unknown skill: ' + name),
|
|
154
|
+
mkItem('tool', 'available: ' + all),
|
|
155
|
+
]);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setPendingSkill(sk);
|
|
159
|
+
setHistory((h) => [
|
|
160
|
+
...h,
|
|
161
|
+
mkItem('tool', 'skill armed: ' + sk.name + ' (applies to next message)'),
|
|
162
|
+
]);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
137
165
|
if (trimmed === '/debug') {
|
|
138
166
|
setInput('');
|
|
139
167
|
const lines = readDebug();
|
|
@@ -180,6 +208,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
180
208
|
mkItem('tool', ' /model switch model (Ctrl+L)'),
|
|
181
209
|
mkItem('tool', ' /settings open settings (Ctrl+O)'),
|
|
182
210
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
211
|
+
mkItem('tool', ' /skills list skills'),
|
|
212
|
+
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
183
213
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
184
214
|
mkItem('tool', ' /debug show last request to provider'),
|
|
185
215
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
@@ -187,21 +217,33 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
187
217
|
]);
|
|
188
218
|
return;
|
|
189
219
|
}
|
|
190
|
-
// ---- normal run ----
|
|
191
220
|
setInput('');
|
|
192
221
|
setHistory((h) => [...h, mkItem('user', trimmed)]);
|
|
193
222
|
setLoading(true);
|
|
194
223
|
setStreaming('');
|
|
195
|
-
// Jump animation on submit
|
|
196
224
|
flash('jump', 400);
|
|
197
225
|
let textBuf = '';
|
|
198
226
|
let toolBuf = '';
|
|
199
227
|
try {
|
|
200
|
-
|
|
228
|
+
const matched = matchSkills(trimmed);
|
|
229
|
+
const activeSkills = [];
|
|
230
|
+
if (pendingSkill)
|
|
231
|
+
activeSkills.push(pendingSkill);
|
|
232
|
+
for (const m of matched) {
|
|
233
|
+
if (!activeSkills.find((x) => x.name === m.name))
|
|
234
|
+
activeSkills.push(m);
|
|
235
|
+
}
|
|
236
|
+
if (activeSkills.length > 0) {
|
|
237
|
+
setHistory((h) => [
|
|
238
|
+
...h,
|
|
239
|
+
mkItem('tool', 'skills active: ' + activeSkills.map((x) => x.name).join(', ')),
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
setPendingSkill(null);
|
|
243
|
+
for await (const ev of agent.run(trimmed, { activeSkills })) {
|
|
201
244
|
if (ev.type === 'text') {
|
|
202
245
|
textBuf += ev.delta;
|
|
203
246
|
setStreaming(textBuf);
|
|
204
|
-
// Clear any tool mascot while text streams
|
|
205
247
|
setMascotState(undefined);
|
|
206
248
|
}
|
|
207
249
|
else if (ev.type === 'tool_start') {
|
|
@@ -211,7 +253,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
211
253
|
}
|
|
212
254
|
toolBuf = '\n[Calling ' + ev.name + '...]\n';
|
|
213
255
|
setStreaming(toolBuf);
|
|
214
|
-
// React to the specific tool
|
|
215
256
|
setMascotState(toolToState(ev.name));
|
|
216
257
|
}
|
|
217
258
|
else if (ev.type === 'compact') {
|
|
@@ -224,7 +265,6 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
224
265
|
flush('tool', toolBuf + '[Result]: ' + ev.preview + '\n');
|
|
225
266
|
toolBuf = '';
|
|
226
267
|
setStreaming('');
|
|
227
|
-
// Brief pulse before next step
|
|
228
268
|
flash('success', 500);
|
|
229
269
|
}
|
|
230
270
|
}
|
|
@@ -264,6 +304,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
264
304
|
if (overlay === 'mcp') {
|
|
265
305
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
266
306
|
}
|
|
307
|
+
if (overlay === 'skills') {
|
|
308
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillsPanel, { onClose: () => setOverlay('none') }) }));
|
|
309
|
+
}
|
|
267
310
|
if (overlay === 'settings') {
|
|
268
311
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Settings, { onClose: () => setOverlay('none'), onModelChange: (m) => {
|
|
269
312
|
setCurrentModel(m);
|
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) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { listSkills, skillsDir } from '../core/skills.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const SkillsPanel = ({ onClose }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [skills, setSkills] = useState(() => listSkills());
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (key.escape)
|
|
13
|
+
return onClose();
|
|
14
|
+
if (input === 'r' || input === 'R')
|
|
15
|
+
setSkills(listSkills());
|
|
16
|
+
});
|
|
17
|
+
const label = 'skills';
|
|
18
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_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(topPad) + '╮' })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["dir: ", skillsDir()] })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [skills.length, " skill", skills.length === 1 ? '' : 's', " loaded"] })] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), skills.length === 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "no skills found" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: ["create ", skillsDir(), "/<name>/SKILL.md"] })] })] })) : (skills.map((s) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.success, children: '● ' }), _jsx(Text, { color: theme.text, bold: true, children: s.name })] }), s.description && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: s.description.slice(0, inner - 6) })] })), s.triggers.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ triggers: ' }), _jsx(Text, { color: theme.muted, children: s.triggers.slice(0, 6).join(', ') })] }))] }, s.name)))), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] reload \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close \u00B7 use " }), _jsx(Text, { color: theme.accent, children: "/skill <name>" }), _jsx(Text, { color: theme.muted, children: " to force-activate" })] })] }));
|
|
20
|
+
};
|
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);
|