@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,157 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
4
|
+
import { readContextFile } from '../core/fs.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collect all .md files recursively from a directory, returning { relPath → content }.
|
|
8
|
+
*/
|
|
9
|
+
function collectFiles(baseDir, subDir = '') {
|
|
10
|
+
const files = {};
|
|
11
|
+
const dir = subDir ? join(baseDir, subDir) : baseDir;
|
|
12
|
+
if (!existsSync(dir)) return files;
|
|
13
|
+
|
|
14
|
+
for (const name of readdirSync(dir)) {
|
|
15
|
+
if (name.startsWith('.')) continue;
|
|
16
|
+
const full = join(dir, name);
|
|
17
|
+
const rel = subDir ? `${subDir}/${name}` : name;
|
|
18
|
+
if (statSync(full).isDirectory()) {
|
|
19
|
+
Object.assign(files, collectFiles(baseDir, rel));
|
|
20
|
+
} else {
|
|
21
|
+
files[rel] = readFileSync(full, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple line-based diff between two strings.
|
|
29
|
+
* Returns { added: string[], removed: string[], unchanged: number }
|
|
30
|
+
*/
|
|
31
|
+
function lineDiff(a, b) {
|
|
32
|
+
const aLines = new Set((a || '').split('\n'));
|
|
33
|
+
const bLines = new Set((b || '').split('\n'));
|
|
34
|
+
const added = [];
|
|
35
|
+
const removed = [];
|
|
36
|
+
let unchanged = 0;
|
|
37
|
+
|
|
38
|
+
for (const line of bLines) {
|
|
39
|
+
if (!aLines.has(line)) added.push(line);
|
|
40
|
+
else unchanged++;
|
|
41
|
+
}
|
|
42
|
+
for (const line of aLines) {
|
|
43
|
+
if (!bLines.has(line)) removed.push(line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { added, removed, unchanged };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default async function diff({ args, flags }) {
|
|
50
|
+
const root = flags._contextRoot;
|
|
51
|
+
const ctxDir = getContextDir(root);
|
|
52
|
+
|
|
53
|
+
const branchName = args[0];
|
|
54
|
+
if (!branchName) {
|
|
55
|
+
console.error('❌ Usage: agent-mem diff <branch>');
|
|
56
|
+
console.error("Compare a branch's context with main.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const branchDir = join(ctxDir, 'branches', branchName);
|
|
61
|
+
if (!existsSync(branchDir)) {
|
|
62
|
+
console.error(`❌ Branch "${branchName}" not found.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Branch metadata files to exclude from diff (these are branch-level, not context)
|
|
67
|
+
const BRANCH_METADATA = new Set(['purpose.md', 'commits.md', 'trace.md']);
|
|
68
|
+
|
|
69
|
+
// Collect branch files, excluding metadata
|
|
70
|
+
const rawBranchFiles = collectFiles(branchDir);
|
|
71
|
+
const branchFiles = {};
|
|
72
|
+
for (const [path, content] of Object.entries(rawBranchFiles)) {
|
|
73
|
+
if (!BRANCH_METADATA.has(path)) {
|
|
74
|
+
branchFiles[path] = content;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Collect main-level files (memory/, system/) for comparison
|
|
79
|
+
// Use same path structure as branch files for valid comparison
|
|
80
|
+
const mainFiles = {};
|
|
81
|
+
for (const dir of ['memory', 'system']) {
|
|
82
|
+
const collected = collectFiles(join(ctxDir, dir));
|
|
83
|
+
for (const [rel, content] of Object.entries(collected)) {
|
|
84
|
+
mainFiles[`${dir}/${rel}`] = content;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Also include main.md
|
|
88
|
+
const mainMd = readContextFile(ctxDir, 'main.md');
|
|
89
|
+
if (mainMd) mainFiles['main.md'] = mainMd;
|
|
90
|
+
|
|
91
|
+
// Compute diffs
|
|
92
|
+
// Branches are sparse — only compare files that exist IN the branch.
|
|
93
|
+
// Files only in main are NOT "deleted" (branches don't clone main).
|
|
94
|
+
const diffs = [];
|
|
95
|
+
let totalAdded = 0;
|
|
96
|
+
let totalRemoved = 0;
|
|
97
|
+
|
|
98
|
+
for (const path of Object.keys(branchFiles).sort()) {
|
|
99
|
+
const mainContent = path in mainFiles ? mainFiles[path] : null;
|
|
100
|
+
const branchContent = branchFiles[path];
|
|
101
|
+
|
|
102
|
+
if (mainContent === branchContent) continue; // identical
|
|
103
|
+
|
|
104
|
+
if (mainContent === null) {
|
|
105
|
+
// File exists in branch but not in main — new file
|
|
106
|
+
diffs.push({ path, status: 'added', lines: branchContent.split('\n').length });
|
|
107
|
+
totalAdded += branchContent.split('\n').length;
|
|
108
|
+
} else {
|
|
109
|
+
// File exists in both — compare
|
|
110
|
+
const { added, removed } = lineDiff(mainContent, branchContent);
|
|
111
|
+
if (added.length || removed.length) {
|
|
112
|
+
diffs.push({ path, status: 'modified', added, removed });
|
|
113
|
+
totalAdded += added.length;
|
|
114
|
+
totalRemoved += removed.length;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Output
|
|
120
|
+
console.log(`🔀 DIFF: main ↔ ${branchName}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
|
|
123
|
+
if (diffs.length === 0) {
|
|
124
|
+
console.log('No differences found.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const d of diffs) {
|
|
129
|
+
if (d.status === 'added') {
|
|
130
|
+
console.log(` + ${d.path} (new, ${d.lines} lines)`);
|
|
131
|
+
} else if (d.status === 'removed') {
|
|
132
|
+
console.log(` - ${d.path} (deleted, ${d.lines} lines)`);
|
|
133
|
+
} else {
|
|
134
|
+
console.log(` ~ ${d.path} (+${d.added.length} / -${d.removed.length})`);
|
|
135
|
+
if (flags.verbose) {
|
|
136
|
+
for (const line of d.added.slice(0, 5)) {
|
|
137
|
+
console.log(` + ${line}`);
|
|
138
|
+
}
|
|
139
|
+
for (const line of d.removed.slice(0, 5)) {
|
|
140
|
+
console.log(` - ${line}`);
|
|
141
|
+
}
|
|
142
|
+
const moreAdded = d.added.length - 5;
|
|
143
|
+
const moreRemoved = d.removed.length - 5;
|
|
144
|
+
if (moreAdded > 0 || moreRemoved > 0) {
|
|
145
|
+
console.log(
|
|
146
|
+
` ... and ${Math.max(0, moreAdded)} more added, ${Math.max(0, moreRemoved)} more removed`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(
|
|
155
|
+
`Summary: ${diffs.length} file${diffs.length > 1 ? 's' : ''} changed, +${totalAdded} / -${totalRemoved} lines`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const CLI = join(import.meta.dirname, '../../bin/agent-context.js');
|
|
9
|
+
|
|
10
|
+
function run(args, cwd) {
|
|
11
|
+
return execSync(`node ${CLI} ${args}`, { cwd, encoding: 'utf-8', timeout: 10000 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('diff', () => {
|
|
15
|
+
let dir;
|
|
16
|
+
|
|
17
|
+
before(() => {
|
|
18
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-diff-'));
|
|
19
|
+
execSync('git init', { cwd: dir, stdio: 'ignore' });
|
|
20
|
+
execSync('git config user.name "test"', { cwd: dir, stdio: 'ignore' });
|
|
21
|
+
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: 'ignore' });
|
|
22
|
+
run('init', dir);
|
|
23
|
+
run('branch test-feature "testing diff"', dir);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
after(() => {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('fresh branch with no context changes shows no differences', () => {
|
|
31
|
+
const out = run('diff test-feature', dir);
|
|
32
|
+
assert.match(out, /DIFF.*main.*test-feature/);
|
|
33
|
+
assert.match(out, /No differences found/);
|
|
34
|
+
// Branch metadata should NOT appear
|
|
35
|
+
assert.ok(!out.includes('purpose.md'), 'should not show branch metadata');
|
|
36
|
+
assert.ok(!out.includes('commits.md'), 'should not show branch metadata');
|
|
37
|
+
// Main files should NOT appear as deleted
|
|
38
|
+
assert.ok(!out.includes('main.md'), 'should not show main files as deleted');
|
|
39
|
+
assert.ok(!out.includes('system/'), 'should not show system files as deleted');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shows added files in branch', () => {
|
|
43
|
+
const branchDir = join(dir, '.context', 'branches', 'test-feature');
|
|
44
|
+
mkdirSync(join(branchDir, 'memory'), { recursive: true });
|
|
45
|
+
writeFileSync(join(branchDir, 'memory', 'experiment.md'), '# Experiment\n- new finding\n');
|
|
46
|
+
|
|
47
|
+
const out = run('diff test-feature', dir);
|
|
48
|
+
assert.match(out, /experiment\.md/);
|
|
49
|
+
assert.match(out, /\+.*experiment/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('shows verbose line details', () => {
|
|
53
|
+
const out = run('diff test-feature --verbose', dir);
|
|
54
|
+
assert.match(out, /DIFF/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('errors on nonexistent branch', () => {
|
|
58
|
+
assert.throws(() => run('diff nonexistent', dir), /not found/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('errors with no branch argument', () => {
|
|
62
|
+
assert.throws(() => run('diff', dir), /Usage/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, relative } from 'node:path';
|
|
3
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
4
|
+
import { readContextFile, writeContextFile } from '../core/fs.js';
|
|
5
|
+
import { commitContext } from '../core/git.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize and validate a relative path within .context/.
|
|
9
|
+
* Returns the normalized relative path or null if invalid.
|
|
10
|
+
*/
|
|
11
|
+
function safePath(ctxDir, inputPath) {
|
|
12
|
+
const fullPath = resolve(ctxDir, inputPath);
|
|
13
|
+
const rel = relative(ctxDir, fullPath);
|
|
14
|
+
// Must not escape .context/ (no leading ..)
|
|
15
|
+
if (rel.startsWith('..') || rel.startsWith('/')) return null;
|
|
16
|
+
return rel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function forget({ args, flags }) {
|
|
20
|
+
const root = flags._contextRoot;
|
|
21
|
+
const ctxDir = getContextDir(root);
|
|
22
|
+
|
|
23
|
+
if (!args.length) {
|
|
24
|
+
console.error('❌ Usage: agent-mem forget <path>');
|
|
25
|
+
console.error('Remove a context file. Archived first for safety.');
|
|
26
|
+
console.error('');
|
|
27
|
+
console.error('Examples:');
|
|
28
|
+
console.error(' agent-mem forget memory/notes.md');
|
|
29
|
+
console.error(' agent-mem forget memory/old-patterns.md');
|
|
30
|
+
console.error('');
|
|
31
|
+
console.error('Cannot forget pinned (system/) files — unpin first.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Normalize path to prevent traversal
|
|
36
|
+
const relPath = safePath(ctxDir, args[0]);
|
|
37
|
+
if (!relPath) {
|
|
38
|
+
console.error('❌ Invalid path — must be inside .context/');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Safety: don't allow forgetting system files (normalized)
|
|
43
|
+
if (relPath.startsWith('system/') || relPath === 'system') {
|
|
44
|
+
console.error("❌ Cannot forget pinned files. Run 'agent-mem unpin' first.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Safety: don't allow forgetting config
|
|
49
|
+
if (relPath === 'config.yaml') {
|
|
50
|
+
console.error('❌ Cannot forget config.yaml.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fullPath = join(ctxDir, relPath);
|
|
55
|
+
if (!existsSync(fullPath)) {
|
|
56
|
+
console.error(`❌ File not found: .context/${relPath}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Archive before deleting
|
|
61
|
+
const content = readContextFile(ctxDir, relPath);
|
|
62
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
63
|
+
const archivePath = `archive/forgotten-${today}/${relPath}`;
|
|
64
|
+
writeContextFile(ctxDir, archivePath, content);
|
|
65
|
+
|
|
66
|
+
// Remove the file
|
|
67
|
+
unlinkSync(fullPath);
|
|
68
|
+
|
|
69
|
+
// Commit
|
|
70
|
+
const hash = commitContext(ctxDir, `forget: removed ${relPath}`);
|
|
71
|
+
|
|
72
|
+
console.log(`🗑️ FORGOT: ${relPath}`);
|
|
73
|
+
console.log(`Archived: .context/${archivePath}`);
|
|
74
|
+
if (hash) console.log(`Committed: ${hash}`);
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log('To restore: agent-mem read ' + archivePath);
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const CLI = join(import.meta.dirname, '../../bin/agent-context.js');
|
|
9
|
+
|
|
10
|
+
function run(args, cwd) {
|
|
11
|
+
return execSync(`node ${CLI} ${args}`, { cwd, encoding: 'utf-8', timeout: 10000 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('forget', () => {
|
|
15
|
+
let dir;
|
|
16
|
+
|
|
17
|
+
before(() => {
|
|
18
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-forget-'));
|
|
19
|
+
execSync('git init', { cwd: dir, stdio: 'ignore' });
|
|
20
|
+
execSync('git config user.name "test"', { cwd: dir, stdio: 'ignore' });
|
|
21
|
+
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: 'ignore' });
|
|
22
|
+
run('init', dir);
|
|
23
|
+
|
|
24
|
+
// Add a memory file to forget
|
|
25
|
+
const memFile = join(dir, '.context', 'memory', 'old-notes.md');
|
|
26
|
+
writeFileSync(memFile, '# Old Notes\n- outdated stuff\n');
|
|
27
|
+
execSync("git -C .context add -A && git -C .context commit -m 'add notes'", {
|
|
28
|
+
cwd: dir,
|
|
29
|
+
stdio: 'ignore',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(() => {
|
|
34
|
+
rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('removes file and archives it', () => {
|
|
38
|
+
const out = run('forget memory/old-notes.md', dir);
|
|
39
|
+
assert.match(out, /FORGOT.*old-notes\.md/);
|
|
40
|
+
assert.match(out, /Archived/);
|
|
41
|
+
assert.ok(!existsSync(join(dir, '.context', 'memory', 'old-notes.md')));
|
|
42
|
+
assert.ok(existsSync(join(dir, '.context', 'archive')));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('refuses to forget pinned (system/) files', () => {
|
|
46
|
+
assert.throws(() => run('forget system/project.md', dir), /Cannot forget pinned/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('refuses ./system/ bypass', () => {
|
|
50
|
+
assert.throws(() => run('forget ./system/project.md', dir), /Cannot forget pinned/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('refuses to forget config.yaml', () => {
|
|
54
|
+
assert.throws(() => run('forget config.yaml', dir), /Cannot forget config/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('refuses ./config.yaml bypass', () => {
|
|
58
|
+
assert.throws(() => run('forget ./config.yaml', dir), /Cannot forget config/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects path traversal (../)', () => {
|
|
62
|
+
assert.throws(() => run('forget ../outside.txt', dir), /Invalid path/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('errors on nonexistent file', () => {
|
|
66
|
+
assert.throws(() => run('forget memory/nope.md', dir), /not found/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export default async function help() {
|
|
2
|
+
console.log(
|
|
3
|
+
`
|
|
4
|
+
agent-mem — Context management CLI for AI coding agents
|
|
5
|
+
|
|
6
|
+
USAGE
|
|
7
|
+
agent-mem <command> [args] [flags]
|
|
8
|
+
amem <command> [args] [flags]
|
|
9
|
+
|
|
10
|
+
CORE
|
|
11
|
+
init [--from-claude] [--from-codex] Bootstrap context + auto-sync IDE rules
|
|
12
|
+
snapshot Context tree (agent's primary view)
|
|
13
|
+
read <path> Read a context file
|
|
14
|
+
write <path> [--content <text>] Write/update a context file (reads stdin if no --content)
|
|
15
|
+
commit [message] Checkpoint: summarize + git commit
|
|
16
|
+
status Quick status overview
|
|
17
|
+
|
|
18
|
+
MEMORY
|
|
19
|
+
remember [--decision|--pattern|--mistake|--note] <text>
|
|
20
|
+
Add to memory (structured categories)
|
|
21
|
+
lesson <title> --problem <text> --resolution <text> [--tags <tags>]
|
|
22
|
+
Record a lesson learned (problem/resolution pair)
|
|
23
|
+
Shorthand: lesson "problem -> resolution"
|
|
24
|
+
search <query> Search across all context files
|
|
25
|
+
pin <path> Move file to system/ (always in context)
|
|
26
|
+
unpin <path> Move file out of system/
|
|
27
|
+
forget <path> Remove a memory file (archived first)
|
|
28
|
+
|
|
29
|
+
BRANCHES
|
|
30
|
+
branch <name> [purpose] Create exploration branch
|
|
31
|
+
switch <name> Switch active branch
|
|
32
|
+
merge <name> [summary] Merge branch back to main
|
|
33
|
+
branches List all branches
|
|
34
|
+
diff <branch> Compare branch context with main
|
|
35
|
+
|
|
36
|
+
COMPACTION
|
|
37
|
+
compact Archive stale context, keep pins + recent
|
|
38
|
+
compact --dry-run Preview what would be archived
|
|
39
|
+
compact --hard Pins only, archive everything else
|
|
40
|
+
|
|
41
|
+
CONFLICTS
|
|
42
|
+
resolve Auto-resolve .context/ merge conflicts
|
|
43
|
+
resolve --dry-run Preview resolution strategy per file
|
|
44
|
+
|
|
45
|
+
REFLECTION
|
|
46
|
+
reflect Gather reflection input (default: gather)
|
|
47
|
+
reflect gather [--since <ref>] [--deep] [--compaction]
|
|
48
|
+
Collect recent activity for reflection
|
|
49
|
+
reflect save [--content <text>] Save agent's reflection output
|
|
50
|
+
reflect history [--last <n>] Show past reflections
|
|
51
|
+
reflect defrag [--dry-run] Analyze memory health
|
|
52
|
+
|
|
53
|
+
SYNC
|
|
54
|
+
sync [--claude|--gemini|--codex|--cursor|--windsurf|--all]
|
|
55
|
+
Export .context/ to IDE rule files
|
|
56
|
+
Auto-detects existing files if no flags
|
|
57
|
+
|
|
58
|
+
SHARING
|
|
59
|
+
track [--enable|--disable] Toggle .context/ in project git
|
|
60
|
+
push [--remote <url>] Push .context/ to its own remote
|
|
61
|
+
pull [--remote <url>] Pull/clone .context/ from remote
|
|
62
|
+
share [--output <path>] Generate portable snapshot file
|
|
63
|
+
import <file> Import a shared snapshot
|
|
64
|
+
|
|
65
|
+
CONFIG
|
|
66
|
+
config Show current config
|
|
67
|
+
config set <key> <value> Update config value
|
|
68
|
+
|
|
69
|
+
FLAGS
|
|
70
|
+
--verbose Show detailed output
|
|
71
|
+
--help, -h Show this help
|
|
72
|
+
|
|
73
|
+
EXAMPLES
|
|
74
|
+
agent-mem init
|
|
75
|
+
agent-mem snapshot
|
|
76
|
+
agent-mem commit "implemented auth flow"
|
|
77
|
+
agent-mem branch try-qdrant "evaluate vector search"
|
|
78
|
+
agent-mem remember --decision "chose Qdrant over Pinecone"
|
|
79
|
+
agent-mem remember --pattern "always check Grafana before fixing"
|
|
80
|
+
agent-mem remember --mistake "never force-push fix branches"
|
|
81
|
+
agent-mem lesson "API 429 -> implement exponential backoff"
|
|
82
|
+
agent-mem lesson "OOM fix" --problem "Memory leak after 1hr" --resolution "Close DB connections"
|
|
83
|
+
agent-mem search "authentication"
|
|
84
|
+
agent-mem reflect
|
|
85
|
+
agent-mem reflect save --content "## Patterns Identified..."
|
|
86
|
+
agent-mem reflect history
|
|
87
|
+
agent-mem reflect defrag --dry-run
|
|
88
|
+
agent-mem compact --dry-run # preview compaction
|
|
89
|
+
agent-mem compact # archive stale, keep recent + pins
|
|
90
|
+
agent-mem compact --hard # nuclear: pins only
|
|
91
|
+
agent-mem sync # auto-detect IDE files
|
|
92
|
+
agent-mem sync --claude # export to CLAUDE.md
|
|
93
|
+
agent-mem sync --all # export to all IDE formats
|
|
94
|
+
|
|
95
|
+
MORE INFO
|
|
96
|
+
https://github.com/lmaksym/agent-mem
|
|
97
|
+
`.trim(),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { writeContextFile } from '../core/fs.js';
|
|
4
|
+
import { initGit, commitContext } from '../core/git.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Import a shared context snapshot.
|
|
8
|
+
*
|
|
9
|
+
* agent-mem import <file> — import from snapshot file
|
|
10
|
+
* agent-mem import --merge — merge into existing context (default: overwrite)
|
|
11
|
+
*/
|
|
12
|
+
export default async function importCmd({ args, flags }) {
|
|
13
|
+
if (!args.length) {
|
|
14
|
+
console.error('❌ Usage: agent-mem import <snapshot-file>');
|
|
15
|
+
console.error('Example: agent-mem import context-myproject-a1b2c3d4.json');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const filePath = args[0];
|
|
20
|
+
|
|
21
|
+
if (!existsSync(filePath)) {
|
|
22
|
+
console.error(`❌ File not found: ${filePath}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let snapshot;
|
|
27
|
+
try {
|
|
28
|
+
snapshot = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`❌ Invalid snapshot file: ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!snapshot.files || snapshot.version !== 1) {
|
|
35
|
+
console.error('❌ Unrecognized snapshot format.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const ctxDir = join(cwd, '.context');
|
|
41
|
+
const isNew = !existsSync(ctxDir);
|
|
42
|
+
|
|
43
|
+
mkdirSync(ctxDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Write all files from snapshot
|
|
46
|
+
let written = 0;
|
|
47
|
+
let skipped = 0;
|
|
48
|
+
|
|
49
|
+
for (const [path, content] of Object.entries(snapshot.files)) {
|
|
50
|
+
if (!flags.merge) {
|
|
51
|
+
// Overwrite mode
|
|
52
|
+
writeContextFile(ctxDir, path, content);
|
|
53
|
+
written++;
|
|
54
|
+
} else {
|
|
55
|
+
// Merge mode — only write if file doesn't exist
|
|
56
|
+
const existing = join(ctxDir, path);
|
|
57
|
+
if (!existsSync(existing)) {
|
|
58
|
+
writeContextFile(ctxDir, path, content);
|
|
59
|
+
written++;
|
|
60
|
+
} else {
|
|
61
|
+
skipped++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize git if new
|
|
67
|
+
if (isNew) {
|
|
68
|
+
initGit(ctxDir);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
commitContext(
|
|
72
|
+
ctxDir,
|
|
73
|
+
`import: from ${snapshot.project || 'unknown'} (${snapshot.createdAt?.slice(0, 10) || '?'})`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(`✅ IMPORTED: ${snapshot.project || 'unknown'}`);
|
|
77
|
+
console.log(` Files written: ${written}${skipped ? ` | Skipped (existing): ${skipped}` : ''}`);
|
|
78
|
+
console.log(
|
|
79
|
+
` Source: ${snapshot.commits || '?'} commits, created ${snapshot.createdAt?.slice(0, 10) || '?'}`,
|
|
80
|
+
);
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('Run: agent-mem snapshot');
|
|
83
|
+
}
|