File size: 2,524 Bytes
3046dfd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Persistent memory — the agent remembers across tasks and restarts.
// Plain markdown files under ~/.mona-agent/memory/.

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';

const MEM_DIR = process.env.MONA_MEMORY_DIR || path.join(os.homedir(), '.mona-agent', 'memory');
const MAX_NOTE = 4000;

function ensureDir() {
  fs.mkdirSync(MEM_DIR, { recursive: true });
}

function todayFile() {
  const d = new Date();
  const ymd = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  return path.join(MEM_DIR, `${ymd}.md`);
}

function allFiles() {
  ensureDir();
  return fs.readdirSync(MEM_DIR).filter((f) => f.endsWith('.md')).map((f) => path.join(MEM_DIR, f));
}

export const memory = {
  name: 'memory',
  description: 'Persistent memory across tasks: remember facts, recall/search them, list recent notes.',
  args: {
    action: 'string — remember | recall | list',
    note: 'string — text to remember (for remember)',
    query: 'string — search terms (for recall)',
  },
  platform: 'any',

  async run(args) {
    const action = String(args.action || 'recall').toLowerCase();

    if (action === 'remember') {
      const note = String(args.note || '').trim().slice(0, MAX_NOTE);
      if (!note) return { error: 'note required' };
      ensureDir();
      const stamp = new Date().toISOString();
      fs.appendFileSync(todayFile(), `\n## ${stamp}\n${note}\n`);
      return { ok: true, stored: todayFile(), chars: note.length };
    }

    if (action === 'list') {
      ensureDir();
      const files = allFiles().map((f) => ({
        file: path.basename(f),
        bytes: fs.statSync(f).size,
      }));
      return { files: files.reverse().slice(0, 14) };
    }

    // recall (default): substring search across memory files, newest first
    const query = String(args.query || '').trim();
    const files = allFiles().sort().reverse();
    const hits = [];
    for (const f of files.slice(0, 40)) {
      let content;
      try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
      const lines = content.split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (!query || lines[i].toLowerCase().includes(query.toLowerCase())) {
          hits.push({ file: path.basename(f), line: i + 1, text: lines[i].slice(0, 300) });
          if (hits.length >= 20) break;
        }
      }
      if (hits.length >= 20) break;
    }
    return { query: query || '(all recent)', hits };
  },
};