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