blyuecode 0.6.1 → 0.7.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/memory.js +63 -0
- package/dist/core/tools.js +30 -0
- package/dist/ui/Chat.js +43 -0
- package/dist/ui/MemoryPanel.js +52 -0
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -2,15 +2,19 @@ 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';
|
|
5
6
|
const SYSTEM_PROMPT = 'You are BLYUE, a coding assistant running in a terminal. ' +
|
|
6
7
|
'Use tools to read, write, and run commands. ' +
|
|
7
8
|
'If n8n tools are available, you can also manage n8n workflows. ' +
|
|
8
|
-
'Always explain what you are doing. Be concise.'
|
|
9
|
+
'Always explain what you are doing. Be concise. ' +
|
|
10
|
+
'When the user says "remember X", call the remember tool. ' +
|
|
11
|
+
'When you discover a durable fact about the project worth persisting, ' +
|
|
12
|
+
'offer to remember it (do not do so silently).';
|
|
9
13
|
export class Agent {
|
|
10
14
|
provider = null;
|
|
11
15
|
messages;
|
|
12
16
|
constructor(resume = true) {
|
|
13
|
-
const sys =
|
|
17
|
+
const sys = this.buildSystemMessage([]);
|
|
14
18
|
const history = resume ? loadSession() : null;
|
|
15
19
|
if (history && history.length > 0) {
|
|
16
20
|
this.messages = [sys, ...history];
|
|
@@ -32,6 +36,13 @@ export class Agent {
|
|
|
32
36
|
}
|
|
33
37
|
buildSystemMessage(skills) {
|
|
34
38
|
let content = SYSTEM_PROMPT;
|
|
39
|
+
const mem = memoryForPrompt();
|
|
40
|
+
if (mem) {
|
|
41
|
+
content += '\n\n# Project memory\n' +
|
|
42
|
+
'The following notes come from .blyue/MEMORY.md in this project.\n' +
|
|
43
|
+
'Treat them as authoritative context unless contradicted by the user.\n\n' +
|
|
44
|
+
mem;
|
|
45
|
+
}
|
|
35
46
|
if (skills.length > 0) {
|
|
36
47
|
content += '\n\n# Active skills\n';
|
|
37
48
|
for (const sk of skills) {
|
|
@@ -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')) {
|
package/dist/ui/Chat.js
CHANGED
|
@@ -21,6 +21,8 @@ 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 { appendMemory } from '../core/memory.js';
|
|
24
26
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
25
27
|
let nextId = 0;
|
|
26
28
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
@@ -110,6 +112,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
110
112
|
setOverlay('settings');
|
|
111
113
|
if (key.ctrl && inputChar === 'k')
|
|
112
114
|
setOverlay('skillstore');
|
|
115
|
+
if (key.ctrl && inputChar === 'y')
|
|
116
|
+
setOverlay('memory');
|
|
113
117
|
});
|
|
114
118
|
useEffect(() => { try {
|
|
115
119
|
ensureSkillsDir();
|
|
@@ -140,6 +144,40 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
140
144
|
setOverlay('mcp');
|
|
141
145
|
return;
|
|
142
146
|
}
|
|
147
|
+
if (trimmed.startsWith('/remember ')) {
|
|
148
|
+
setInput('');
|
|
149
|
+
const text = trimmed.slice(10).trim();
|
|
150
|
+
if (!text) {
|
|
151
|
+
setHistory((h) => [
|
|
152
|
+
...h,
|
|
153
|
+
mkItem('error', 'usage: /remember <text> [| section]'),
|
|
154
|
+
]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let section = 'Notes';
|
|
158
|
+
let note = text;
|
|
159
|
+
const pipe = text.indexOf('|');
|
|
160
|
+
if (pipe > 0) {
|
|
161
|
+
note = text.slice(0, pipe).trim();
|
|
162
|
+
section = text.slice(pipe + 1).trim() || 'Notes';
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
appendMemory(note, section);
|
|
166
|
+
setHistory((h) => [
|
|
167
|
+
...h,
|
|
168
|
+
mkItem('tool', 'remembered (' + section + '): ' + note),
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (trimmed === '/memory') {
|
|
177
|
+
setInput('');
|
|
178
|
+
setOverlay('memory');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
143
181
|
if (trimmed === '/skillstore') {
|
|
144
182
|
setInput('');
|
|
145
183
|
setOverlay('skillstore');
|
|
@@ -218,6 +256,8 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
218
256
|
mkItem('tool', ' /mcp show MCP servers and tools'),
|
|
219
257
|
mkItem('tool', ' /skills list skills'),
|
|
220
258
|
mkItem('tool', ' /skillstore browse and install community skills'),
|
|
259
|
+
mkItem('tool', ' /memory view this project\'s persistent memory'),
|
|
260
|
+
mkItem('tool', ' /remember <text> [| section] append a note to memory'),
|
|
221
261
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
222
262
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
223
263
|
mkItem('tool', ' /debug show last request to provider'),
|
|
@@ -313,6 +353,9 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
313
353
|
if (overlay === 'mcp') {
|
|
314
354
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(McpPanel, { onClose: () => setOverlay('none') }) }));
|
|
315
355
|
}
|
|
356
|
+
if (overlay === 'memory') {
|
|
357
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(MemoryPanel, { onClose: () => setOverlay('none') }) }));
|
|
358
|
+
}
|
|
316
359
|
if (overlay === 'skillstore') {
|
|
317
360
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(SkillStorePanel, { onClose: () => setOverlay('none') }) }));
|
|
318
361
|
}
|
|
@@ -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
|
+
};
|