@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.
Files changed (58) hide show
  1. package/.claude/commands/context.md +24 -0
  2. package/.claude/skills/agent-mem/SKILL.md +66 -0
  3. package/.claude/skills/agent-mem/references/branching-merging.md +34 -0
  4. package/.claude/skills/agent-mem/references/coexistence.md +19 -0
  5. package/.claude/skills/agent-mem/references/collaboration.md +33 -0
  6. package/.claude/skills/agent-mem/references/reflection-compaction.md +104 -0
  7. package/.claude/skills/agent-mem/references/sub-agent-patterns.md +60 -0
  8. package/LICENSE +21 -0
  9. package/README.md +235 -0
  10. package/bin/agent-context.js +95 -0
  11. package/bin/parse-args.js +85 -0
  12. package/package.json +58 -0
  13. package/src/commands/branch.js +57 -0
  14. package/src/commands/branch.test.js +91 -0
  15. package/src/commands/branches.js +34 -0
  16. package/src/commands/commit.js +55 -0
  17. package/src/commands/compact.js +307 -0
  18. package/src/commands/compact.test.js +110 -0
  19. package/src/commands/config.js +47 -0
  20. package/src/commands/core.test.js +166 -0
  21. package/src/commands/diff.js +157 -0
  22. package/src/commands/diff.test.js +64 -0
  23. package/src/commands/forget.js +77 -0
  24. package/src/commands/forget.test.js +68 -0
  25. package/src/commands/help.js +99 -0
  26. package/src/commands/import.js +83 -0
  27. package/src/commands/init.js +269 -0
  28. package/src/commands/init.test.js +80 -0
  29. package/src/commands/lesson.js +95 -0
  30. package/src/commands/lesson.test.js +93 -0
  31. package/src/commands/merge.js +105 -0
  32. package/src/commands/pin.js +34 -0
  33. package/src/commands/pull.js +80 -0
  34. package/src/commands/push.js +80 -0
  35. package/src/commands/read.js +62 -0
  36. package/src/commands/reflect.js +328 -0
  37. package/src/commands/remember.js +95 -0
  38. package/src/commands/resolve.js +230 -0
  39. package/src/commands/resolve.test.js +167 -0
  40. package/src/commands/search.js +70 -0
  41. package/src/commands/share.js +65 -0
  42. package/src/commands/snapshot.js +106 -0
  43. package/src/commands/status.js +37 -0
  44. package/src/commands/switch.js +31 -0
  45. package/src/commands/sync.js +328 -0
  46. package/src/commands/track.js +61 -0
  47. package/src/commands/unpin.js +28 -0
  48. package/src/commands/write.js +58 -0
  49. package/src/core/auto-commit.js +22 -0
  50. package/src/core/config.js +93 -0
  51. package/src/core/context-root.js +28 -0
  52. package/src/core/fs.js +137 -0
  53. package/src/core/git.js +182 -0
  54. package/src/core/importers.js +210 -0
  55. package/src/core/lock.js +62 -0
  56. package/src/core/reflect-defrag.js +287 -0
  57. package/src/core/reflect-gather.js +360 -0
  58. 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
+ }