blyuecode 0.6.1 → 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 +18 -2
- package/dist/core/audit.js +94 -0
- package/dist/core/mcpRegistry.js +9 -0
- package/dist/core/memory.js +63 -0
- package/dist/core/tools.js +30 -0
- package/dist/ui/AuditPanel.js +78 -0
- package/dist/ui/Chat.js +62 -0
- package/dist/ui/MemoryPanel.js +52 -0
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -2,15 +2,20 @@ import { Provider } from './provider.js';
|
|
|
2
2
|
import { getToolDefs, executeTool } from './tools.js';
|
|
3
3
|
import { loadSession, saveSession, clearSession } from './session.js';
|
|
4
4
|
import { formatSkillForPrompt } from './skills.js';
|
|
5
|
+
import { memoryForPrompt } from './memory.js';
|
|
6
|
+
import { auditTool } from './audit.js';
|
|
5
7
|
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
6
8
|
'Use tools to read, write, and run commands. ' +
|
|
7
9
|
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
8
|
-
'Always explain what you are doing. Be concise.'
|
|
10
|
+
'Always explain what you are doing. Be concise. ' +
|
|
11
|
+
'When the user says "remember X", call the remember tool. ' +
|
|
12
|
+
'When you discover a durable fact about the project worth persisting, ' +
|
|
13
|
+
'offer to remember it (do not do so silently).';
|
|
9
14
|
export class Agent {
|
|
10
15
|
provider = null;
|
|
11
16
|
messages;
|
|
12
17
|
constructor(resume = true) {
|
|
13
|
-
const sys =
|
|
18
|
+
const sys = this.buildSystemMessage([]);
|
|
14
19
|
const history = resume ? loadSession() : null;
|
|
15
20
|
if (history && history.length > 0) {
|
|
16
21
|
this.messages = [sys, ...history];
|
|
@@ -32,6 +37,13 @@ export class Agent {
|
|
|
32
37
|
}
|
|
33
38
|
buildSystemMessage(skills) {
|
|
34
39
|
let content = SYSTEM_PROMPT;
|
|
40
|
+
const mem = memoryForPrompt();
|
|
41
|
+
if (mem) {
|
|
42
|
+
content += '\n\n# Project memory\n' +
|
|
43
|
+
'The following notes come from .blyue/MEMORY.md in this project.\n' +
|
|
44
|
+
'Treat them as authoritative context unless contradicted by the user.\n\n' +
|
|
45
|
+
mem;
|
|
46
|
+
}
|
|
35
47
|
if (skills.length > 0) {
|
|
36
48
|
content += '\n\n# Active skills\n';
|
|
37
49
|
for (const sk of skills) {
|
|
@@ -118,6 +130,10 @@ export class Agent {
|
|
|
118
130
|
}
|
|
119
131
|
catch { }
|
|
120
132
|
const result = await executeTool(tc.name, args);
|
|
133
|
+
try {
|
|
134
|
+
auditTool(tc.name, args, result, 0, !result.startsWith('Error:'));
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
121
137
|
this.messages.push({
|
|
122
138
|
role: 'tool',
|
|
123
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,63 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const REL_DIR = '.blyue';
|
|
4
|
+
const FILE = 'MEMORY.md';
|
|
5
|
+
const MAX_INJECT_BYTES = 8 * 1024;
|
|
6
|
+
export function memoryPath(cwd = process.cwd()) {
|
|
7
|
+
return path.join(cwd, REL_DIR, FILE);
|
|
8
|
+
}
|
|
9
|
+
export function memoryExists(cwd = process.cwd()) {
|
|
10
|
+
return fs.existsSync(memoryPath(cwd));
|
|
11
|
+
}
|
|
12
|
+
export function loadMemory(cwd = process.cwd()) {
|
|
13
|
+
const p = memoryPath(cwd);
|
|
14
|
+
if (!fs.existsSync(p))
|
|
15
|
+
return '';
|
|
16
|
+
try {
|
|
17
|
+
return fs.readFileSync(p, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function memoryForPrompt(cwd = process.cwd()) {
|
|
24
|
+
const raw = loadMemory(cwd);
|
|
25
|
+
if (!raw.trim())
|
|
26
|
+
return '';
|
|
27
|
+
if (Buffer.byteLength(raw, 'utf8') <= MAX_INJECT_BYTES)
|
|
28
|
+
return raw;
|
|
29
|
+
return raw.slice(0, MAX_INJECT_BYTES) +
|
|
30
|
+
'\n\n[...memory truncated by BLYUE to fit context...]\n';
|
|
31
|
+
}
|
|
32
|
+
export function saveMemory(text, cwd = process.cwd()) {
|
|
33
|
+
const p = memoryPath(cwd);
|
|
34
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
35
|
+
fs.writeFileSync(p, text, 'utf8');
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
export function appendMemory(text, section = 'Notes', cwd = process.cwd()) {
|
|
39
|
+
const clean = text.trim().replace(/\n+/g, ' ');
|
|
40
|
+
if (!clean)
|
|
41
|
+
return loadMemory(cwd);
|
|
42
|
+
const existing = loadMemory(cwd);
|
|
43
|
+
const bullet = `- ${clean}`;
|
|
44
|
+
const header = `## ${section}`;
|
|
45
|
+
const headerRe = new RegExp(`^${header.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
|
|
46
|
+
let next;
|
|
47
|
+
if (!existing.trim()) {
|
|
48
|
+
next = `# Project memory\n\n${header}\n${bullet}\n`;
|
|
49
|
+
}
|
|
50
|
+
else if (headerRe.test(existing)) {
|
|
51
|
+
next = existing.replace(headerRe, `${header}\n${bullet}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
next = existing.trimEnd() + `\n\n${header}\n${bullet}\n`;
|
|
55
|
+
}
|
|
56
|
+
saveMemory(next, cwd);
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
export function clearMemory(cwd = process.cwd()) {
|
|
60
|
+
const p = memoryPath(cwd);
|
|
61
|
+
if (fs.existsSync(p))
|
|
62
|
+
fs.unlinkSync(p);
|
|
63
|
+
}
|
package/dist/core/tools.js
CHANGED
|
@@ -6,6 +6,7 @@ import { tmpdir } from 'os';
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { hasN8n } from './config.js';
|
|
8
8
|
import { getMcpToolDefsSync, executeMcpTool, isMcpTool } from './mcpRegistry.js';
|
|
9
|
+
import { appendMemory } from './memory.js';
|
|
9
10
|
import { permissions } from './permission.js';
|
|
10
11
|
import { lineDiff, compactDiff } from './diff.js';
|
|
11
12
|
import { isBlocked } from './sandbox.js';
|
|
@@ -77,6 +78,27 @@ const BASE_TOOLS = [
|
|
|
77
78
|
},
|
|
78
79
|
},
|
|
79
80
|
},
|
|
81
|
+
{
|
|
82
|
+
type: 'function',
|
|
83
|
+
function: {
|
|
84
|
+
name: 'remember',
|
|
85
|
+
description: 'Append a note to this project\'s persistent memory (.blyue/MEMORY.md). ' +
|
|
86
|
+
'Use when the user says "remember X", or when you discover a durable fact ' +
|
|
87
|
+
'about the project (conventions, file layout, gotchas) worth persisting ' +
|
|
88
|
+
'across sessions. Do not use for transient task state.',
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
text: { type: 'string', description: 'The note to remember (one line).' },
|
|
93
|
+
section: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Optional section heading (e.g. "Conventions", "Gotchas", "Notes"). Defaults to "Notes".',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ['text'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
80
102
|
];
|
|
81
103
|
const N8N_TOOLS = [
|
|
82
104
|
{ type: 'function', function: { name: 'n8n_list_workflows', description: 'List n8n workflows', parameters: { type: 'object', properties: {} } } },
|
|
@@ -212,6 +234,14 @@ export async function executeTool(name, args) {
|
|
|
212
234
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
213
235
|
return entries.map((e) => (e.isDirectory() ? e.name + '/' : e.name)).join('\n');
|
|
214
236
|
}
|
|
237
|
+
case 'remember': {
|
|
238
|
+
const text = String(args.text || '').trim();
|
|
239
|
+
if (!text)
|
|
240
|
+
return 'Error: remember needs a non-empty text.';
|
|
241
|
+
const section = String(args.section || 'Notes').trim() || 'Notes';
|
|
242
|
+
appendMemory(text, section);
|
|
243
|
+
return 'Remembered (' + section + '): ' + text;
|
|
244
|
+
}
|
|
215
245
|
case 'run_command': {
|
|
216
246
|
const cmd = args.command;
|
|
217
247
|
if (!permissions.isAlwaysAllowed('run_command')) {
|
|
@@ -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
|
@@ -21,6 +21,10 @@ import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
|
21
21
|
import { recordModelUse } from '../core/modelUsage.js';
|
|
22
22
|
import { SkillsPanel } from './SkillsPanel.js';
|
|
23
23
|
import { SkillStorePanel } from './SkillStorePanel.js';
|
|
24
|
+
import { MemoryPanel } from './MemoryPanel.js';
|
|
25
|
+
import { AuditPanel } from './AuditPanel.js';
|
|
26
|
+
import { appendMemory } from '../core/memory.js';
|
|
27
|
+
import { audit } from '../core/audit.js';
|
|
24
28
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
25
29
|
let nextId = 0;
|
|
26
30
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -110,6 +114,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
110
114
|
setOverlay('settings');
|
|
111
115
|
if (key.ctrl && inputChar === 'k')
|
|
112
116
|
setOverlay('skillstore');
|
|
117
|
+
if (key.ctrl && inputChar === 'y')
|
|
118
|
+
setOverlay('memory');
|
|
113
119
|
});
|
|
114
120
|
useEffect(() => { try {
|
|
115
121
|
ensureSkillsDir();
|
|
@@ -140,6 +146,49 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
140
146
|
setOverlay('mcp');
|
|
141
147
|
return;
|
|
142
148
|
}
|
|
149
|
+
if (trimmed.startsWith('/remember ')) {
|
|
150
|
+
setInput('');
|
|
151
|
+
const text = trimmed.slice(10).trim();
|
|
152
|
+
if (!text) {
|
|
153
|
+
setHistory((h) => [
|
|
154
|
+
...h,
|
|
155
|
+
mkItem('error', 'usage: /remember <text> [| section]'),
|
|
156
|
+
]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let section = 'Notes';
|
|
160
|
+
let note = text;
|
|
161
|
+
const pipe = text.indexOf('|');
|
|
162
|
+
if (pipe > 0) {
|
|
163
|
+
note = text.slice(0, pipe).trim();
|
|
164
|
+
section = text.slice(pipe + 1).trim() || 'Notes';
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
appendMemory(note, section);
|
|
168
|
+
try {
|
|
169
|
+
audit('memory_write', { section, note: note.slice(0, 200) });
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
setHistory((h) => [
|
|
173
|
+
...h,
|
|
174
|
+
mkItem('tool', 'remembered (' + section + '): ' + note),
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (trimmed === '/audit') {
|
|
183
|
+
setInput('');
|
|
184
|
+
setOverlay('audit');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (trimmed === '/memory') {
|
|
188
|
+
setInput('');
|
|
189
|
+
setOverlay('memory');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
143
192
|
if (trimmed === '/skillstore') {
|
|
144
193
|
setInput('');
|
|
145
194
|
setOverlay('skillstore');
|
|
@@ -218,6 +267,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
218
267
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
219
268
|
mkItem('tool', ' /skills list skills'),
|
|
220
269
|
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
270
|
+
mkItem('tool', ' /memory view this project\'s persistent memory'),
|
|
271
|
+
mkItem('tool', ' /audit view the local audit log'),
|
|
272
|
+
mkItem('tool', ' /remember <text> [| section] append a note to memory'),
|
|
221
273
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
222
274
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
223
275
|
mkItem('tool', ' /debug show last request to provider'),
|
|
@@ -291,6 +343,10 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
291
343
|
};
|
|
292
344
|
const handleModelSelect = (id) => {
|
|
293
345
|
recordModelUse(id);
|
|
346
|
+
try {
|
|
347
|
+
audit('model_switch', { model: id });
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
294
350
|
process.env.MODEL = id;
|
|
295
351
|
try {
|
|
296
352
|
const cfg = getConfig();
|
|
@@ -313,6 +369,12 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
313
369
|
if (overlay === 'mcp') {
|
|
314
370
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
315
371
|
}
|
|
372
|
+
if (overlay === 'audit') {
|
|
373
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(AuditPanel, { onClose: () => setOverlay('none') }) }));
|
|
374
|
+
}
|
|
375
|
+
if (overlay === 'memory') {
|
|
376
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(MemoryPanel, { onClose: () => setOverlay('none') }) }));
|
|
377
|
+
}
|
|
316
378
|
if (overlay === 'skillstore') {
|
|
317
379
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillStorePanel, { onClose: () => setOverlay('none') }) }));
|
|
318
380
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { loadMemory, memoryPath, clearMemory } from '../core/memory.js';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
export const MemoryPanel = ({ onClose, onChanged }) => {
|
|
7
|
+
const { stdout } = useStdout();
|
|
8
|
+
const width = Math.max(40, stdout?.columns ?? 60);
|
|
9
|
+
const inner = width - 4;
|
|
10
|
+
const [content, setContent] = useState(() => loadMemory());
|
|
11
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
12
|
+
const [status, setStatus] = useState(null);
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (confirmClear) {
|
|
15
|
+
if (input === 'y' || input === 'Y') {
|
|
16
|
+
clearMemory();
|
|
17
|
+
setContent('');
|
|
18
|
+
setConfirmClear(false);
|
|
19
|
+
setStatus('✓ memory cleared');
|
|
20
|
+
onChanged?.();
|
|
21
|
+
}
|
|
22
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
23
|
+
setConfirmClear(false);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (key.escape)
|
|
28
|
+
return onClose();
|
|
29
|
+
if (input === 'r' || input === 'R') {
|
|
30
|
+
setContent(loadMemory());
|
|
31
|
+
setStatus('refreshed');
|
|
32
|
+
setTimeout(() => setStatus(null), 1500);
|
|
33
|
+
}
|
|
34
|
+
if (input === 'c' || input === 'C') {
|
|
35
|
+
setConfirmClear(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const label = 'project memory';
|
|
39
|
+
const topPad = Math.max(1, inner - label.length - 4);
|
|
40
|
+
const path = memoryPath();
|
|
41
|
+
const pathShort = path.length > inner - 4 ? '…' + path.slice(-(inner - 5)) : path;
|
|
42
|
+
const lines = content ? content.split('\n') : [];
|
|
43
|
+
const maxShow = 20;
|
|
44
|
+
const shown = lines.slice(0, maxShow);
|
|
45
|
+
const truncated = lines.length > maxShow;
|
|
46
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
47
|
+
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: [bytes, " bytes \u00B7 ", lines.length, " line", lines.length === 1 ? '' : 's'] }), 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)) }), !content.trim() ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.warning, children: "memory is empty" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: theme.muted, children: "the agent will add notes when you say \"remember ...\"" })] })] })) : (shown.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: line.startsWith('#')
|
|
48
|
+
? theme.accent
|
|
49
|
+
: line.startsWith('-')
|
|
50
|
+
? theme.text
|
|
51
|
+
: theme.muted, bold: line.startsWith('#'), children: line.slice(0, inner - 2) })] }, i)))), truncated && (_jsx(Text, { children: _jsx(Text, { color: theme.muted, children: '│ …+' + (lines.length - maxShow) + ' more lines' }) })), _jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }), confirmClear ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: " clear all project memory? " }), _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 [" }), _jsx(Text, { color: theme.accent, children: "esc" }), _jsx(Text, { color: theme.muted, children: "] close" })] }))] }));
|
|
52
|
+
};
|