blyuecode 0.7.0 → 0.7.2
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 +5 -0
- package/dist/core/audit.js +94 -0
- package/dist/core/mcpRegistry.js +9 -0
- package/dist/index.js +36 -5
- package/dist/ui/AuditPanel.js +78 -0
- package/dist/ui/Banner.js +10 -3
- package/dist/ui/Chat.js +19 -0
- package/dist/ui/SplashBanner.js +119 -0
- package/dist/ui/splash.js +121 -0
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getToolDefs, executeTool } from './tools.js';
|
|
|
3
3
|
import { loadSession, saveSession, clearSession } from './session.js';
|
|
4
4
|
import { formatSkillForPrompt } from './skills.js';
|
|
5
5
|
import { memoryForPrompt } from './memory.js';
|
|
6
|
+
import { auditTool } from './audit.js';
|
|
6
7
|
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
7
8
|
'Use tools to read, write, and run commands. ' +
|
|
8
9
|
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
@@ -129,6 +130,10 @@ export class Agent {
|
|
|
129
130
|
}
|
|
130
131
|
catch { }
|
|
131
132
|
const result = await executeTool(tc.name, args);
|
|
133
|
+
try {
|
|
134
|
+
auditTool(tc.name, args, result, 0, !result.startsWith('Error:'));
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
132
137
|
this.messages.push({
|
|
133
138
|
role: 'tool',
|
|
134
139
|
tool_call_id: tc.id,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const DIR = path.join(os.homedir(), '.blyue');
|
|
5
|
+
const FILE = path.join(DIR, 'audit.jsonl');
|
|
6
|
+
const ROTATE_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
const KEEP_ROTATIONS = 3;
|
|
8
|
+
let disabled = false;
|
|
9
|
+
if (process.env.BLYUE_NO_AUDIT === '1')
|
|
10
|
+
disabled = true;
|
|
11
|
+
function rotateIfNeeded() {
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(FILE))
|
|
14
|
+
return;
|
|
15
|
+
const st = fs.statSync(FILE);
|
|
16
|
+
if (st.size < ROTATE_BYTES)
|
|
17
|
+
return;
|
|
18
|
+
for (let i = KEEP_ROTATIONS - 1; i >= 1; i--) {
|
|
19
|
+
const from = path.join(DIR, `audit.${i}.jsonl`);
|
|
20
|
+
const to = path.join(DIR, `audit.${i + 1}.jsonl`);
|
|
21
|
+
if (fs.existsSync(from))
|
|
22
|
+
fs.renameSync(from, to);
|
|
23
|
+
}
|
|
24
|
+
fs.renameSync(FILE, path.join(DIR, 'audit.1.jsonl'));
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
function truncate(s, n) {
|
|
29
|
+
const str = typeof s === 'string' ? s : JSON.stringify(s);
|
|
30
|
+
if (str == null)
|
|
31
|
+
return '';
|
|
32
|
+
return str.length > n ? str.slice(0, n) + '…' : str;
|
|
33
|
+
}
|
|
34
|
+
export function audit(kind, data = {}) {
|
|
35
|
+
if (disabled)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(DIR))
|
|
39
|
+
fs.mkdirSync(DIR, { recursive: true });
|
|
40
|
+
rotateIfNeeded();
|
|
41
|
+
const entry = {
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
kind,
|
|
44
|
+
...data,
|
|
45
|
+
};
|
|
46
|
+
fs.appendFileSync(FILE, JSON.stringify(entry) + '\n', 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function auditTool(name, args, resultPreview, durationMs, ok) {
|
|
52
|
+
audit('tool_call', {
|
|
53
|
+
name,
|
|
54
|
+
args: truncate(args, 400),
|
|
55
|
+
result: truncate(resultPreview, 400),
|
|
56
|
+
ms: durationMs,
|
|
57
|
+
ok,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function auditFile() {
|
|
61
|
+
return FILE;
|
|
62
|
+
}
|
|
63
|
+
export function readAudit(opts = {}) {
|
|
64
|
+
const limit = opts.limit ?? 200;
|
|
65
|
+
if (!fs.existsSync(FILE))
|
|
66
|
+
return [];
|
|
67
|
+
let text;
|
|
68
|
+
try {
|
|
69
|
+
text = fs.readFileSync(FILE, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const lines = text.split('\n').filter(Boolean);
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = lines.length - 1; i >= 0 && out.length < limit; i--) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(line);
|
|
80
|
+
if (opts.kind && parsed.kind !== opts.kind)
|
|
81
|
+
continue;
|
|
82
|
+
out.push({ ts: parsed.ts, kind: parsed.kind, raw: line });
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export function clearAudit() {
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(FILE))
|
|
91
|
+
fs.unlinkSync(FILE);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
package/dist/core/mcpRegistry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpClient, loadMcpConfig, } from './mcp.js';
|
|
2
2
|
import { permissions } from './permission.js';
|
|
3
|
+
import { audit } from './audit.js';
|
|
3
4
|
const servers = new Map();
|
|
4
5
|
let cachedDefs = [];
|
|
5
6
|
let initPromise = null;
|
|
@@ -27,6 +28,10 @@ export async function initMcpRegistry() {
|
|
|
27
28
|
await client.connect();
|
|
28
29
|
const tools = await client.listTools();
|
|
29
30
|
servers.set(name, { client, tools });
|
|
31
|
+
try {
|
|
32
|
+
audit('mcp_connect', { server: name, tools: tools.length });
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
30
35
|
}
|
|
31
36
|
catch (err) {
|
|
32
37
|
servers.set(name, {
|
|
@@ -34,6 +39,10 @@ export async function initMcpRegistry() {
|
|
|
34
39
|
tools: [],
|
|
35
40
|
error: err.message,
|
|
36
41
|
});
|
|
42
|
+
try {
|
|
43
|
+
audit('mcp_error', { server: name, error: err.message });
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
37
46
|
try {
|
|
38
47
|
await client.close();
|
|
39
48
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { render, Box, Static } from 'ink';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
5
8
|
import { Setup } from './ui/Setup.js';
|
|
6
9
|
import { Chat } from './ui/Chat.js';
|
|
7
10
|
import { Banner } from './ui/Banner.js';
|
|
@@ -9,6 +12,24 @@ import { Consent, hasConsented } from './ui/Consent.js';
|
|
|
9
12
|
import { loadConfig, applyConfig } from './core/config.js';
|
|
10
13
|
import { loadSession } from './core/session.js';
|
|
11
14
|
import { initMcpRegistry } from './core/mcpRegistry.js';
|
|
15
|
+
import { playSplash } from './ui/splash.js';
|
|
16
|
+
function getVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'))
|
|
20
|
+
.version || '0.0.0';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '0.0.0';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function splashWanted() {
|
|
27
|
+
if (process.env.BLYUE_NO_SPLASH === '1')
|
|
28
|
+
return false;
|
|
29
|
+
if (process.env.BLYUE_STATIC === '1')
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
12
33
|
const App = () => {
|
|
13
34
|
const [ready, setReady] = useState(false);
|
|
14
35
|
const [resumedCount, setResumedCount] = useState(0);
|
|
@@ -19,15 +40,25 @@ const App = () => {
|
|
|
19
40
|
const existing = loadConfig();
|
|
20
41
|
if (existing) {
|
|
21
42
|
applyConfig(existing);
|
|
22
|
-
void initMcpRegistry();
|
|
23
43
|
const hist = loadSession();
|
|
24
44
|
setResumedCount(hist ? hist.length : 0);
|
|
25
45
|
setReady(true);
|
|
26
46
|
}
|
|
27
47
|
}, [consented]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!consented)
|
|
50
|
+
return;
|
|
51
|
+
void initMcpRegistry();
|
|
52
|
+
}, [consented]);
|
|
28
53
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Static, { items: ['banner'], children: () => _jsx(Banner, {}, "banner") }), !consented ? (_jsx(Consent, { onAccept: () => setConsented(true) })) : ready ? (_jsx(Chat, { resumedCount: resumedCount })) : (_jsx(Setup, { onDone: () => setReady(true) }))] }));
|
|
29
54
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
55
|
+
async function main() {
|
|
56
|
+
if (splashWanted()) {
|
|
57
|
+
await playSplash(getVersion());
|
|
58
|
+
}
|
|
59
|
+
render(_jsx(App, {}), {
|
|
60
|
+
patchConsole: false,
|
|
61
|
+
exitOnCtrlC: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
void main();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { readAudit, clearAudit, auditFile } from '../core/audit.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const AuditPanel = ({ onClose }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [entries, setEntries] = useState(() => readAudit({ limit: 200 }));
|
|
11
|
+
const [filter, setFilter] = useState('');
|
|
12
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
13
|
+
const [status, setStatus] = useState(null);
|
|
14
|
+
const filtered = useMemo(() => {
|
|
15
|
+
if (!filter)
|
|
16
|
+
return entries;
|
|
17
|
+
const q = filter.toLowerCase();
|
|
18
|
+
return entries.filter((e) => e.raw.toLowerCase().includes(q));
|
|
19
|
+
}, [entries, filter]);
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (confirmClear) {
|
|
22
|
+
if (input === 'y' || input === 'Y') {
|
|
23
|
+
clearAudit();
|
|
24
|
+
setEntries([]);
|
|
25
|
+
setConfirmClear(false);
|
|
26
|
+
setStatus('cleared');
|
|
27
|
+
setTimeout(() => setStatus(null), 1500);
|
|
28
|
+
}
|
|
29
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
30
|
+
setConfirmClear(false);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.escape)
|
|
35
|
+
return onClose();
|
|
36
|
+
if (input === 'r' || input === 'R') {
|
|
37
|
+
setEntries(readAudit({ limit: 200 }));
|
|
38
|
+
setStatus('refreshed');
|
|
39
|
+
setTimeout(() => setStatus(null), 1200);
|
|
40
|
+
}
|
|
41
|
+
if (input === 'c' || input === 'C')
|
|
42
|
+
setConfirmClear(true);
|
|
43
|
+
if (key.backspace || key.delete)
|
|
44
|
+
setFilter((f) => f.slice(0, -1));
|
|
45
|
+
if (input && !key.ctrl && !key.meta) {
|
|
46
|
+
const clean = input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
47
|
+
if (clean && clean !== 'r' && clean !== 'c')
|
|
48
|
+
setFilter((f) => f + clean);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const label = filter ? `audit · filter: ${filter}` : 'audit log';
|
|
52
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
53
|
+
const pathShort = (() => {
|
|
54
|
+
const p = auditFile();
|
|
55
|
+
return p.length > inner - 4 ? '…' + p.slice(-(inner - 5)) : p;
|
|
56
|
+
})();
|
|
57
|
+
const maxRows = 16;
|
|
58
|
+
const visible = filtered.slice(0, maxRows);
|
|
59
|
+
const colorFor = (kind) => kind === 'tool_call'
|
|
60
|
+
? theme.text
|
|
61
|
+
: kind === 'permission'
|
|
62
|
+
? theme.warning
|
|
63
|
+
: kind === 'mcp_error'
|
|
64
|
+
? theme.error
|
|
65
|
+
: kind === 'model_switch'
|
|
66
|
+
? theme.accent
|
|
67
|
+
: theme.muted;
|
|
68
|
+
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: '│ ' }), _jsx(Text, { color: theme.muted, children: pathShort })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsxs(Text, { color: theme.muted, children: [entries.length, " entries", filter ? ` · ${filtered.length} match` : ''] }), status && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.success, children: status })] }))] }), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), visible.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: "no entries" })] })) : (visible.map((e, i) => {
|
|
69
|
+
let preview = e.raw;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(e.raw);
|
|
72
|
+
const { ts, kind, ...rest } = parsed;
|
|
73
|
+
preview = `${String(ts).slice(11, 19)} ${kind} ${JSON.stringify(rest).slice(0, inner - 30)}`;
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: colorFor(e.kind), children: preview })] }, i));
|
|
77
|
+
})), filtered.length > maxRows && (_jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '│ …+' + (filtered.length - maxRows) + ' older entries' }) })), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), confirmClear ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: " delete the audit log? " }), _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" })] })) : (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: " [" }), _jsx(Text, { color: theme.accent, children: "r" }), _jsx(Text, { color: theme.muted, children: "] refresh \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "c" }), _jsx(Text, { color: theme.muted, children: "] clear \u00B7 type to filter \u00B7 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close" })] }))] }));
|
|
78
|
+
};
|
package/dist/ui/Banner.js
CHANGED
|
@@ -63,7 +63,9 @@ const CODE_SMALL = [
|
|
|
63
63
|
];
|
|
64
64
|
const BLYUE_WIDTH = 44;
|
|
65
65
|
const BLYUE_SMALL_WIDTH = 27;
|
|
66
|
-
const
|
|
66
|
+
const LUMEN_COL_BIG = 35;
|
|
67
|
+
const LUMEN_COL_SMALL = 9;
|
|
68
|
+
const LUMEN_TEXT = 'L U M E N';
|
|
67
69
|
export const Banner = () => {
|
|
68
70
|
const { stdout } = useStdout();
|
|
69
71
|
const width = stdout?.columns ?? 80;
|
|
@@ -71,6 +73,7 @@ export const Banner = () => {
|
|
|
71
73
|
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
72
74
|
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
73
75
|
const blockWidth = wide ? BLYUE_WIDTH : BLYUE_SMALL_WIDTH;
|
|
76
|
+
const lumenCol = wide ? LUMEN_COL_BIG : LUMEN_COL_SMALL;
|
|
74
77
|
const leftPad = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
75
78
|
const pad = ' '.repeat(leftPad);
|
|
76
79
|
const [animate] = useState(() => shouldAnimate());
|
|
@@ -83,6 +86,10 @@ export const Banner = () => {
|
|
|
83
86
|
}, [animate]);
|
|
84
87
|
const baseHue = animate ? t : 0.08;
|
|
85
88
|
const brand = pastelAt(baseHue);
|
|
86
|
-
const
|
|
87
|
-
|
|
89
|
+
const versionText = 'v' + getVersion();
|
|
90
|
+
const lumenStart = leftPad + lumenCol;
|
|
91
|
+
const lumenEnd = lumenStart + LUMEN_TEXT.length;
|
|
92
|
+
const versionStart = Math.max(lumenEnd + 2, width - versionText.length - 1);
|
|
93
|
+
const gap = Math.max(1, versionStart - lumenEnd);
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [' '.repeat(lumenStart), _jsx(Text, { color: brand, bold: true, children: LUMEN_TEXT }), ' '.repeat(gap), _jsx(Text, { color: "gray", children: versionText })] }), _jsx(Text, { children: " " }), blyue.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035) % 1), bold: true, children: pad + line }, 'b' + i))), code.map((line, i) => (_jsx(Text, { color: pastelAt((baseHue + i * 0.035 + 0.18) % 1), bold: true, children: pad + line }, 'c' + i)))] }));
|
|
88
95
|
};
|
package/dist/ui/Chat.js
CHANGED
|
@@ -22,7 +22,9 @@ import { recordModelUse } from '../core/modelUsage.js';
|
|
|
22
22
|
import { SkillsPanel } from './SkillsPanel.js';
|
|
23
23
|
import { SkillStorePanel } from './SkillStorePanel.js';
|
|
24
24
|
import { MemoryPanel } from './MemoryPanel.js';
|
|
25
|
+
import { AuditPanel } from './AuditPanel.js';
|
|
25
26
|
import { appendMemory } from '../core/memory.js';
|
|
27
|
+
import { audit } from '../core/audit.js';
|
|
26
28
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
27
29
|
let nextId = 0;
|
|
28
30
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -163,6 +165,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
163
165
|
}
|
|
164
166
|
try {
|
|
165
167
|
appendMemory(note, section);
|
|
168
|
+
try {
|
|
169
|
+
audit('memory_write', { section, note: note.slice(0, 200) });
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
166
172
|
setHistory((h) => [
|
|
167
173
|
...h,
|
|
168
174
|
mkItem('tool', 'remembered (' + section + '): ' + note),
|
|
@@ -173,6 +179,11 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
173
179
|
}
|
|
174
180
|
return;
|
|
175
181
|
}
|
|
182
|
+
if (trimmed === '/audit') {
|
|
183
|
+
setInput('');
|
|
184
|
+
setOverlay('audit');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
176
187
|
if (trimmed === '/memory') {
|
|
177
188
|
setInput('');
|
|
178
189
|
setOverlay('memory');
|
|
@@ -257,6 +268,7 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
257
268
|
mkItem('tool', ' /skills list skills'),
|
|
258
269
|
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
259
270
|
mkItem('tool', ' /memory view this project\'s persistent memory'),
|
|
271
|
+
mkItem('tool', ' /audit view the local audit log'),
|
|
260
272
|
mkItem('tool', ' /remember <text> [| section] append a note to memory'),
|
|
261
273
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
262
274
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
@@ -331,6 +343,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
331
343
|
};
|
|
332
344
|
const handleModelSelect = (id) => {
|
|
333
345
|
recordModelUse(id);
|
|
346
|
+
try {
|
|
347
|
+
audit('model_switch', { model: id });
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
334
350
|
process.env.MODEL = id;
|
|
335
351
|
try {
|
|
336
352
|
const cfg = getConfig();
|
|
@@ -353,6 +369,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
353
369
|
if (overlay === 'mcp') {
|
|
354
370
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
355
371
|
}
|
|
372
|
+
if (overlay === 'audit') {
|
|
373
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(AuditPanel, { onClose: () => setOverlay('none') }) }));
|
|
374
|
+
}
|
|
356
375
|
if (overlay === 'memory') {
|
|
357
376
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(MemoryPanel, { onClose: () => setOverlay('none') }) }));
|
|
358
377
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { pastelAt } from './pastel.js';
|
|
5
|
+
const BLYUE_BIG = [
|
|
6
|
+
'██████╗ ██╗ ██╗ ██╗ ██╗ ██╗ ███████╗',
|
|
7
|
+
'██╔══██╗██║ ╚██╗ ██╔╝ ██║ ██║ ██╔════╝',
|
|
8
|
+
'██████╔╝██║ ╚████╔╝ ██║ ██║ █████╗ ',
|
|
9
|
+
'██╔══██╗██║ ╚██╔╝ ██║ ██║ ██╔══╝ ',
|
|
10
|
+
'██████╔╝███████╗ ██║ ╚██████╔╝ ███████╗',
|
|
11
|
+
'╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚══════╝',
|
|
12
|
+
];
|
|
13
|
+
const BLYUE_SMALL = [
|
|
14
|
+
' ___ _ _ _ _ _ ___ ',
|
|
15
|
+
'| _ )| | | | | | | | | __|',
|
|
16
|
+
'| _ \\| |__| |_| | |_| | _| ',
|
|
17
|
+
'|___/|____|\\___/ \\___/|___|',
|
|
18
|
+
];
|
|
19
|
+
const CODE_BIG = [
|
|
20
|
+
' ██████╗ ██████╗ ██████╗ ███████╗',
|
|
21
|
+
' ██╔════╝██╔═══██╗██╔══██╗██╔════╝',
|
|
22
|
+
' ██║ ██║ ██║██║ ██║█████╗ ',
|
|
23
|
+
' ██║ ██║ ██║██║ ██║██╔══╝ ',
|
|
24
|
+
' ╚██████╗╚██████╔╝██████╔╝███████╗',
|
|
25
|
+
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
|
|
26
|
+
];
|
|
27
|
+
const CODE_SMALL = [
|
|
28
|
+
' █▀▀ █▀█ █▀▄ █▀▀',
|
|
29
|
+
' █▄▄ █▄█ █▄▀ ██▄',
|
|
30
|
+
];
|
|
31
|
+
const BLYUE_WIDTH = 44;
|
|
32
|
+
const BLYUE_SMALL_WIDTH = 27;
|
|
33
|
+
const LUMEN_COL_BIG = 35;
|
|
34
|
+
const LUMEN_COL_SMALL = 9;
|
|
35
|
+
const LUMEN_LETTERS = ['L', 'U', 'M', 'E', 'N'];
|
|
36
|
+
const UNLIT = '#1c1c1c';
|
|
37
|
+
const FRAME_MS = 55;
|
|
38
|
+
const T_LUMEN_IN_START = 3;
|
|
39
|
+
const T_LUMEN_IN_END = 7;
|
|
40
|
+
const T_LUMEN_HOLD_END = 13;
|
|
41
|
+
const T_LUMEN_OUT_START = 14;
|
|
42
|
+
const T_LUMEN_OUT_END = 18;
|
|
43
|
+
const T_GAP_END = 21;
|
|
44
|
+
const T_BANNER_IN_START = 22;
|
|
45
|
+
const T_BANNER_IN_END = 33;
|
|
46
|
+
const T_HOLD_END = 39;
|
|
47
|
+
export const SplashBanner = ({ onDone, version }) => {
|
|
48
|
+
const { stdout } = useStdout();
|
|
49
|
+
const width = stdout?.columns ?? 80;
|
|
50
|
+
const wide = width >= 60;
|
|
51
|
+
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
52
|
+
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
53
|
+
const blockWidth = wide ? BLYUE_WIDTH : BLYUE_SMALL_WIDTH;
|
|
54
|
+
const lumenCol = wide ? LUMEN_COL_BIG : LUMEN_COL_SMALL;
|
|
55
|
+
const leftPad = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
56
|
+
const pad = ' '.repeat(leftPad);
|
|
57
|
+
const lumenStart = leftPad + lumenCol;
|
|
58
|
+
const LUMEN_TEXT_LEN = 9;
|
|
59
|
+
const totalLetterRows = blyue.length + code.length;
|
|
60
|
+
const [frame, setFrame] = useState(0);
|
|
61
|
+
const onDoneRef = useRef(onDone);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
onDoneRef.current = onDone;
|
|
64
|
+
}, [onDone]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const int = setInterval(() => {
|
|
67
|
+
setFrame((f) => {
|
|
68
|
+
const nf = f + 1;
|
|
69
|
+
if (nf > T_HOLD_END) {
|
|
70
|
+
clearInterval(int);
|
|
71
|
+
setTimeout(() => onDoneRef.current(), 80);
|
|
72
|
+
return T_HOLD_END;
|
|
73
|
+
}
|
|
74
|
+
return nf;
|
|
75
|
+
});
|
|
76
|
+
}, FRAME_MS);
|
|
77
|
+
return () => clearInterval(int);
|
|
78
|
+
}, []);
|
|
79
|
+
const lumenLetterLit = (i) => {
|
|
80
|
+
if (frame < T_LUMEN_IN_START)
|
|
81
|
+
return false;
|
|
82
|
+
if (frame <= T_LUMEN_IN_END)
|
|
83
|
+
return frame >= T_LUMEN_IN_START + i;
|
|
84
|
+
if (frame <= T_LUMEN_HOLD_END)
|
|
85
|
+
return true;
|
|
86
|
+
if (frame <= T_LUMEN_OUT_END)
|
|
87
|
+
return frame < T_LUMEN_OUT_START + i;
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
const bannerRowLit = (rowIdx) => {
|
|
91
|
+
if (frame < T_BANNER_IN_START)
|
|
92
|
+
return false;
|
|
93
|
+
if (frame <= T_BANNER_IN_END)
|
|
94
|
+
return frame >= T_BANNER_IN_START + rowIdx;
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
const versionVisible = frame > T_BANNER_IN_END;
|
|
98
|
+
const renderLumenLetters = () => {
|
|
99
|
+
const out = [];
|
|
100
|
+
for (let i = 0; i < 5; i++) {
|
|
101
|
+
const lit = lumenLetterLit(i);
|
|
102
|
+
const hue = 0.3 + (((frame * 0.02) + i * 0.06) % 0.45);
|
|
103
|
+
out.push(_jsx(Text, { color: lit ? pastelAt(hue) : 'gray', bold: lit, dimColor: !lit, children: lit ? LUMEN_LETTERS[i] : ' ' }, 'L' + i));
|
|
104
|
+
if (i < 4)
|
|
105
|
+
out.push(_jsx(Text, { children: " " }, 'sp' + i));
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
const renderBannerRow = (line, rowIdx) => {
|
|
110
|
+
const lit = bannerRowLit(rowIdx);
|
|
111
|
+
const hue = (rowIdx * 0.045) % 1;
|
|
112
|
+
return (_jsx(Text, { color: lit ? pastelAt(hue) : UNLIT, bold: lit, children: pad + line }, 'r' + rowIdx));
|
|
113
|
+
};
|
|
114
|
+
const versionText = 'v' + version;
|
|
115
|
+
const lumenEnd = lumenStart + LUMEN_TEXT_LEN;
|
|
116
|
+
const versionStart = Math.max(lumenEnd + 2, width - versionText.length - 1);
|
|
117
|
+
const gap = Math.max(1, versionStart - lumenEnd);
|
|
118
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: ' '.repeat(lumenStart) }), renderLumenLetters(), _jsx(Text, { children: ' '.repeat(gap) }), versionVisible ? (_jsx(Text, { color: "gray", children: versionText })) : (_jsx(Text, { children: " " }))] }), _jsx(Text, { children: " " }), blyue.map((line, i) => renderBannerRow(line, i)), code.map((line, i) => renderBannerRow(line, blyue.length + i))] }));
|
|
119
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { pastelAt } from './pastel.js';
|
|
2
|
+
const BLYUE_BIG = [
|
|
3
|
+
'██████╗ ██╗ ██╗ ██╗ ██╗ ██╗ ███████╗',
|
|
4
|
+
'██╔══██╗██║ ╚██╗ ██╔╝ ██║ ██║ ██╔════╝',
|
|
5
|
+
'██████╔╝██║ ╚████╔╝ ██║ ██║ █████╗ ',
|
|
6
|
+
'██╔══██╗██║ ╚██╔╝ ██║ ██║ ██╔══╝ ',
|
|
7
|
+
'██████╔╝███████╗ ██║ ╚██████╔╝ ███████╗',
|
|
8
|
+
'╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚══════╝',
|
|
9
|
+
];
|
|
10
|
+
const BLYUE_SMALL = [
|
|
11
|
+
' ___ _ _ _ _ _ ___ ',
|
|
12
|
+
'| _ )| | | | | | | | | __|',
|
|
13
|
+
'| _ \\| |__| |_| | |_| | _| ',
|
|
14
|
+
'|___/|____|\\___/ \\___/|___|',
|
|
15
|
+
];
|
|
16
|
+
const CODE_BIG = [
|
|
17
|
+
' ██████╗ ██████╗ ██████╗ ███████╗',
|
|
18
|
+
' ██╔════╝██╔═══██╗██╔══██╗██╔════╝',
|
|
19
|
+
' ██║ ██║ ██║██║ ██║█████╗ ',
|
|
20
|
+
' ██║ ██║ ██║██║ ██║██╔══╝ ',
|
|
21
|
+
' ╚██████╗╚██████╔╝██████╔╝███████╗',
|
|
22
|
+
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
|
|
23
|
+
];
|
|
24
|
+
const CODE_SMALL = [
|
|
25
|
+
' █▀▀ █▀█ █▀▄ █▀▀',
|
|
26
|
+
' █▄▄ █▄█ █▄▀ ██▄',
|
|
27
|
+
];
|
|
28
|
+
const LETTERS = ['L', 'U', 'M', 'E', 'N'];
|
|
29
|
+
const RESET = '\x1b[0m';
|
|
30
|
+
const BOLD = '\x1b[1m';
|
|
31
|
+
const DIM = '\x1b[2m';
|
|
32
|
+
const HOME_CLEAR = '\x1b[2J\x1b[H';
|
|
33
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
34
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
35
|
+
function hexToAnsi(hex) {
|
|
36
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
37
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
38
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
39
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
40
|
+
}
|
|
41
|
+
const FRAME_MS = 55;
|
|
42
|
+
const T_LUMEN_IN_START = 3;
|
|
43
|
+
const T_LUMEN_IN_END = 7;
|
|
44
|
+
const T_BANNER_IN_START = 12;
|
|
45
|
+
const T_BANNER_IN_END = 23;
|
|
46
|
+
const T_HOLD_END = 31;
|
|
47
|
+
function lumenLetterLit(frame, i) {
|
|
48
|
+
if (frame < T_LUMEN_IN_START)
|
|
49
|
+
return false;
|
|
50
|
+
if (frame <= T_LUMEN_IN_END)
|
|
51
|
+
return frame >= T_LUMEN_IN_START + i;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function bannerRowLit(frame, i) {
|
|
55
|
+
if (frame < T_BANNER_IN_START)
|
|
56
|
+
return false;
|
|
57
|
+
if (frame <= T_BANNER_IN_END)
|
|
58
|
+
return frame >= T_BANNER_IN_START + i;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function stripAnsi(s) {
|
|
62
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
63
|
+
}
|
|
64
|
+
function buildFrame(frame, cols, version) {
|
|
65
|
+
const wide = cols >= 60;
|
|
66
|
+
const blyue = wide ? BLYUE_BIG : BLYUE_SMALL;
|
|
67
|
+
const code = wide ? CODE_BIG : CODE_SMALL;
|
|
68
|
+
const blockWidth = wide ? 44 : 27;
|
|
69
|
+
const lumenCol = wide ? 35 : 9;
|
|
70
|
+
const leftPad = Math.max(0, Math.floor((cols - blockWidth) / 2));
|
|
71
|
+
const pad = ' '.repeat(leftPad);
|
|
72
|
+
const out = [];
|
|
73
|
+
const lumenStart = leftPad + lumenCol;
|
|
74
|
+
let lumenRow = ' '.repeat(lumenStart);
|
|
75
|
+
for (let i = 0; i < LETTERS.length; i++) {
|
|
76
|
+
const lit = lumenLetterLit(frame, i);
|
|
77
|
+
if (lit) {
|
|
78
|
+
const hue = 0.3 + ((frame * 0.02 + i * 0.06) % 0.45);
|
|
79
|
+
lumenRow += BOLD + hexToAnsi(pastelAt(hue)) + LETTERS[i] + RESET;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
lumenRow += ' ';
|
|
83
|
+
}
|
|
84
|
+
if (i < LETTERS.length - 1)
|
|
85
|
+
lumenRow += ' ';
|
|
86
|
+
}
|
|
87
|
+
const versionText = 'v' + version;
|
|
88
|
+
const versionStart = Math.max(lumenStart + 9 + 2, cols - versionText.length - 1);
|
|
89
|
+
const visibleLen = stripAnsi(lumenRow).length;
|
|
90
|
+
lumenRow += ' '.repeat(Math.max(1, versionStart - visibleLen));
|
|
91
|
+
if (frame > T_BANNER_IN_END)
|
|
92
|
+
lumenRow += DIM + versionText + RESET;
|
|
93
|
+
out.push(lumenRow);
|
|
94
|
+
out.push('');
|
|
95
|
+
const rows = [...blyue, ...code];
|
|
96
|
+
for (let i = 0; i < rows.length; i++) {
|
|
97
|
+
const lit = bannerRowLit(frame, i);
|
|
98
|
+
const hue = (i * 0.045) % 1;
|
|
99
|
+
const color = lit ? BOLD + hexToAnsi(pastelAt(hue)) : hexToAnsi('#1c1c1c');
|
|
100
|
+
out.push(color + pad + rows[i] + RESET);
|
|
101
|
+
}
|
|
102
|
+
return out.join('\n');
|
|
103
|
+
}
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
106
|
+
}
|
|
107
|
+
export async function playSplash(version) {
|
|
108
|
+
const cols = process.stdout.columns || 80;
|
|
109
|
+
process.stdout.write(HIDE_CURSOR);
|
|
110
|
+
try {
|
|
111
|
+
for (let frame = 0; frame <= T_HOLD_END; frame++) {
|
|
112
|
+
process.stdout.write(HOME_CLEAR + buildFrame(frame, cols, version));
|
|
113
|
+
await sleep(FRAME_MS);
|
|
114
|
+
}
|
|
115
|
+
await sleep(150);
|
|
116
|
+
process.stdout.write(HOME_CLEAR);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
process.stdout.write(SHOW_CURSOR);
|
|
120
|
+
}
|
|
121
|
+
}
|