blyuecode 0.7.0 → 0.7.1
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/ui/AuditPanel.js +78 -0
- package/dist/ui/Chat.js +19 -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
|
}
|
|
@@ -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/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
|
}
|