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.
@@ -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
+ }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blyuecode",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "BLYUE Code — a coding agent by Lumen",
5
5
  "type": "module",
6
6
  "license": "MIT",