@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,230 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { contextDir as getContextDir } from '../core/context-root.js';
4
+ import { commitContext } from '../core/git.js';
5
+ import { execSync } from 'node:child_process';
6
+
7
+ /**
8
+ * Find files with merge conflict markers in .context/.
9
+ */
10
+ function findConflicted(ctxDir, verbose = false) {
11
+ const conflicted = [];
12
+ const walk = (dir) => {
13
+ if (!existsSync(dir)) return;
14
+ for (const name of readdirSync(dir)) {
15
+ if (name.startsWith('.')) continue;
16
+ const full = join(dir, name);
17
+ try {
18
+ const stat = statSync(full);
19
+ if (stat.isDirectory()) {
20
+ walk(full);
21
+ } else {
22
+ const content = readFileSync(full, 'utf-8');
23
+ // Check for actual conflict block structure (not just substrings)
24
+ if (
25
+ /^<<<<<<<\s/m.test(content) &&
26
+ /^=======/m.test(content) &&
27
+ /^>>>>>>>\s/m.test(content)
28
+ ) {
29
+ const relPath = full.slice(ctxDir.length + 1);
30
+ conflicted.push({ path: relPath, fullPath: full, content });
31
+ }
32
+ }
33
+ } catch (err) {
34
+ if (verbose) console.error(` ⚠️ Could not read ${full}: ${err.message}`);
35
+ }
36
+ }
37
+ };
38
+ walk(ctxDir);
39
+ return conflicted;
40
+ }
41
+
42
+ /**
43
+ * Resolve a single file's merge conflicts.
44
+ *
45
+ * Strategy for .context/ files:
46
+ * - Memory files (append-only entries): keep BOTH sides, deduplicate
47
+ * - Config files: prefer "ours" (local) for settings
48
+ * - Other files: keep both sides concatenated with separator
49
+ */
50
+ function resolveFile(relPath, content) {
51
+ const isMemory = relPath.startsWith('memory/') || relPath.startsWith('archive/');
52
+ const isConfig = relPath === 'config.yaml';
53
+
54
+ if (isConfig) {
55
+ return resolveConfigConflict(content);
56
+ }
57
+
58
+ if (isMemory) {
59
+ return resolveAppendOnly(content);
60
+ }
61
+
62
+ // Default: keep both sides
63
+ return resolveKeepBoth(content);
64
+ }
65
+
66
+ /**
67
+ * Resolve append-only files (memory) by merging both sides.
68
+ * Deduplicates entries that are exact matches (preserves blank lines and whitespace).
69
+ */
70
+ function resolveAppendOnly(content) {
71
+ const lines = content.split('\n');
72
+ const resolved = [];
73
+ let inConflict = false;
74
+ let oursLines = [];
75
+ let theirsLines = [];
76
+ let side = null;
77
+
78
+ for (const line of lines) {
79
+ if (line.startsWith('<<<<<<<')) {
80
+ inConflict = true;
81
+ side = 'ours';
82
+ oursLines = [];
83
+ theirsLines = [];
84
+ continue;
85
+ }
86
+ if (line === '=======' && inConflict) {
87
+ side = 'theirs';
88
+ continue;
89
+ }
90
+ if (line.startsWith('>>>>>>>') && inConflict) {
91
+ // Merge: keep all lines from ours, then unique lines from theirs
92
+ // Exact string match for dedup (not trimmed)
93
+ const seen = new Set(oursLines);
94
+ const merged = [...oursLines];
95
+
96
+ for (const l of theirsLines) {
97
+ if (!seen.has(l)) {
98
+ seen.add(l);
99
+ merged.push(l);
100
+ }
101
+ }
102
+
103
+ resolved.push(...merged);
104
+ inConflict = false;
105
+ side = null;
106
+ continue;
107
+ }
108
+
109
+ if (inConflict) {
110
+ if (side === 'ours') oursLines.push(line);
111
+ else theirsLines.push(line);
112
+ } else {
113
+ resolved.push(line);
114
+ }
115
+ }
116
+
117
+ return resolved.join('\n');
118
+ }
119
+
120
+ /**
121
+ * Resolve config conflicts by preferring "ours" (local) values.
122
+ */
123
+ function resolveConfigConflict(content) {
124
+ const lines = content.split('\n');
125
+ const resolved = [];
126
+ let inConflict = false;
127
+ let side = null;
128
+
129
+ for (const line of lines) {
130
+ if (line.startsWith('<<<<<<<')) {
131
+ inConflict = true;
132
+ side = 'ours';
133
+ continue;
134
+ }
135
+ if (line === '=======' && inConflict) {
136
+ side = 'theirs';
137
+ continue;
138
+ }
139
+ if (line.startsWith('>>>>>>>') && inConflict) {
140
+ inConflict = false;
141
+ side = null;
142
+ continue;
143
+ }
144
+
145
+ if (inConflict) {
146
+ // Keep only "ours" lines
147
+ if (side === 'ours') {
148
+ resolved.push(line);
149
+ }
150
+ } else {
151
+ resolved.push(line);
152
+ }
153
+ }
154
+
155
+ return resolved.join('\n');
156
+ }
157
+
158
+ /**
159
+ * Fallback: keep both sides separated.
160
+ */
161
+ function resolveKeepBoth(content) {
162
+ // Just strip conflict markers and keep everything
163
+ return content
164
+ .replace(/^<<<<<<<.*\n/gm, '')
165
+ .replace(/^=======\n/gm, '\n--- merged ---\n\n')
166
+ .replace(/^>>>>>>>.*\n/gm, '');
167
+ }
168
+
169
+ /**
170
+ * Get resolution strategy label for a file path.
171
+ */
172
+ function getStrategy(relPath) {
173
+ if (relPath.startsWith('memory/') || relPath.startsWith('archive/')) {
174
+ return 'append-only (merge both, deduplicate)';
175
+ }
176
+ if (relPath === 'config.yaml') {
177
+ return 'prefer-ours (keep local config)';
178
+ }
179
+ return 'keep-both (concatenate)';
180
+ }
181
+
182
+ export default async function resolve({ args, flags }) {
183
+ const root = flags._contextRoot;
184
+ const ctxDir = getContextDir(root);
185
+ const isDryRun = flags['dry-run'] === true;
186
+
187
+ const verbose = flags.verbose === true;
188
+ const conflicted = findConflicted(ctxDir, verbose);
189
+
190
+ if (conflicted.length === 0) {
191
+ console.log('✅ No merge conflicts in .context/');
192
+ return;
193
+ }
194
+
195
+ console.log(`🔀 RESOLVE${isDryRun ? ' (dry run)' : ''}`);
196
+ console.log(`Found ${conflicted.length} conflicted file${conflicted.length > 1 ? 's' : ''}:`);
197
+ console.log('');
198
+
199
+ for (const file of conflicted) {
200
+ const strategy = getStrategy(file.path);
201
+
202
+ console.log(` ${file.path} — ${strategy}`);
203
+
204
+ if (!isDryRun) {
205
+ const resolved = resolveFile(file.path, file.content);
206
+ writeFileSync(file.fullPath, resolved, 'utf-8');
207
+ }
208
+ }
209
+
210
+ if (!isDryRun) {
211
+ // Stage resolved files
212
+ try {
213
+ execSync('git add .', { cwd: ctxDir, stdio: 'ignore' });
214
+ } catch (err) {
215
+ if (verbose) console.error(` ⚠️ git add failed: ${err.message}`);
216
+ }
217
+
218
+ const hash = commitContext(
219
+ ctxDir,
220
+ `resolve: auto-resolved ${conflicted.length} conflict${conflicted.length > 1 ? 's' : ''}`,
221
+ );
222
+
223
+ console.log('');
224
+ console.log(`✅ Resolved ${conflicted.length} file${conflicted.length > 1 ? 's' : ''}`);
225
+ if (hash) console.log(`Committed: ${hash}`);
226
+ } else {
227
+ console.log('');
228
+ console.log('Run without --dry-run to apply resolutions.');
229
+ }
230
+ }
@@ -0,0 +1,167 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, 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
+ function makeConflict(ours, theirs) {
15
+ return `<<<<<<< HEAD\n${ours}\n=======\n${theirs}\n>>>>>>> branch-b`;
16
+ }
17
+
18
+ describe('resolve', () => {
19
+ let dir;
20
+
21
+ before(() => {
22
+ dir = mkdtempSync(join(tmpdir(), 'amem-resolve-'));
23
+ execSync('git init', { cwd: dir, stdio: 'ignore' });
24
+ execSync('git config user.name "test"', { cwd: dir, stdio: 'ignore' });
25
+ execSync('git config user.email "test@test.com"', { cwd: dir, stdio: 'ignore' });
26
+ run('init', dir);
27
+ });
28
+
29
+ after(() => {
30
+ rmSync(dir, { recursive: true, force: true });
31
+ });
32
+
33
+ it('reports no conflicts when clean', () => {
34
+ const out = run('resolve', dir);
35
+ assert.match(out, /No merge conflicts/);
36
+ });
37
+
38
+ it('resolves memory file conflicts by keeping both sides deduplicated', () => {
39
+ const memFile = join(dir, '.context', 'memory', 'decisions.md');
40
+ writeFileSync(
41
+ memFile,
42
+ [
43
+ '# Decisions',
44
+ '',
45
+ '- [2026-01-01] Shared decision',
46
+ makeConflict('- [2026-02-01] Our local decision', '- [2026-02-01] Their remote decision'),
47
+ ].join('\n'),
48
+ );
49
+
50
+ const out = run('resolve', dir);
51
+ assert.match(out, /Resolved/);
52
+ assert.match(out, /append-only/);
53
+
54
+ const resolved = readFileSync(memFile, 'utf-8');
55
+ assert.ok(!resolved.includes('<<<<<<<'));
56
+ assert.ok(!resolved.includes('>>>>>>>'));
57
+ assert.match(resolved, /Our local decision/);
58
+ assert.match(resolved, /Their remote decision/);
59
+ assert.match(resolved, /Shared decision/);
60
+ });
61
+
62
+ it('deduplicates exact matches in append-only merge', () => {
63
+ const memFile = join(dir, '.context', 'memory', 'patterns.md');
64
+ mkdirSync(join(dir, '.context', 'memory'), { recursive: true });
65
+ writeFileSync(
66
+ memFile,
67
+ [
68
+ '# Patterns',
69
+ makeConflict(
70
+ '- [2026-01-01] Same entry\n- [2026-01-02] Only ours',
71
+ '- [2026-01-01] Same entry\n- [2026-01-03] Only theirs',
72
+ ),
73
+ ].join('\n'),
74
+ );
75
+
76
+ run('resolve', dir);
77
+ const resolved = readFileSync(memFile, 'utf-8');
78
+
79
+ // "Same entry" should appear exactly once
80
+ const count = (resolved.match(/Same entry/g) || []).length;
81
+ assert.equal(count, 1, 'duplicate entry should appear only once');
82
+ assert.match(resolved, /Only ours/);
83
+ assert.match(resolved, /Only theirs/);
84
+ });
85
+
86
+ it('preserves whitespace differences (no trim-based dedupe)', () => {
87
+ const memFile = join(dir, '.context', 'memory', 'ws-test.md');
88
+ writeFileSync(
89
+ memFile,
90
+ makeConflict('- [2026-01-01] spaced entry', '- [2026-01-01] spaced entry'),
91
+ );
92
+
93
+ run('resolve', dir);
94
+ const resolved = readFileSync(memFile, 'utf-8');
95
+ // Both should be kept — different exact strings
96
+ const count = (resolved.match(/spaced entry/g) || []).length;
97
+ assert.equal(count, 2, 'whitespace-different lines should both be kept');
98
+ });
99
+
100
+ it('resolves config.yaml with prefer-ours strategy', () => {
101
+ const configFile = join(dir, '.context', 'config.yaml');
102
+ writeFileSync(
103
+ configFile,
104
+ [
105
+ 'auto_commit: true',
106
+ makeConflict('branch: feature-a', 'branch: feature-b'),
107
+ 'system_files_max: 10',
108
+ ].join('\n'),
109
+ );
110
+
111
+ run('resolve', dir);
112
+ const resolved = readFileSync(configFile, 'utf-8');
113
+ assert.ok(!resolved.includes('<<<<<<<'));
114
+ assert.match(resolved, /feature-a/, 'should keep ours');
115
+ assert.ok(!resolved.includes('feature-b'), 'should drop theirs');
116
+ assert.match(resolved, /auto_commit: true/);
117
+ assert.match(resolved, /system_files_max: 10/);
118
+ });
119
+
120
+ it('resolves other files with keep-both strategy', () => {
121
+ const otherFile = join(dir, '.context', 'main.md');
122
+ writeFileSync(
123
+ otherFile,
124
+ ['# Project', makeConflict('## Our section', '## Their section')].join('\n'),
125
+ );
126
+
127
+ run('resolve', dir);
128
+ const resolved = readFileSync(otherFile, 'utf-8');
129
+ assert.ok(!resolved.includes('<<<<<<<'));
130
+ assert.match(resolved, /Our section/);
131
+ assert.match(resolved, /Their section/);
132
+ assert.match(resolved, /merged/);
133
+ });
134
+
135
+ it('handles multiple conflict blocks in one file', () => {
136
+ const memFile = join(dir, '.context', 'memory', 'multi.md');
137
+ writeFileSync(
138
+ memFile,
139
+ [
140
+ '# Multi',
141
+ makeConflict('- block1-ours', '- block1-theirs'),
142
+ '- shared middle line',
143
+ makeConflict('- block2-ours', '- block2-theirs'),
144
+ ].join('\n'),
145
+ );
146
+
147
+ run('resolve', dir);
148
+ const resolved = readFileSync(memFile, 'utf-8');
149
+ assert.ok(!resolved.includes('<<<<<<<'));
150
+ assert.match(resolved, /block1-ours/);
151
+ assert.match(resolved, /block1-theirs/);
152
+ assert.match(resolved, /shared middle line/);
153
+ assert.match(resolved, /block2-ours/);
154
+ assert.match(resolved, /block2-theirs/);
155
+ });
156
+
157
+ it('dry-run shows strategy without modifying files', () => {
158
+ const memFile = join(dir, '.context', 'memory', 'drytest.md');
159
+ writeFileSync(memFile, makeConflict('ours', 'theirs'));
160
+
161
+ const out = run('resolve --dry-run', dir);
162
+ assert.match(out, /dry run/);
163
+
164
+ const content = readFileSync(memFile, 'utf-8');
165
+ assert.ok(content.includes('<<<<<<<'), 'should not modify file in dry-run');
166
+ });
167
+ });
@@ -0,0 +1,70 @@
1
+ import { contextDir as getContextDir } from '../core/context-root.js';
2
+ import { buildTree, readContextFile } from '../core/fs.js';
3
+ import { readConfig } from '../core/config.js';
4
+
5
+ export default async function search({ args, flags }) {
6
+ const root = flags._contextRoot;
7
+ const ctxDir = getContextDir(root);
8
+
9
+ if (!args.length) {
10
+ console.error('❌ Usage: agent-mem search <query>');
11
+ process.exit(1);
12
+ }
13
+
14
+ const config = readConfig(ctxDir);
15
+ const branch = config.branch || 'main';
16
+ const query = args.join(' ').toLowerCase();
17
+ const tree = buildTree(ctxDir);
18
+
19
+ // When on a branch, search branch memory first, then system/ and global memory
20
+ // Skip global memory/ duplicates to avoid noise
21
+ const files = tree.filter((e) => !e.isDir);
22
+ const branchMemPrefix = branch !== 'main' ? `branches/${branch}/memory/` : null;
23
+
24
+ const searchFiles = branchMemPrefix
25
+ ? files.filter((f) => {
26
+ // Include branch memory, system/, reflections, config — skip global memory/
27
+ if (f.path.startsWith(branchMemPrefix)) return true;
28
+ if (f.path.startsWith('memory/')) return false;
29
+ return true;
30
+ })
31
+ : files;
32
+
33
+ const results = [];
34
+
35
+ for (const file of searchFiles) {
36
+ const content = readContextFile(ctxDir, file.path);
37
+ if (!content) continue;
38
+
39
+ const lines = content.split('\n');
40
+ for (let i = 0; i < lines.length; i++) {
41
+ if (lines[i].toLowerCase().includes(query)) {
42
+ results.push({
43
+ path: file.path,
44
+ line: i + 1,
45
+ text: lines[i].trim(),
46
+ });
47
+ }
48
+ }
49
+ }
50
+
51
+ if (!results.length) {
52
+ console.log(`🔍 SEARCH: "${args.join(' ')}"\nNo results found.`);
53
+ return;
54
+ }
55
+
56
+ const branchLabel = branchMemPrefix ? ` [branch: ${branch}]` : '';
57
+ console.log(`🔍 SEARCH${branchLabel}: "${args.join(' ')}" (${results.length} matches)\n`);
58
+
59
+ // Group by file, show top 20
60
+ const shown = results.slice(0, 20);
61
+ for (let i = 0; i < shown.length; i++) {
62
+ const r = shown[i];
63
+ const preview = r.text.length > 100 ? r.text.slice(0, 97) + '...' : r.text;
64
+ console.log(`${i + 1}. ${r.path}:${r.line} — ${preview}`);
65
+ }
66
+
67
+ if (results.length > 20) {
68
+ console.log(`\n... and ${results.length - 20} more matches`);
69
+ }
70
+ }
@@ -0,0 +1,65 @@
1
+ import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { contextDir as getContextDir } from '../core/context-root.js';
5
+ import { buildTree, readContextFile } from '../core/fs.js';
6
+ import { readConfig } from '../core/config.js';
7
+ import { commitCount, lastCommit } from '../core/git.js';
8
+
9
+ /**
10
+ * Generate a portable context snapshot that can be shared with anyone.
11
+ *
12
+ * agent-mem share — generate snapshot file
13
+ * agent-mem share --output <path> — custom output path
14
+ * agent-mem import <file-or-url> — import a shared snapshot
15
+ */
16
+ export default async function share({ args, flags }) {
17
+ const root = flags._contextRoot;
18
+ const ctxDir = getContextDir(root);
19
+
20
+ const config = readConfig(ctxDir);
21
+ const projectName = basename(root);
22
+ const commits = commitCount(ctxDir);
23
+ const last = lastCommit(ctxDir);
24
+
25
+ // Gather all context into a single portable object
26
+ const tree = buildTree(ctxDir);
27
+ const files = {};
28
+
29
+ for (const entry of tree) {
30
+ if (entry.isDir) continue;
31
+ const content = readContextFile(ctxDir, entry.path);
32
+ if (content !== null) {
33
+ files[entry.path] = content;
34
+ }
35
+ }
36
+
37
+ const snapshot = {
38
+ version: 1,
39
+ tool: 'agent-mem',
40
+ project: projectName,
41
+ branch: config.branch || 'main',
42
+ commits,
43
+ lastCommit: last,
44
+ createdAt: new Date().toISOString(),
45
+ files,
46
+ };
47
+
48
+ const json = JSON.stringify(snapshot, null, 2);
49
+ const hash = createHash('sha256').update(json).digest('hex').slice(0, 8);
50
+ const filename = `context-${projectName}-${hash}.json`;
51
+
52
+ const outputPath = flags.output || join(root, filename);
53
+ writeFileSync(outputPath, json);
54
+
55
+ const fileCount = Object.keys(files).length;
56
+ const sizeKb = (Buffer.byteLength(json) / 1024).toFixed(1);
57
+
58
+ console.log(`✅ SHARED: ${filename}`);
59
+ console.log(` Project: ${projectName}`);
60
+ console.log(` Files: ${fileCount} | Size: ${sizeKb} KB | Commits: ${commits}`);
61
+ console.log(` Path: ${outputPath}`);
62
+ console.log('');
63
+ console.log('To import on another machine:');
64
+ console.log(` agent-mem import ${filename}`);
65
+ }
@@ -0,0 +1,106 @@
1
+ import { join } from 'node:path';
2
+ import { contextDir as getContextDir } from '../core/context-root.js';
3
+ import { buildTree, readContextFile, countFiles } from '../core/fs.js';
4
+ import { readConfig } from '../core/config.js';
5
+ import { commitCount, lastCommit, hasChanges } from '../core/git.js';
6
+
7
+ export default async function snapshot({ args, flags }) {
8
+ const root = flags._contextRoot;
9
+ const ctxDir = getContextDir(root);
10
+ const config = readConfig(ctxDir);
11
+ const projectName = root.split('/').pop();
12
+
13
+ const commits = commitCount(ctxDir);
14
+ const last = lastCommit(ctxDir);
15
+ const dirty = hasChanges(ctxDir);
16
+ const branch = config.branch || 'main';
17
+
18
+ const tree = buildTree(ctxDir);
19
+
20
+ // Header
21
+ const lines = [
22
+ `📋 CONTEXT SNAPSHOT`,
23
+ `Project: ${projectName} | Branch: ${branch} | Commits: ${commits}${dirty ? ' (uncommitted changes)' : ''}`,
24
+ last ? `Last commit: "${last.message}" (${last.timeAgo})` : '',
25
+ '',
26
+ ];
27
+
28
+ // System files (pinned — show full content)
29
+ const systemFiles = tree.filter((e) => e.path.startsWith('system/') && !e.isDir);
30
+ if (systemFiles.length) {
31
+ lines.push('PINNED (system/) — always in agent context:');
32
+ for (const f of systemFiles) {
33
+ const content = readContextFile(ctxDir, f.path);
34
+ if (content) {
35
+ // Strip frontmatter for display
36
+ const stripped = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
37
+ const preview = stripped.length > 500 ? stripped.slice(0, 497) + '...' : stripped;
38
+ lines.push(` --- ${f.path} ---`);
39
+ lines.push(` ${preview.split('\n').join('\n ')}`);
40
+ lines.push('');
41
+ }
42
+ }
43
+ }
44
+
45
+ // Memory files — branch-scoped when on a branch
46
+ const branchMemPrefix = branch !== 'main' ? `branches/${branch}/memory/` : null;
47
+ const branchMemFiles = branchMemPrefix
48
+ ? tree.filter((e) => e.path.startsWith(branchMemPrefix) && !e.isDir)
49
+ : [];
50
+ const globalMemFiles = tree.filter((e) => e.path.startsWith('memory/') && !e.isDir);
51
+
52
+ if (branchMemPrefix && branchMemFiles.length) {
53
+ lines.push(`MEMORY [branch: ${branch}] (${branchMemFiles.length} files):`);
54
+ for (const f of branchMemFiles) {
55
+ lines.push(` ${f.name} — ${f.description || '(no description)'}`);
56
+ }
57
+ lines.push('');
58
+ }
59
+
60
+ if (branchMemPrefix && globalMemFiles.length) {
61
+ lines.push(
62
+ `MEMORY [main] (${globalMemFiles.length} files — use 'amem read memory/<file>' for global):`,
63
+ );
64
+ for (const f of globalMemFiles) {
65
+ lines.push(` ${f.name} — ${f.description || '(no description)'}`);
66
+ }
67
+ lines.push('');
68
+ } else if (!branchMemPrefix && globalMemFiles.length) {
69
+ lines.push(`MEMORY (${globalMemFiles.length} files):`);
70
+ for (const f of globalMemFiles) {
71
+ lines.push(` ${f.name} — ${f.description || '(no description)'}`);
72
+ }
73
+ lines.push('');
74
+ }
75
+
76
+ // Branches
77
+ const branchEntries = tree.filter(
78
+ (e) => e.path.startsWith('branches/') && e.isDir && e.depth === 1,
79
+ );
80
+ if (branchEntries.length) {
81
+ lines.push(`BRANCHES (${branchEntries.length}):`);
82
+ for (const b of branchEntries) {
83
+ const purpose = readContextFile(ctxDir, `${b.path}/purpose.md`);
84
+ const purposeLine =
85
+ purpose?.split('\n').find((l) => l.trim() && !l.startsWith('#') && !l.startsWith('---')) ||
86
+ '';
87
+ lines.push(` ${b.name} — ${purposeLine.trim() || '(no purpose set)'}`);
88
+ }
89
+ lines.push('');
90
+ }
91
+
92
+ // Reflections
93
+ const reflectionFiles = tree.filter((e) => e.path.startsWith('reflections/') && !e.isDir);
94
+ if (reflectionFiles.length) {
95
+ const latest = reflectionFiles[reflectionFiles.length - 1];
96
+ lines.push(`REFLECTIONS: ${reflectionFiles.length} total, latest: ${latest.name}`);
97
+ lines.push('');
98
+ }
99
+
100
+ // Config summary
101
+ lines.push(
102
+ `CONFIG: auto_commit=${config.auto_commit} | reflection=${config.reflection?.trigger || 'manual'}`,
103
+ );
104
+
105
+ console.log(lines.filter((l) => l !== undefined).join('\n'));
106
+ }
@@ -0,0 +1,37 @@
1
+ import { contextDir as getContextDir } from '../core/context-root.js';
2
+ import { readConfig } from '../core/config.js';
3
+ import { commitCount, lastCommit, hasChanges } from '../core/git.js';
4
+ import { countFiles, listFiles } from '../core/fs.js';
5
+ import { existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ export default async function status({ args, flags }) {
9
+ const root = flags._contextRoot;
10
+ const ctxDir = getContextDir(root);
11
+ const config = readConfig(ctxDir);
12
+ const projectName = root.split('/').pop();
13
+
14
+ const commits = commitCount(ctxDir);
15
+ const last = lastCommit(ctxDir);
16
+ const dirty = hasChanges(ctxDir);
17
+ const systemCount = countFiles(ctxDir, 'system');
18
+ const memoryCount = countFiles(ctxDir, 'memory');
19
+ const reflectionCount = countFiles(ctxDir, 'reflections');
20
+
21
+ // Count branches
22
+ const branchDir = join(ctxDir, 'branches');
23
+ const branchCount = existsSync(branchDir) ? listFiles(ctxDir, 'branches').length : 0;
24
+
25
+ console.log(`📊 STATUS: ${projectName}`);
26
+ console.log(
27
+ `Branch: ${config.branch || 'main'} | Commits: ${commits}${dirty ? ' ⚠️ uncommitted changes' : ''}`,
28
+ );
29
+ if (last) console.log(`Last: "${last.message}" (${last.timeAgo})`);
30
+ console.log(
31
+ `Files: ${systemCount} pinned, ${memoryCount} memory, ${reflectionCount} reflections`,
32
+ );
33
+ console.log(`Branches: ${branchCount}`);
34
+ console.log(
35
+ `Config: auto_commit=${config.auto_commit} | reflection=${config.reflection?.trigger || 'manual'}`,
36
+ );
37
+ }