@lmaksym/agent-mem 0.1.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/.claude/commands/context.md +24 -0
- package/.claude/skills/agent-mem/SKILL.md +66 -0
- package/.claude/skills/agent-mem/references/branching-merging.md +34 -0
- package/.claude/skills/agent-mem/references/coexistence.md +19 -0
- package/.claude/skills/agent-mem/references/collaboration.md +33 -0
- package/.claude/skills/agent-mem/references/reflection-compaction.md +104 -0
- package/.claude/skills/agent-mem/references/sub-agent-patterns.md +60 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/bin/agent-context.js +95 -0
- package/bin/parse-args.js +85 -0
- package/package.json +58 -0
- package/src/commands/branch.js +57 -0
- package/src/commands/branch.test.js +91 -0
- package/src/commands/branches.js +34 -0
- package/src/commands/commit.js +55 -0
- package/src/commands/compact.js +307 -0
- package/src/commands/compact.test.js +110 -0
- package/src/commands/config.js +47 -0
- package/src/commands/core.test.js +166 -0
- package/src/commands/diff.js +157 -0
- package/src/commands/diff.test.js +64 -0
- package/src/commands/forget.js +77 -0
- package/src/commands/forget.test.js +68 -0
- package/src/commands/help.js +99 -0
- package/src/commands/import.js +83 -0
- package/src/commands/init.js +269 -0
- package/src/commands/init.test.js +80 -0
- package/src/commands/lesson.js +95 -0
- package/src/commands/lesson.test.js +93 -0
- package/src/commands/merge.js +105 -0
- package/src/commands/pin.js +34 -0
- package/src/commands/pull.js +80 -0
- package/src/commands/push.js +80 -0
- package/src/commands/read.js +62 -0
- package/src/commands/reflect.js +328 -0
- package/src/commands/remember.js +95 -0
- package/src/commands/resolve.js +230 -0
- package/src/commands/resolve.test.js +167 -0
- package/src/commands/search.js +70 -0
- package/src/commands/share.js +65 -0
- package/src/commands/snapshot.js +106 -0
- package/src/commands/status.js +37 -0
- package/src/commands/switch.js +31 -0
- package/src/commands/sync.js +328 -0
- package/src/commands/track.js +61 -0
- package/src/commands/unpin.js +28 -0
- package/src/commands/write.js +58 -0
- package/src/core/auto-commit.js +22 -0
- package/src/core/config.js +93 -0
- package/src/core/context-root.js +28 -0
- package/src/core/fs.js +137 -0
- package/src/core/git.js +182 -0
- package/src/core/importers.js +210 -0
- package/src/core/lock.js +62 -0
- package/src/core/reflect-defrag.js +287 -0
- package/src/core/reflect-gather.js +360 -0
- package/src/core/reflect-parse.js +168 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
5
|
+
import { git, isGitRepo } from '../core/git.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pull .context/ from its remote repository.
|
|
9
|
+
*
|
|
10
|
+
* agent-mem pull — pull from configured remote
|
|
11
|
+
* agent-mem pull --remote <url> — clone/pull from specific remote
|
|
12
|
+
*/
|
|
13
|
+
export default async function pull({ args, flags }) {
|
|
14
|
+
const root = flags._contextRoot || process.cwd();
|
|
15
|
+
const ctxDir = join(root, '.context');
|
|
16
|
+
|
|
17
|
+
// Case 1: .context/ doesn't exist yet — clone from remote
|
|
18
|
+
if (!existsSync(ctxDir)) {
|
|
19
|
+
const remote = flags.remote || args[0];
|
|
20
|
+
if (!remote) {
|
|
21
|
+
console.error('❌ No .context/ found and no remote specified.');
|
|
22
|
+
console.error('Usage: agent-mem pull --remote <git-url>');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(` ⬇️ Cloning context from ${remote}...`);
|
|
27
|
+
try {
|
|
28
|
+
execSync(`git clone ${remote} .context`, {
|
|
29
|
+
cwd: root,
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
});
|
|
33
|
+
console.log(`\n✅ PULLED: ${remote} → .context/`);
|
|
34
|
+
console.log('Run: agent-mem snapshot');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`❌ Clone failed: ${err.stderr?.trim() || err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Case 2: .context/ exists with a remote — pull updates
|
|
43
|
+
if (!isGitRepo(ctxDir)) {
|
|
44
|
+
console.error('❌ .context/ exists but is not a git repo.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Set remote if provided
|
|
49
|
+
if (flags.remote) {
|
|
50
|
+
try {
|
|
51
|
+
git('remote remove origin', ctxDir);
|
|
52
|
+
} catch {}
|
|
53
|
+
git(`remote add origin ${flags.remote}`, ctxDir);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let remoteUrl;
|
|
57
|
+
try {
|
|
58
|
+
remoteUrl = git('remote get-url origin', ctxDir);
|
|
59
|
+
} catch {
|
|
60
|
+
console.error('❌ No remote configured. Use: agent-mem pull --remote <git-url>');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(` ⬇️ Pulling from ${remoteUrl}...`);
|
|
65
|
+
try {
|
|
66
|
+
git('pull --rebase origin main', ctxDir);
|
|
67
|
+
console.log(`\n✅ PULLED: latest context from ${remoteUrl}`);
|
|
68
|
+
console.log('Run: agent-mem snapshot');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Try without rebase
|
|
71
|
+
try {
|
|
72
|
+
git('pull origin main --no-rebase', ctxDir);
|
|
73
|
+
console.log(`\n✅ PULLED (merged): latest context from ${remoteUrl}`);
|
|
74
|
+
} catch (err2) {
|
|
75
|
+
console.error(`❌ Pull failed: ${err2.message}`);
|
|
76
|
+
console.error('There may be conflicts. Check .context/ and resolve manually.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { git, isGitRepo, hasChanges, commitContext } from '../core/git.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Push .context/ to its own remote repository.
|
|
6
|
+
*
|
|
7
|
+
* agent-mem push — push to configured remote
|
|
8
|
+
* agent-mem push --remote <url> — set remote and push
|
|
9
|
+
*/
|
|
10
|
+
export default async function push({ args, flags }) {
|
|
11
|
+
const root = flags._contextRoot;
|
|
12
|
+
const ctxDir = getContextDir(root);
|
|
13
|
+
|
|
14
|
+
if (!isGitRepo(ctxDir)) {
|
|
15
|
+
console.error("❌ .context/ is not a git repo. Run 'agent-mem init' first.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Set remote if provided
|
|
20
|
+
if (flags.remote) {
|
|
21
|
+
try {
|
|
22
|
+
git('remote remove origin', ctxDir);
|
|
23
|
+
} catch {}
|
|
24
|
+
git(`remote add origin ${flags.remote}`, ctxDir);
|
|
25
|
+
console.log(` 🔗 Remote set: ${flags.remote}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if remote exists
|
|
29
|
+
let remoteUrl;
|
|
30
|
+
try {
|
|
31
|
+
remoteUrl = git('remote get-url origin', ctxDir);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error('❌ No remote configured. Use: agent-mem push --remote <git-url>');
|
|
34
|
+
console.error('');
|
|
35
|
+
console.error('Example:');
|
|
36
|
+
console.error(' agent-mem push --remote git@github.com:user/myproject-context.git');
|
|
37
|
+
console.error(' agent-mem push --remote https://github.com/user/myproject-context.git');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Auto-commit if there are changes
|
|
42
|
+
if (hasChanges(ctxDir)) {
|
|
43
|
+
const hash = commitContext(ctxDir, `sync: ${new Date().toISOString().slice(0, 16)}`);
|
|
44
|
+
if (hash) console.log(` ⚡ Auto-committed: ${hash}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ensure branch exists
|
|
48
|
+
try {
|
|
49
|
+
git('rev-parse HEAD', ctxDir);
|
|
50
|
+
} catch {
|
|
51
|
+
console.error('❌ No commits yet. Run some commands first, then push.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get current branch name
|
|
56
|
+
let branch;
|
|
57
|
+
try {
|
|
58
|
+
branch = git('branch --show-current', ctxDir);
|
|
59
|
+
} catch {
|
|
60
|
+
branch = 'main';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Push
|
|
64
|
+
try {
|
|
65
|
+
console.log(` ⬆️ Pushing to ${remoteUrl}...`);
|
|
66
|
+
git(`push -u origin ${branch}`, ctxDir);
|
|
67
|
+
console.log(`\n✅ PUSHED: .context/ → ${remoteUrl}`);
|
|
68
|
+
console.log(`Other machines can now: agent-mem pull`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// First push might need --set-upstream or force
|
|
71
|
+
try {
|
|
72
|
+
git(`push --set-upstream origin ${branch}`, ctxDir);
|
|
73
|
+
console.log(`\n✅ PUSHED: .context/ → ${remoteUrl}`);
|
|
74
|
+
} catch (err2) {
|
|
75
|
+
console.error(`❌ Push failed: ${err2.message}`);
|
|
76
|
+
console.error('Try: agent-mem push --remote <url> (to reconfigure)');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readContextFile } from '../core/fs.js';
|
|
3
|
+
import { readConfig } from '../core/config.js';
|
|
4
|
+
|
|
5
|
+
export default async function read({ args, flags }) {
|
|
6
|
+
const root = flags._contextRoot;
|
|
7
|
+
const ctxDir = getContextDir(root);
|
|
8
|
+
|
|
9
|
+
if (!args.length) {
|
|
10
|
+
console.error('❌ Usage: agent-mem read <path>');
|
|
11
|
+
console.error('Example: agent-mem read memory/decisions.md');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const relPath = args[0];
|
|
16
|
+
const config = readConfig(ctxDir);
|
|
17
|
+
const branch = config.branch || 'main';
|
|
18
|
+
|
|
19
|
+
// When on a branch and reading memory/, try branch-local first
|
|
20
|
+
let content = null;
|
|
21
|
+
if (branch !== 'main' && relPath.startsWith('memory/')) {
|
|
22
|
+
const branchPath = relPath.replace(/^memory\//, `branches/${branch}/memory/`);
|
|
23
|
+
content = readContextFile(ctxDir, branchPath);
|
|
24
|
+
if (content !== null) {
|
|
25
|
+
console.log(`(reading from branch: ${branch})\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fall back to the exact path requested
|
|
30
|
+
if (content === null) {
|
|
31
|
+
content = readContextFile(ctxDir, relPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (content === null) {
|
|
35
|
+
console.error(`❌ File not found: .context/${relPath}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --last N: show only the last N entries (lines matching ^- [)
|
|
40
|
+
const last = parseInt(flags.last, 10);
|
|
41
|
+
if (last > 0) {
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
const entryIndices = [];
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
if (/^- \[/.test(lines[i])) entryIndices.push(i);
|
|
46
|
+
}
|
|
47
|
+
if (entryIndices.length > last) {
|
|
48
|
+
const cutoff = entryIndices[entryIndices.length - last];
|
|
49
|
+
// Keep header (everything before first entry) + last N entries
|
|
50
|
+
const headerEnd = entryIndices[0];
|
|
51
|
+
const header = lines.slice(0, headerEnd).join('\n');
|
|
52
|
+
const entries = lines.slice(cutoff).join('\n');
|
|
53
|
+
const skipped = entryIndices.length - last;
|
|
54
|
+
console.log(
|
|
55
|
+
header + `(${skipped} older entries hidden — showing last ${last})\n\n` + entries,
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(content);
|
|
62
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readContextFile, writeContextFile, parseFrontmatter, listFiles } from '../core/fs.js';
|
|
3
|
+
import { readConfig } from '../core/config.js';
|
|
4
|
+
import { commitContext } from '../core/git.js';
|
|
5
|
+
import { gatherReflectionPrompt, getReflectionFiles } from '../core/reflect-gather.js';
|
|
6
|
+
import {
|
|
7
|
+
parseReflection,
|
|
8
|
+
extractGaps,
|
|
9
|
+
extractStaleEntries,
|
|
10
|
+
extractSummary,
|
|
11
|
+
} from '../core/reflect-parse.js';
|
|
12
|
+
import { analyzeDefrag, formatDefragOutput, applyStaleMarkers } from '../core/reflect-defrag.js';
|
|
13
|
+
|
|
14
|
+
const MEMORY_CATEGORIES = {
|
|
15
|
+
decision: {
|
|
16
|
+
file: 'memory/decisions.md',
|
|
17
|
+
title: 'Decisions',
|
|
18
|
+
desc: 'Architectural decisions and rationale',
|
|
19
|
+
},
|
|
20
|
+
pattern: {
|
|
21
|
+
file: 'memory/patterns.md',
|
|
22
|
+
title: 'Patterns',
|
|
23
|
+
desc: 'Learned patterns and best practices',
|
|
24
|
+
},
|
|
25
|
+
mistake: {
|
|
26
|
+
file: 'memory/mistakes.md',
|
|
27
|
+
title: 'Mistakes',
|
|
28
|
+
desc: 'Anti-patterns and things to avoid',
|
|
29
|
+
},
|
|
30
|
+
note: { file: 'memory/notes.md', title: 'Notes', desc: 'Quick notes and observations' },
|
|
31
|
+
lesson: {
|
|
32
|
+
file: 'memory/lessons.md',
|
|
33
|
+
title: 'Lessons Learned',
|
|
34
|
+
desc: 'Lessons learned — problem/resolution pairs',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default async function reflect({ args, flags }) {
|
|
39
|
+
const root = flags._contextRoot;
|
|
40
|
+
const ctxDir = getContextDir(root);
|
|
41
|
+
|
|
42
|
+
// Determine subcommand
|
|
43
|
+
const subcommand = args[0] || 'gather';
|
|
44
|
+
const subArgs = args.slice(1);
|
|
45
|
+
|
|
46
|
+
switch (subcommand) {
|
|
47
|
+
case 'gather':
|
|
48
|
+
return doGather(ctxDir, flags);
|
|
49
|
+
case 'save':
|
|
50
|
+
return doSave(ctxDir, subArgs, flags);
|
|
51
|
+
case 'history':
|
|
52
|
+
return doHistory(ctxDir, flags);
|
|
53
|
+
case 'defrag':
|
|
54
|
+
return doDefrag(ctxDir, flags);
|
|
55
|
+
default:
|
|
56
|
+
// If unrecognized subcommand, treat as gather
|
|
57
|
+
return doGather(ctxDir, flags);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── GATHER ──────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async function doGather(ctxDir, flags) {
|
|
64
|
+
const result = gatherReflectionPrompt(ctxDir, flags);
|
|
65
|
+
|
|
66
|
+
if (result.empty) {
|
|
67
|
+
console.log('ℹ️ No activity since last reflection. Nothing to reflect on.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(result.prompt);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── SAVE ────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async function doSave(ctxDir, args, flags) {
|
|
77
|
+
let content = flags.content;
|
|
78
|
+
|
|
79
|
+
if (!content) {
|
|
80
|
+
if (args.length > 0) {
|
|
81
|
+
content = args.join(' ');
|
|
82
|
+
} else {
|
|
83
|
+
content = await readStdin();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!content) {
|
|
88
|
+
console.error('❌ Usage: amem reflect save --content "YOUR_REFLECTION"');
|
|
89
|
+
console.error(" Or pipe: echo 'reflection' | amem reflect save");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse the reflection
|
|
94
|
+
const parsed = parseReflection(content);
|
|
95
|
+
|
|
96
|
+
if (!parsed.recognized) {
|
|
97
|
+
console.log('⚠️ Could not parse structured sections. Saving as raw text.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Determine filename (handle multiple reflections same day)
|
|
101
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
102
|
+
let filename = `reflections/${today}.md`;
|
|
103
|
+
let counter = 1;
|
|
104
|
+
while (readContextFile(ctxDir, filename) !== null) {
|
|
105
|
+
counter++;
|
|
106
|
+
filename = `reflections/${today}-${counter}.md`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Read breadcrumb state from gather phase
|
|
110
|
+
let state = {};
|
|
111
|
+
const stateRaw = readContextFile(ctxDir, '.reflect-state.json');
|
|
112
|
+
if (stateRaw) {
|
|
113
|
+
try {
|
|
114
|
+
state = JSON.parse(stateRaw);
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract actionable items (lessons can come from dedicated section or gaps)
|
|
119
|
+
const extractedLessons = parsed.recognized ? extractGaps(parsed.sections.lessons) : [];
|
|
120
|
+
const extractedGapsRaw = parsed.recognized ? extractGaps(parsed.sections.gaps) : [];
|
|
121
|
+
const extractedGaps = [...extractedLessons, ...extractedGapsRaw];
|
|
122
|
+
const staleEntries = parsed.recognized ? extractStaleEntries(parsed.sections.stale) : [];
|
|
123
|
+
const summary = parsed.recognized ? extractSummary(parsed.sections) : null;
|
|
124
|
+
|
|
125
|
+
// Build reflection file with frontmatter
|
|
126
|
+
const frontmatterLines = [
|
|
127
|
+
'---',
|
|
128
|
+
`date: "${today}"`,
|
|
129
|
+
state.window_start ? `window_start: "${state.window_start}"` : null,
|
|
130
|
+
state.window_end ? `window_end: "${state.window_end}"` : null,
|
|
131
|
+
state.commits_reviewed != null ? `commits_reviewed: ${state.commits_reviewed}` : null,
|
|
132
|
+
state.last_commit_hash ? `last_commit_hash: "${state.last_commit_hash}"` : null,
|
|
133
|
+
`entries_extracted: ${extractedGaps.length}`,
|
|
134
|
+
'---',
|
|
135
|
+
].filter(Boolean);
|
|
136
|
+
|
|
137
|
+
const body = parsed.recognized ? content : `## Raw Reflection\n\n${content}`;
|
|
138
|
+
const reflectionContent = `${frontmatterLines.join('\n')}\n\n# Reflection: ${today}\n\n${body}`;
|
|
139
|
+
writeContextFile(ctxDir, filename, reflectionContent);
|
|
140
|
+
|
|
141
|
+
// Extract gaps to memory files
|
|
142
|
+
const extracted = {};
|
|
143
|
+
for (const gap of extractedGaps) {
|
|
144
|
+
const cat = MEMORY_CATEGORIES[gap.type];
|
|
145
|
+
if (!cat) continue;
|
|
146
|
+
|
|
147
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
148
|
+
const time = new Date().toISOString().slice(11, 16);
|
|
149
|
+
|
|
150
|
+
let existing =
|
|
151
|
+
readContextFile(ctxDir, cat.file) ||
|
|
152
|
+
['---', `description: "${cat.desc}"`, '---', '', `# ${cat.title}`, ''].join('\n');
|
|
153
|
+
|
|
154
|
+
if (gap.type === 'lesson') {
|
|
155
|
+
// Structured block format for lessons
|
|
156
|
+
let block = `\n### [${date} ${time}] ${gap.text}\n`;
|
|
157
|
+
block += `**Problem:** ${gap.problem || gap.text}\n`;
|
|
158
|
+
block += `**Resolution:** ${gap.resolution || gap.text}\n`;
|
|
159
|
+
existing += block;
|
|
160
|
+
} else {
|
|
161
|
+
existing += `\n- [${date} ${time}] ${gap.text}`;
|
|
162
|
+
}
|
|
163
|
+
writeContextFile(ctxDir, cat.file, existing);
|
|
164
|
+
|
|
165
|
+
if (!extracted[cat.file]) extracted[cat.file] = 0;
|
|
166
|
+
extracted[cat.file]++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Flag stale entries (non-destructive markers)
|
|
170
|
+
let staleMarked = 0;
|
|
171
|
+
if (staleEntries.length > 0) {
|
|
172
|
+
staleMarked = applyStaleMarkers(ctxDir, staleEntries);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update breadcrumb state
|
|
176
|
+
if (stateRaw) {
|
|
177
|
+
state.saved_at = new Date().toISOString();
|
|
178
|
+
state.reflection_file = filename;
|
|
179
|
+
writeContextFile(ctxDir, '.reflect-state.json', JSON.stringify(state, null, 2));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Git commit
|
|
183
|
+
const commitMsg = summary ? `reflect: ${summary.slice(0, 60)}` : `reflect: ${today}`;
|
|
184
|
+
const hash = commitContext(ctxDir, commitMsg);
|
|
185
|
+
|
|
186
|
+
// Output
|
|
187
|
+
console.log(`✅ REFLECTED: ${filename}`);
|
|
188
|
+
if (state.window_start && state.window_end) {
|
|
189
|
+
console.log(
|
|
190
|
+
`Window: ${state.window_start} → ${state.window_end} (${state.commits_reviewed || '?'} commits reviewed)`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Object.keys(extracted).length > 0) {
|
|
195
|
+
console.log('Extracted:');
|
|
196
|
+
for (const [file, count] of Object.entries(extracted)) {
|
|
197
|
+
console.log(` → ${file}: +${count} entr${count > 1 ? 'ies' : 'y'}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (staleMarked > 0) {
|
|
202
|
+
console.log(`Flagged stale: ${staleMarked} entr${staleMarked > 1 ? 'ies' : 'y'}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (hash) {
|
|
206
|
+
console.log(`Committed: ${hash}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── HISTORY ─────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function doHistory(ctxDir, flags) {
|
|
213
|
+
const reflections = getReflectionFiles(ctxDir);
|
|
214
|
+
const lastN = flags.last ? parseInt(flags.last, 10) : 5;
|
|
215
|
+
|
|
216
|
+
if (reflections.length === 0) {
|
|
217
|
+
console.log('📋 No reflections yet. Run `amem reflect` to create your first.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const shown = reflections.slice(-lastN).reverse();
|
|
222
|
+
console.log(`📋 REFLECTION HISTORY (showing ${shown.length} of ${reflections.length})`);
|
|
223
|
+
console.log('');
|
|
224
|
+
|
|
225
|
+
const allThemes = [];
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < shown.length; i++) {
|
|
228
|
+
const raw = readContextFile(ctxDir, shown[i]);
|
|
229
|
+
if (!raw) continue;
|
|
230
|
+
|
|
231
|
+
const { content: body } = parseFrontmatter(raw);
|
|
232
|
+
|
|
233
|
+
// Parse frontmatter for metadata
|
|
234
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
235
|
+
const fm = fmMatch ? fmMatch[1] : '';
|
|
236
|
+
const commits = fm.match(/commits_reviewed:\s*(\d+)/)?.[1] || '?';
|
|
237
|
+
const extracted = fm.match(/entries_extracted:\s*(\d+)/)?.[1] || '0';
|
|
238
|
+
const windowStart = fm.match(/window_start:\s*['"]?([^'"]+)['"]?/)?.[1] || '';
|
|
239
|
+
const windowEnd = fm.match(/window_end:\s*['"]?([^'"]+)['"]?/)?.[1] || '';
|
|
240
|
+
|
|
241
|
+
// Extract themes
|
|
242
|
+
const themeMatch = body.match(/## Themes?\n([\s\S]*?)(?=\n## |\n$|$)/);
|
|
243
|
+
const themes = themeMatch
|
|
244
|
+
? themeMatch[1]
|
|
245
|
+
.split('\n')
|
|
246
|
+
.filter((l) => l.trim().startsWith('-'))
|
|
247
|
+
.map((l) => l.replace(/^-\s*/, '').trim())
|
|
248
|
+
: [];
|
|
249
|
+
allThemes.push(...themes);
|
|
250
|
+
|
|
251
|
+
// Extract summary
|
|
252
|
+
const summaryMatch = body.match(/## Summary\n([\s\S]*?)(?=\n## |\n$|$)/);
|
|
253
|
+
const summary = summaryMatch ? summaryMatch[1].trim().slice(0, 120) : '(no summary)';
|
|
254
|
+
|
|
255
|
+
const window =
|
|
256
|
+
windowStart && windowEnd ? ` | Window: ${windowStart.slice(5)}–${windowEnd.slice(5)}` : '';
|
|
257
|
+
const date = shown[i].replace('reflections/', '').replace('.md', '');
|
|
258
|
+
|
|
259
|
+
console.log(`${i + 1}. ${date} — ${commits} commits | ${extracted} extracted${window}`);
|
|
260
|
+
if (themes.length > 0) {
|
|
261
|
+
console.log(` Themes: ${themes.join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
console.log(` Summary: ${summary}`);
|
|
264
|
+
console.log('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Recurring themes across 3+ reflections
|
|
268
|
+
if (allThemes.length > 0 && shown.length >= 3) {
|
|
269
|
+
const themeCounts = {};
|
|
270
|
+
for (const theme of allThemes) {
|
|
271
|
+
const words = theme
|
|
272
|
+
.toLowerCase()
|
|
273
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
274
|
+
.split(/\s+/)
|
|
275
|
+
.filter((w) => w.length > 3);
|
|
276
|
+
for (const word of words) {
|
|
277
|
+
themeCounts[word] = (themeCounts[word] || 0) + 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const recurring = Object.entries(themeCounts)
|
|
282
|
+
.filter(([, count]) => count >= 3)
|
|
283
|
+
.sort((a, b) => b[1] - a[1])
|
|
284
|
+
.slice(0, 5);
|
|
285
|
+
|
|
286
|
+
if (recurring.length > 0) {
|
|
287
|
+
console.log('Recurring themes (3+ reflections):');
|
|
288
|
+
for (const [word, count] of recurring) {
|
|
289
|
+
console.log(` - "${word}" (${count}/${shown.length})`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── DEFRAG ──────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
async function doDefrag(ctxDir, flags) {
|
|
298
|
+
const isDryRun = flags['dry-run'] === true;
|
|
299
|
+
const analysis = analyzeDefrag(ctxDir);
|
|
300
|
+
|
|
301
|
+
console.log(formatDefragOutput(analysis, isDryRun));
|
|
302
|
+
|
|
303
|
+
// If not dry run, apply stale markers
|
|
304
|
+
if (!isDryRun && analysis.stale.length > 0) {
|
|
305
|
+
const marked = applyStaleMarkers(ctxDir, analysis.stale);
|
|
306
|
+
if (marked > 0) {
|
|
307
|
+
const hash = commitContext(ctxDir, `defrag: flagged ${marked} stale entries`);
|
|
308
|
+
console.log('');
|
|
309
|
+
console.log(`Applied: ${marked} stale markers. Committed: ${hash}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── UTILS ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function readStdin() {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
if (process.stdin.isTTY) {
|
|
319
|
+
resolve(null);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
let data = '';
|
|
323
|
+
process.stdin.setEncoding('utf-8');
|
|
324
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
325
|
+
process.stdin.on('end', () => resolve(data || null));
|
|
326
|
+
setTimeout(() => resolve(data || null), 100);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readContextFile, writeContextFile } from '../core/fs.js';
|
|
3
|
+
import { readConfig } from '../core/config.js';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = {
|
|
6
|
+
decision: {
|
|
7
|
+
file: 'memory/decisions.md',
|
|
8
|
+
title: 'Decisions',
|
|
9
|
+
desc: 'Architectural decisions and rationale',
|
|
10
|
+
},
|
|
11
|
+
pattern: {
|
|
12
|
+
file: 'memory/patterns.md',
|
|
13
|
+
title: 'Patterns',
|
|
14
|
+
desc: 'Learned patterns and best practices',
|
|
15
|
+
},
|
|
16
|
+
mistake: {
|
|
17
|
+
file: 'memory/mistakes.md',
|
|
18
|
+
title: 'Mistakes',
|
|
19
|
+
desc: 'Anti-patterns and things to avoid',
|
|
20
|
+
},
|
|
21
|
+
note: { file: 'memory/notes.md', title: 'Notes', desc: 'Quick notes and observations' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve memory file path based on active branch.
|
|
26
|
+
* On main: memory/notes.md
|
|
27
|
+
* On branch: branches/<name>/memory/notes.md
|
|
28
|
+
*/
|
|
29
|
+
function resolvePath(target, branch) {
|
|
30
|
+
if (branch && branch !== 'main') {
|
|
31
|
+
return target.replace(/^memory\//, `branches/${branch}/memory/`);
|
|
32
|
+
}
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function remember({ args, flags }) {
|
|
37
|
+
const root = flags._contextRoot;
|
|
38
|
+
const ctxDir = getContextDir(root);
|
|
39
|
+
|
|
40
|
+
if (!args.length) {
|
|
41
|
+
console.error('❌ Usage: agent-mem remember [--category] <text>');
|
|
42
|
+
console.error('');
|
|
43
|
+
console.error('Categories:');
|
|
44
|
+
console.error(' --decision "Chose Qdrant over Pinecone because self-hosted"');
|
|
45
|
+
console.error(' --pattern "Always check Grafana logs before fixing bugs"');
|
|
46
|
+
console.error(' --mistake "Never force-push to fix branches"');
|
|
47
|
+
console.error(' --note "Meeting with team re: auth refactor" (default)');
|
|
48
|
+
console.error('');
|
|
49
|
+
console.error('Or specify file directly:');
|
|
50
|
+
console.error(' --file memory/custom.md "some text"');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine category from flags
|
|
55
|
+
let category = 'note';
|
|
56
|
+
for (const cat of Object.keys(CATEGORIES)) {
|
|
57
|
+
if (flags[cat] !== undefined) {
|
|
58
|
+
category = cat;
|
|
59
|
+
// Flag parser may consume the next arg as value — restore it
|
|
60
|
+
if (typeof flags[cat] === 'string') {
|
|
61
|
+
args.unshift(flags[cat]);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!args.length) {
|
|
68
|
+
console.error(`❌ No text provided. Usage: agent-mem remember --${category} "your text"`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = readConfig(ctxDir);
|
|
73
|
+
const branch = config.branch || 'main';
|
|
74
|
+
const baseTarget = flags.file || CATEGORIES[category].file;
|
|
75
|
+
const target = resolvePath(baseTarget, branch);
|
|
76
|
+
const { title, desc } = CATEGORIES[category] || CATEGORIES.note;
|
|
77
|
+
const text = args.join(' ');
|
|
78
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
79
|
+
const time = new Date().toISOString().slice(11, 16);
|
|
80
|
+
|
|
81
|
+
let existing =
|
|
82
|
+
readContextFile(ctxDir, target) ||
|
|
83
|
+
['---', `description: "${desc}"`, '---', '', `# ${title}`, ''].join('\n');
|
|
84
|
+
|
|
85
|
+
existing += `\n- [${date} ${time}] ${text}`;
|
|
86
|
+
|
|
87
|
+
writeContextFile(ctxDir, target, existing);
|
|
88
|
+
const branchLabel = branch !== 'main' ? ` [branch: ${branch}]` : '';
|
|
89
|
+
console.log(`✅ REMEMBERED (${category})${branchLabel} → .context/${target}`);
|
|
90
|
+
console.log(` "${text}"`);
|
|
91
|
+
|
|
92
|
+
// Auto-commit if enabled
|
|
93
|
+
const { maybeAutoCommit } = await import('../core/auto-commit.js');
|
|
94
|
+
maybeAutoCommit(ctxDir, `remember ${category}`);
|
|
95
|
+
}
|