@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,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * agent-mem — Context management CLI for AI coding agents
5
+ *
6
+ * Usage:
7
+ * agent-mem init Bootstrap context from codebase
8
+ * agent-mem snapshot Get context tree (agent's primary view)
9
+ * agent-mem read <path> Read a specific context file
10
+ * agent-mem write <path> Write/update a context file
11
+ * agent-mem commit [msg] Checkpoint progress
12
+ * agent-mem status Quick status overview
13
+ * agent-mem remember Quick-add to memory
14
+ * agent-mem search Search across context
15
+ * agent-mem branch Create exploration branch
16
+ * agent-mem switch Switch active branch
17
+ * agent-mem merge Merge branch back
18
+ * agent-mem branches List branches
19
+ * agent-mem pin Move file to system/ (always loaded)
20
+ * agent-mem unpin Move file out of system/
21
+ * agent-mem config Show/set configuration
22
+ * agent-mem help Show help
23
+ */
24
+
25
+ import { resolve } from 'node:path';
26
+ import { parseArgs } from './parse-args.js';
27
+ import { findContextRoot } from '../src/core/context-root.js';
28
+
29
+ const COMMANDS = {
30
+ init: () => import('../src/commands/init.js'),
31
+ snapshot: () => import('../src/commands/snapshot.js'),
32
+ read: () => import('../src/commands/read.js'),
33
+ write: () => import('../src/commands/write.js'),
34
+ commit: () => import('../src/commands/commit.js'),
35
+ status: () => import('../src/commands/status.js'),
36
+ remember: () => import('../src/commands/remember.js'),
37
+ search: () => import('../src/commands/search.js'),
38
+ branch: () => import('../src/commands/branch.js'),
39
+ switch: () => import('../src/commands/switch.js'),
40
+ merge: () => import('../src/commands/merge.js'),
41
+ branches: () => import('../src/commands/branches.js'),
42
+ pin: () => import('../src/commands/pin.js'),
43
+ unpin: () => import('../src/commands/unpin.js'),
44
+ reflect: () => import('../src/commands/reflect.js'),
45
+ compact: () => import('../src/commands/compact.js'),
46
+ resolve: () => import('../src/commands/resolve.js'),
47
+ diff: () => import('../src/commands/diff.js'),
48
+ forget: () => import('../src/commands/forget.js'),
49
+ lesson: () => import('../src/commands/lesson.js'),
50
+ sync: () => import('../src/commands/sync.js'),
51
+ track: () => import('../src/commands/track.js'),
52
+ push: () => import('../src/commands/push.js'),
53
+ pull: () => import('../src/commands/pull.js'),
54
+ share: () => import('../src/commands/share.js'),
55
+ import: () => import('../src/commands/import.js'),
56
+ config: () => import('../src/commands/config.js'),
57
+ help: () => import('../src/commands/help.js'),
58
+ };
59
+
60
+ async function main() {
61
+ const { command, args, flags } = parseArgs(process.argv.slice(2));
62
+
63
+ if (!command || command === 'help' || flags.help || flags.h) {
64
+ const mod = await COMMANDS.help();
65
+ await mod.default({ args, flags });
66
+ process.exit(0);
67
+ }
68
+
69
+ if (!(command in COMMANDS)) {
70
+ console.error(`❌ Unknown command: ${command}`);
71
+ console.error(`Run 'agent-mem help' for available commands.`);
72
+ process.exit(1);
73
+ }
74
+
75
+ // All commands except init, help, pull, import require an existing .context/
76
+ if (!['init', 'help', 'pull', 'import'].includes(command)) {
77
+ const root = findContextRoot(process.cwd());
78
+ if (!root) {
79
+ console.error(`❌ No .context/ found. Run 'agent-mem init' first.`);
80
+ process.exit(1);
81
+ }
82
+ flags._contextRoot = root;
83
+ }
84
+
85
+ try {
86
+ const mod = await COMMANDS[command]();
87
+ await mod.default({ args, flags });
88
+ } catch (err) {
89
+ console.error(`❌ ${err.message}`);
90
+ if (flags.verbose) console.error(err.stack);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ main();
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Minimal argument parser. Zero deps.
3
+ *
4
+ * parseArgs(["init", "--from-claude", "--verbose"])
5
+ * → { command: "init", args: [], flags: { "from-claude": true, verbose: true } }
6
+ *
7
+ * parseArgs(["read", "memory/decisions.md"])
8
+ * → { command: "read", args: ["memory/decisions.md"], flags: {} }
9
+ *
10
+ * parseArgs(["commit", "implemented auth flow"])
11
+ * → { command: "commit", args: ["implemented", "auth", "flow"], flags: {} }
12
+ */
13
+
14
+ export function parseArgs(argv) {
15
+ const flags = {};
16
+ const args = [];
17
+ let command = null;
18
+
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i];
21
+
22
+ if (!command && !arg.startsWith('-')) {
23
+ command = arg;
24
+ continue;
25
+ }
26
+
27
+ if (arg.startsWith('--')) {
28
+ const key = arg.slice(2);
29
+ const eqIdx = key.indexOf('=');
30
+ if (eqIdx !== -1) {
31
+ flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
32
+ } else {
33
+ // Boolean-only flags (never consume next arg as value)
34
+ const BOOLEAN_FLAGS = new Set([
35
+ 'help',
36
+ 'h',
37
+ 'verbose',
38
+ 'force',
39
+ 'deep',
40
+ 'compare',
41
+ 'landscape',
42
+ 'from-claude',
43
+ 'from-codex',
44
+ 'json',
45
+ 'no-fetch',
46
+ 'decision',
47
+ 'pattern',
48
+ 'mistake',
49
+ 'note',
50
+ 'all',
51
+ 'claude',
52
+ 'gemini',
53
+ 'codex',
54
+ 'cursor',
55
+ 'windsurf',
56
+ 'enable',
57
+ 'disable',
58
+ 'merge',
59
+ 'dry-run',
60
+ 'dry-run',
61
+ 'compaction',
62
+ 'hard',
63
+ ]);
64
+ if (BOOLEAN_FLAGS.has(key)) {
65
+ flags[key] = true;
66
+ } else {
67
+ // Check if next arg is a value
68
+ const next = argv[i + 1];
69
+ if (next && !next.startsWith('-')) {
70
+ flags[key] = next;
71
+ i++;
72
+ } else {
73
+ flags[key] = true;
74
+ }
75
+ }
76
+ }
77
+ } else if (arg.startsWith('-') && arg.length === 2) {
78
+ flags[arg.slice(1)] = true;
79
+ } else {
80
+ args.push(arg);
81
+ }
82
+ }
83
+
84
+ return { command, args, flags };
85
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@lmaksym/agent-mem",
3
+ "version": "0.1.0",
4
+ "description": "Persistent, git-backed memory for AI coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-mem": "./bin/agent-context.js",
8
+ "amem": "./bin/agent-context.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/agent-context.js",
12
+ "test": "node --test src/**/*.test.js",
13
+ "format": "prettier --write 'src/**/*.js' 'bin/**/*.js'",
14
+ "format:check": "prettier --check 'src/**/*.js' 'bin/**/*.js'",
15
+ "prepare": "husky"
16
+ },
17
+ "lint-staged": {
18
+ "*.js": "prettier --write"
19
+ },
20
+ "keywords": [
21
+ "ai",
22
+ "agent",
23
+ "context",
24
+ "memory",
25
+ "cli",
26
+ "claude-code",
27
+ "codex",
28
+ "cursor",
29
+ "coding-agent",
30
+ "agent-skills"
31
+ ],
32
+ "author": "Maksym Lypivskyi",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/lmaksym/agent-context.git"
37
+ },
38
+ "homepage": "https://github.com/lmaksym/agent-context",
39
+ "bugs": {
40
+ "url": "https://github.com/lmaksym/agent-context/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "files": [
46
+ "bin/",
47
+ "src/",
48
+ ".claude/commands/",
49
+ ".claude/skills/",
50
+ "LICENSE",
51
+ "README.md"
52
+ ],
53
+ "devDependencies": {
54
+ "husky": "^9.1.7",
55
+ "lint-staged": "^16.2.7",
56
+ "prettier": "^3.8.1"
57
+ }
58
+ }
@@ -0,0 +1,57 @@
1
+ import { contextDir as getContextDir } from '../core/context-root.js';
2
+ import { writeContextFile, readContextFile } from '../core/fs.js';
3
+ import { readConfig, writeConfig } from '../core/config.js';
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export default async function branch({ args, flags }) {
8
+ const root = flags._contextRoot;
9
+ const ctxDir = getContextDir(root);
10
+
11
+ if (!args.length) {
12
+ console.error('❌ Usage: agent-mem branch <name> [purpose]');
13
+ console.error('Example: agent-mem branch try-qdrant "evaluate vector search"');
14
+ process.exit(1);
15
+ }
16
+
17
+ const name = args[0];
18
+ const purpose = args.slice(1).join(' ') || '';
19
+ const branchDir = join(ctxDir, 'branches', name);
20
+
21
+ if (existsSync(branchDir)) {
22
+ console.error(`❌ Branch "${name}" already exists.`);
23
+ process.exit(1);
24
+ }
25
+
26
+ const date = new Date().toISOString().slice(0, 10);
27
+
28
+ writeContextFile(
29
+ ctxDir,
30
+ `branches/${name}/purpose.md`,
31
+ [`# Branch: ${name}`, '', purpose || 'Purpose not specified.', '', `Created: ${date}`, ''].join(
32
+ '\n',
33
+ ),
34
+ );
35
+
36
+ writeContextFile(
37
+ ctxDir,
38
+ `branches/${name}/commits.md`,
39
+ [`# Commits: ${name}`, '', 'Milestone log for this branch.', ''].join('\n'),
40
+ );
41
+
42
+ writeContextFile(
43
+ ctxDir,
44
+ `branches/${name}/trace.md`,
45
+ [`# Trace: ${name}`, '', 'Fine-grained execution log.', ''].join('\n'),
46
+ );
47
+
48
+ // Switch to new branch
49
+ const config = readConfig(ctxDir);
50
+ config.branch = name;
51
+ writeConfig(ctxDir, config);
52
+
53
+ console.log(`✅ BRANCHED: ${name}`);
54
+ if (purpose) console.log(`Purpose: ${purpose}`);
55
+ console.log(`Switched to branch: ${name}`);
56
+ console.log(`Files: branches/${name}/{purpose,commits,trace}.md`);
57
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } 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
+ const run = (args, cwd) =>
10
+ execSync(`node ${CLI} ${args}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
11
+
12
+ describe('branch / switch / merge / branches', () => {
13
+ let dir;
14
+
15
+ before(() => {
16
+ dir = mkdtempSync(join(tmpdir(), 'amem-test-branch-'));
17
+ writeFileSync(join(dir, 'package.json'), '{}');
18
+ run('init', dir);
19
+ });
20
+
21
+ after(() => rmSync(dir, { recursive: true, force: true }));
22
+
23
+ it('creates a branch with purpose', () => {
24
+ const out = run('branch try-redis "evaluate Redis for caching"', dir);
25
+ assert.ok(out.includes('BRANCHED: try-redis'));
26
+ assert.ok(existsSync(join(dir, '.context/branches/try-redis/purpose.md')));
27
+ assert.ok(existsSync(join(dir, '.context/branches/try-redis/commits.md')));
28
+ assert.ok(existsSync(join(dir, '.context/branches/try-redis/trace.md')));
29
+ });
30
+
31
+ it('switches config to new branch', () => {
32
+ const config = readFileSync(join(dir, '.context/config.yaml'), 'utf-8');
33
+ assert.ok(config.includes('branch: try-redis'));
34
+ });
35
+
36
+ it('lists branches', () => {
37
+ const out = run('branches', dir);
38
+ assert.ok(out.includes('try-redis'));
39
+ assert.ok(out.includes('evaluate Redis'));
40
+ });
41
+
42
+ it('switches back to main', () => {
43
+ const out = run('switch main', dir);
44
+ assert.ok(out.includes('SWITCHED'));
45
+ assert.ok(out.includes('main'));
46
+ });
47
+
48
+ it('refuses duplicate branch', () => {
49
+ assert.throws(() => run('branch try-redis', dir));
50
+ });
51
+
52
+ it('merges branch back', () => {
53
+ const out = run('merge try-redis "Redis too complex, using in-memory cache"', dir);
54
+ assert.ok(out.includes('MERGED: try-redis'));
55
+ assert.ok(existsSync(join(dir, '.context/memory/decisions.md')));
56
+ const decisions = readFileSync(join(dir, '.context/memory/decisions.md'), 'utf-8');
57
+ assert.ok(decisions.includes('Redis too complex'));
58
+ });
59
+
60
+ it('switches to main after merge', () => {
61
+ const config = readFileSync(join(dir, '.context/config.yaml'), 'utf-8');
62
+ assert.ok(config.includes('branch: main'));
63
+ });
64
+ });
65
+
66
+ describe('pin / unpin', () => {
67
+ let dir;
68
+
69
+ before(() => {
70
+ dir = mkdtempSync(join(tmpdir(), 'amem-test-pin-'));
71
+ writeFileSync(join(dir, 'package.json'), '{}');
72
+ run('init', dir);
73
+ run('remember --decision "test decision"', dir);
74
+ });
75
+
76
+ after(() => rmSync(dir, { recursive: true, force: true }));
77
+
78
+ it('pins a memory file to system/', () => {
79
+ const out = run('pin memory/decisions.md', dir);
80
+ assert.ok(out.includes('PINNED'));
81
+ assert.ok(existsSync(join(dir, '.context/system/decisions.md')));
82
+ assert.ok(!existsSync(join(dir, '.context/memory/decisions.md')));
83
+ });
84
+
85
+ it('unpins a system file to memory/', () => {
86
+ const out = run('unpin system/decisions.md', dir);
87
+ assert.ok(out.includes('UNPINNED'));
88
+ assert.ok(existsSync(join(dir, '.context/memory/decisions.md')));
89
+ assert.ok(!existsSync(join(dir, '.context/system/decisions.md')));
90
+ });
91
+ });
@@ -0,0 +1,34 @@
1
+ import { contextDir as getContextDir } from '../core/context-root.js';
2
+ import { listFiles, readContextFile } from '../core/fs.js';
3
+ import { readConfig } from '../core/config.js';
4
+
5
+ export default async function branches({ args, flags }) {
6
+ const root = flags._contextRoot;
7
+ const ctxDir = getContextDir(root);
8
+ const config = readConfig(ctxDir);
9
+ const current = config.branch || 'main';
10
+
11
+ const branchNames = listFiles(ctxDir, 'branches');
12
+
13
+ console.log(`📂 BRANCHES:`);
14
+ console.log(` ${current === 'main' ? '* ' : ' '}main (default)`);
15
+
16
+ if (!branchNames.length) {
17
+ console.log('\n No exploration branches yet.');
18
+ console.log(' Create one: agent-mem branch <name> "purpose"');
19
+ return;
20
+ }
21
+
22
+ for (const name of branchNames) {
23
+ const purpose = readContextFile(ctxDir, `branches/${name}/purpose.md`);
24
+ const purposeLine =
25
+ purpose
26
+ ?.split('\n')
27
+ .find(
28
+ (l) =>
29
+ l.trim() && !l.startsWith('#') && !l.startsWith('---') && !l.startsWith('Created:'),
30
+ ) || '';
31
+ const marker = current === name ? '* ' : ' ';
32
+ console.log(` ${marker}${name} — ${purposeLine.trim() || '(no purpose)'}`);
33
+ }
34
+ }
@@ -0,0 +1,55 @@
1
+ import { contextDir as getContextDir } from '../core/context-root.js';
2
+ import { commitContext, hasChanges, commitCount, commitCountSince } from '../core/git.js';
3
+ import { readConfig } from '../core/config.js';
4
+ import { readContextFile } from '../core/fs.js';
5
+ import { acquireLock } from '../core/lock.js';
6
+
7
+ export default async function commit({ args, flags }) {
8
+ const root = flags._contextRoot;
9
+ const ctxDir = getContextDir(root);
10
+ const releaseLock = acquireLock(root);
11
+
12
+ try {
13
+ if (!hasChanges(ctxDir)) {
14
+ console.log('â„šī¸ No changes to commit.');
15
+ return;
16
+ }
17
+
18
+ const message = args.length
19
+ ? args.join(' ')
20
+ : `checkpoint ${new Date().toISOString().slice(0, 16)}`;
21
+ const hash = commitContext(ctxDir, message);
22
+
23
+ if (!hash) {
24
+ console.log('â„šī¸ No changes to commit.');
25
+ return;
26
+ }
27
+
28
+ const count = commitCount(ctxDir);
29
+ console.log(`✅ COMMITTED: "${message}"`);
30
+ console.log(`Commit: ${hash} | Total: ${count}`);
31
+
32
+ // Check if reflection is due (auto-commit trigger)
33
+ const config = readConfig(ctxDir);
34
+ if (config.reflection?.trigger === 'auto-commit') {
35
+ const freq = config.reflection?.frequency || 5;
36
+ // Find last reflection's commit hash
37
+ let sinceRef = null;
38
+ const stateRaw = readContextFile(ctxDir, '.reflect-state.json');
39
+ if (stateRaw) {
40
+ try {
41
+ sinceRef = JSON.parse(stateRaw).last_commit_hash;
42
+ } catch {}
43
+ }
44
+ const sinceLast = sinceRef ? commitCountSince(ctxDir, sinceRef) : count;
45
+ if (sinceLast >= freq) {
46
+ console.log('');
47
+ console.log(
48
+ `📌 REFLECTION DUE: ${sinceLast} commits since last reflection. Run: amem reflect`,
49
+ );
50
+ }
51
+ }
52
+ } finally {
53
+ releaseLock();
54
+ }
55
+ }