@mndrk/memx 0.3.3 → 0.3.4

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 (39) hide show
  1. package/README.md +98 -77
  2. package/coverage/clover.xml +1160 -0
  3. package/coverage/coverage-final.json +3 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +131 -0
  8. package/coverage/lcov-report/index.js.html +7255 -0
  9. package/coverage/lcov-report/mcp.js.html +1009 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov.info +2017 -0
  15. package/index.js +651 -243
  16. package/package.json +24 -2
  17. package/test/additional.test.js +373 -0
  18. package/test/branches.test.js +247 -0
  19. package/test/commands.test.js +663 -0
  20. package/test/context.test.js +185 -0
  21. package/test/coverage.test.js +366 -0
  22. package/test/dispatch.test.js +220 -0
  23. package/test/edge-coverage.test.js +250 -0
  24. package/test/edge.test.js +434 -0
  25. package/test/final-coverage.test.js +316 -0
  26. package/test/final-edges.test.js +199 -0
  27. package/test/init-local.test.js +316 -0
  28. package/test/init.test.js +122 -0
  29. package/test/interactive.test.js +229 -0
  30. package/test/main-dispatch.test.js +164 -0
  31. package/test/main-full.test.js +590 -0
  32. package/test/main.test.js +197 -0
  33. package/test/mcp-server.test.js +320 -0
  34. package/test/mcp.test.js +288 -0
  35. package/test/more.test.js +312 -0
  36. package/test/new.test.js +175 -0
  37. package/test/skill.test.js +247 -0
  38. package/test/tasks-interactive.test.js +243 -0
  39. package/test/utils.test.js +367 -0
package/index.js CHANGED
@@ -1,5 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ============================================================
4
+ // mem - Git-backed Key-Value Store for AI Agents
5
+ //
6
+ // Architecture:
7
+ // - All data stored in central ~/.mem git repository
8
+ // - Each task is a git branch (task/<name>)
9
+ // - index.json maps project directories → task branches
10
+ // - agx orchestrator calls mem commands, never accesses ~/.mem directly
11
+ //
12
+ // Storage structure (per task branch):
13
+ // goal.md - task goal, criteria, progress %
14
+ // state.md - status, provider, next step, checkpoints
15
+ // memory.md - learnings and insights
16
+ // playbook.md - global learnings (on main branch)
17
+ //
18
+ // Primitives:
19
+ // mem new "goal" --provider c Create task (branch + files + commit)
20
+ // mem branch <name> Create/switch branch
21
+ // mem commit "msg" Commit changes
22
+ // mem set <key> <value> Set frontmatter value
23
+ // mem get <key> Get frontmatter value
24
+ // mem context --json Get full task context
25
+ // ============================================================
26
+
3
27
  const { execSync, spawn } = require('child_process');
4
28
  const fs = require('fs');
5
29
  const path = require('path');
@@ -26,9 +50,8 @@ Use \`mem\` to maintain state, track progress, and accumulate learnings across s
26
50
  - **Git-backed**: All state is versioned and syncable
27
51
  - **Branches = Tasks**: Each task/goal is a separate branch
28
52
  - **Two scopes**: Task-local memory + global playbook
29
- - **Wake system**: Store schedule intent, export to cron
30
53
 
31
- ## On Wake (Start of Session)
54
+ ## On Session Start
32
55
 
33
56
  \`\`\`bash
34
57
  mem context # Load full state: goal, progress, learnings
@@ -75,11 +98,8 @@ mem tasks # List all tasks (branches)
75
98
  mem switch <name> # Switch to different task
76
99
  \`\`\`
77
100
 
78
- ### Wake & Sync
101
+ ### Sync
79
102
  \`\`\`bash
80
- mem wake "every 15m" # Set wake schedule
81
- mem wake "8am daily" # Other patterns: monday 9am, */30 * * * *
82
- mem cron export # Export as crontab entry
83
103
  mem sync # Push/pull with remote
84
104
  \`\`\`
85
105
 
@@ -88,14 +108,14 @@ mem sync # Push/pull with remote
88
108
  \`\`\`
89
109
  .mem/
90
110
  goal.md # Objective + criteria + constraints
91
- state.md # Progress, next step, blockers, wake
111
+ state.md # Progress, next step, blockers
92
112
  memory.md # Task-specific learnings
93
113
  playbook.md # Global learnings (shared)
94
114
  \`\`\`
95
115
 
96
116
  ## Typical Session Loop
97
117
 
98
- 1. \`mem context\` - Load state on wake
118
+ 1. \`mem context\` - Load current state
99
119
  2. \`mem next\` - See what to work on
100
120
  3. Do work
101
121
  4. \`mem checkpoint "..."\` - Save progress
@@ -238,6 +258,23 @@ function git(memDir, ...args) {
238
258
  }
239
259
  }
240
260
 
261
+ // Parallel-safe: read file from branch without checkout
262
+ function gitShow(memDir, branch, filename) {
263
+ try {
264
+ const result = require('child_process').spawnSync('git', ['show', `${branch}:${filename}`], {
265
+ cwd: memDir,
266
+ encoding: 'utf8',
267
+ stdio: ['pipe', 'pipe', 'pipe']
268
+ });
269
+ if (result.status !== 0) {
270
+ return null;
271
+ }
272
+ return result.stdout || '';
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
241
278
  // Get current branch
242
279
  function getCurrentBranch(memDir) {
243
280
  return git(memDir, 'rev-parse', '--abbrev-ref', 'HEAD');
@@ -759,26 +796,221 @@ function cmdContext(memDir) {
759
796
  }
760
797
 
761
798
  // List tasks
762
- function cmdTasks(memDir) {
799
+ // Uses git show for parallel-safe reading (no checkout required)
800
+ function cmdTasks(args, memDir) {
801
+ const jsonMode = args.includes('--json');
802
+
763
803
  if (!memDir) {
764
- console.log(`${c.yellow}No .mem repo found.${c.reset}`);
804
+ if (jsonMode) {
805
+ console.log(JSON.stringify({ tasks: [], error: 'no_repo' }));
806
+ } else {
807
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
808
+ }
765
809
  return;
766
810
  }
767
811
 
768
- const current = getCurrentBranch(memDir);
769
- const branches = git(memDir, 'branch', '--list').split('\n');
770
-
771
- console.log(`\n${c.bold}Tasks${c.reset}\n`);
772
-
773
- branches.forEach(b => {
774
- const name = b.replace('*', '').trim();
775
- const isCurrent = b.includes('*');
776
- const marker = isCurrent ? `${c.green}→${c.reset}` : ' ';
777
- const display = name.replace('task/', '');
778
- console.log(`${marker} ${isCurrent ? c.cyan : c.dim}${display}${c.reset}`);
812
+ const currentBranch = getCurrentBranch(memDir);
813
+ const branches = git(memDir, 'branch', '--list').split('\n')
814
+ .map(b => b.replace('*', '').trim())
815
+ .filter(b => b && b !== 'main' && b !== 'master');
816
+
817
+ if (branches.length === 0) {
818
+ if (jsonMode) {
819
+ console.log(JSON.stringify({ tasks: [] }));
820
+ } else {
821
+ console.log(`${c.yellow}No tasks found${c.reset}`);
822
+ }
823
+ return;
824
+ }
825
+
826
+ // Build reverse index: branch -> projectDir
827
+ const index = loadIndex();
828
+ const branchToDir = {};
829
+ for (const [dir, branch] of Object.entries(index)) {
830
+ branchToDir[branch] = dir;
831
+ }
832
+
833
+ // Load task info using git show (parallel-safe, no checkout)
834
+ const tasks = branches.map(branch => {
835
+ const taskName = branch.replace('task/', '');
836
+ let status = 'active';
837
+ let progress = '—';
838
+ let goal = '';
839
+ let provider = 'claude';
840
+
841
+ try {
842
+ // Use git show to read files without checkout (parallel-safe)
843
+ const stateContent = gitShow(memDir, branch, 'state.md');
844
+ if (stateContent) {
845
+ const { frontmatter } = parseFrontmatter(stateContent);
846
+ status = frontmatter.status || 'active';
847
+ provider = frontmatter.provider || 'claude';
848
+ }
849
+ } catch {}
850
+
851
+ try {
852
+ const goalContent = gitShow(memDir, branch, 'goal.md');
853
+ if (goalContent) {
854
+ const progressMatch = goalContent.match(/Progress:\s*(\d+)%/i);
855
+ if (progressMatch) progress = `${progressMatch[1]}%`;
856
+
857
+ const goalMatch = goalContent.match(/^#\s*Goal\s*\n+([^\n#]+)/m);
858
+ if (goalMatch) goal = goalMatch[1].trim().slice(0, 50);
859
+ }
860
+ } catch {}
861
+
862
+ return {
863
+ branch,
864
+ taskName,
865
+ status,
866
+ progress,
867
+ goal,
868
+ provider,
869
+ projectDir: branchToDir[branch] || null,
870
+ isCurrent: branch === currentBranch
871
+ };
872
+ });
873
+
874
+ // JSON mode - output structured data for agx
875
+ if (jsonMode) {
876
+ console.log(JSON.stringify({ tasks }));
877
+ return;
878
+ }
879
+
880
+ // Non-interactive if not TTY
881
+ if (!process.stdin.isTTY) {
882
+ console.log(`${c.bold}Tasks${c.reset}\n`);
883
+ tasks.forEach(t => {
884
+ const marker = t.isCurrent ? `${c.green}→${c.reset}` : ' ';
885
+ const statusIcon = t.status === 'done' ? `${c.dim}✓${c.reset}`
886
+ : t.status === 'blocked' ? `${c.yellow}○${c.reset}`
887
+ : `${c.green}●${c.reset}`;
888
+ console.log(`${marker} ${statusIcon} ${t.taskName} ${c.dim}${t.progress}${c.reset}`);
889
+ });
890
+ return;
891
+ }
892
+
893
+ // Interactive mode
894
+ let selectedIdx = tasks.findIndex(t => t.isCurrent);
895
+ if (selectedIdx < 0) selectedIdx = 0;
896
+ let inDetailView = false;
897
+
898
+ const clearScreen = () => process.stdout.write('\x1b[2J\x1b[H');
899
+ const hideCursor = () => process.stdout.write('\x1b[?25l');
900
+ const showCursor = () => process.stdout.write('\x1b[?25h');
901
+
902
+ const renderList = () => {
903
+ clearScreen();
904
+ console.log(`${c.bold}Tasks${c.reset}\n`);
905
+
906
+ tasks.forEach((task, idx) => {
907
+ const selected = idx === selectedIdx;
908
+ const prefix = selected ? `${c.cyan}❯${c.reset}` : ' ';
909
+ const statusIcon = task.status === 'done' ? `${c.dim}✓${c.reset}`
910
+ : task.status === 'blocked' ? `${c.yellow}○${c.reset}`
911
+ : `${c.green}●${c.reset}`;
912
+ const current = task.isCurrent ? ` ${c.green}(current)${c.reset}` : '';
913
+ const name = selected ? `${c.bold}${task.taskName}${c.reset}` : task.taskName;
914
+ const progressText = task.progress !== '—' ? ` ${c.green}${task.progress}${c.reset}` : '';
915
+
916
+ console.log(`${prefix} ${statusIcon} ${name}${progressText}${current}`);
917
+ });
918
+
919
+ console.log(`\n${c.dim}↑/↓ select · enter view · s switch · d done · x delete · q quit${c.reset}`);
920
+ };
921
+
922
+ const renderDetail = () => {
923
+ clearScreen();
924
+ const task = tasks[selectedIdx];
925
+ const statusColor = task.status === 'done' ? c.dim
926
+ : task.status === 'blocked' ? c.yellow
927
+ : c.green;
928
+
929
+ console.log(`${c.bold}${c.cyan}${task.taskName}${c.reset}${task.isCurrent ? ` ${c.green}(current)${c.reset}` : ''}\n`);
930
+ console.log(` ${c.dim}Status:${c.reset} ${statusColor}${task.status}${c.reset}`);
931
+ console.log(` ${c.dim}Progress:${c.reset} ${task.progress !== '—' ? c.green + task.progress + c.reset : c.dim + '—' + c.reset}`);
932
+ if (task.goal) console.log(` ${c.dim}Goal:${c.reset} ${task.goal}`);
933
+
934
+ console.log(`\n${c.dim}esc back · s switch · d done · x delete · q quit${c.reset}`);
935
+ };
936
+
937
+ const render = () => inDetailView ? renderDetail() : renderList();
938
+
939
+ const doAction = (action) => {
940
+ const task = tasks[selectedIdx];
941
+ showCursor();
942
+ clearScreen();
943
+
944
+ if (action === 'switch') {
945
+ try {
946
+ git(memDir, 'checkout', task.branch);
947
+ console.log(`${c.green}✓${c.reset} Switched to: ${c.bold}${task.taskName}${c.reset}`);
948
+ } catch (err) {
949
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
950
+ }
951
+ process.exit(0);
952
+ } else if (action === 'done') {
953
+ try {
954
+ git(memDir, 'checkout', task.branch);
955
+ const stateFile = path.join(memDir, 'state.md');
956
+ if (fs.existsSync(stateFile)) {
957
+ let state = fs.readFileSync(stateFile, 'utf8');
958
+ state = state.replace(/^status:\s*.+$/m, 'status: done');
959
+ fs.writeFileSync(stateFile, state);
960
+ git(memDir, 'add', 'state.md');
961
+ git(memDir, 'commit', '-m', 'done: marked complete');
962
+ }
963
+ console.log(`${c.green}✓${c.reset} Marked ${c.bold}${task.taskName}${c.reset} done`);
964
+ } catch (err) {
965
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
966
+ }
967
+ process.exit(0);
968
+ } else if (action === 'delete') {
969
+ try {
970
+ if (task.isCurrent) {
971
+ git(memDir, 'checkout', 'main');
972
+ }
973
+ git(memDir, 'branch', '-D', task.branch);
974
+ console.log(`${c.red}✗${c.reset} Deleted ${c.bold}${task.taskName}${c.reset}`);
975
+ } catch (err) {
976
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
977
+ }
978
+ process.exit(0);
979
+ }
980
+ };
981
+
982
+ process.stdin.setRawMode(true);
983
+ process.stdin.resume();
984
+ hideCursor();
985
+ render();
986
+
987
+ process.stdin.on('data', (key) => {
988
+ const k = key.toString();
989
+
990
+ if (k === 'q' || k === '\x03') {
991
+ showCursor();
992
+ clearScreen();
993
+ process.exit(0);
994
+ } else if (k === '\x1b[A') { // up
995
+ selectedIdx = Math.max(0, selectedIdx - 1);
996
+ render();
997
+ } else if (k === '\x1b[B') { // down
998
+ selectedIdx = Math.min(tasks.length - 1, selectedIdx + 1);
999
+ render();
1000
+ } else if (k === '\r' || k === '\n') { // enter
1001
+ inDetailView = true;
1002
+ render();
1003
+ } else if (k === '\x1b' || k === '\x1b[D') { // esc or left
1004
+ inDetailView = false;
1005
+ render();
1006
+ } else if (k === 's') {
1007
+ doAction('switch');
1008
+ } else if (k === 'd') {
1009
+ doAction('done');
1010
+ } else if (k === 'x') {
1011
+ doAction('delete');
1012
+ }
779
1013
  });
780
-
781
- console.log('');
782
1014
  }
783
1015
 
784
1016
  // Switch task
@@ -1299,6 +1531,202 @@ function cmdCriteria(args, memDir) {
1299
1531
 
1300
1532
  // ==================== PRIMITIVES ====================
1301
1533
 
1534
+ function cmdBranch(args, memDir) {
1535
+ const name = args[0];
1536
+
1537
+ // Initialize central mem if needed
1538
+ if (!fs.existsSync(CENTRAL_MEM)) {
1539
+ fs.mkdirSync(CENTRAL_MEM, { recursive: true });
1540
+ execSync('git init', { cwd: CENTRAL_MEM, stdio: 'ignore' });
1541
+ writeMemFile(CENTRAL_MEM, 'playbook.md', '# Playbook\n\n');
1542
+ git(CENTRAL_MEM, 'add', '-A');
1543
+ git(CENTRAL_MEM, 'commit', '-m', 'init: central memory');
1544
+ console.log(`${c.green}✓${c.reset} Initialized central memory: ${c.dim}${CENTRAL_MEM}${c.reset}`);
1545
+ }
1546
+
1547
+ const targetDir = memDir || CENTRAL_MEM;
1548
+
1549
+ // No args: list branches
1550
+ if (!name) {
1551
+ const branches = git(targetDir, 'branch', '--list');
1552
+ console.log(branches || `${c.dim}No branches${c.reset}`);
1553
+ return;
1554
+ }
1555
+
1556
+ // Check if branch exists
1557
+ const branches = git(targetDir, 'branch', '--list').split('\n').map(b => b.replace('*', '').trim());
1558
+ const branchExists = branches.includes(name) || branches.includes(`task/${name}`);
1559
+
1560
+ try {
1561
+ if (branchExists) {
1562
+ // Switch to existing branch
1563
+ const branchName = branches.includes(name) ? name : `task/${name}`;
1564
+ git(targetDir, 'checkout', branchName);
1565
+ console.log(`${c.green}✓${c.reset} Switched to: ${c.cyan}${branchName}${c.reset}`);
1566
+ } else {
1567
+ // Create new branch
1568
+ const branchName = name.startsWith('task/') ? name : `task/${name}`;
1569
+ git(targetDir, 'checkout', '-b', branchName);
1570
+ console.log(`${c.green}✓${c.reset} Created branch: ${c.cyan}${branchName}${c.reset}`);
1571
+ }
1572
+ } catch (err) {
1573
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
1574
+ }
1575
+ }
1576
+
1577
+ function cmdCommit(args, memDir) {
1578
+ if (!memDir) {
1579
+ // Use central mem
1580
+ memDir = CENTRAL_MEM;
1581
+ if (!fs.existsSync(memDir)) {
1582
+ console.log(`${c.yellow}No .mem repo found.${c.reset} Run ${c.cyan}mem branch <name>${c.reset} first.`);
1583
+ return;
1584
+ }
1585
+ }
1586
+
1587
+ const msg = args.join(' ') || 'checkpoint';
1588
+
1589
+ try {
1590
+ // Check for changes
1591
+ const status = git(memDir, 'status', '--porcelain');
1592
+ if (!status.trim()) {
1593
+ console.log(`${c.dim}No changes to commit${c.reset}`);
1594
+ return;
1595
+ }
1596
+
1597
+ // Add and commit
1598
+ git(memDir, 'add', '-A');
1599
+ git(memDir, 'commit', '-m', msg);
1600
+ console.log(`${c.green}✓${c.reset} Committed: ${c.dim}${msg}${c.reset}`);
1601
+ } catch (err) {
1602
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
1603
+ }
1604
+ }
1605
+
1606
+ // ============================================================
1607
+ // cmdNew: Create a new task with all necessary files
1608
+ // This is the ONLY way to create tasks - agx calls this
1609
+ // ============================================================
1610
+ function cmdNew(args, memDir) {
1611
+ // Parse flags
1612
+ const jsonMode = args.includes('--json');
1613
+
1614
+ // Parse --provider / -P flag
1615
+ let provider = 'claude';
1616
+ const providerIdx = args.findIndex(a => a === '--provider' || a === '-P');
1617
+ if (providerIdx !== -1 && args[providerIdx + 1]) {
1618
+ provider = args[providerIdx + 1].toLowerCase();
1619
+ }
1620
+
1621
+ // Parse --dir flag for project directory mapping
1622
+ let projectDir = process.cwd();
1623
+ const dirIdx = args.findIndex(a => a === '--dir');
1624
+ if (dirIdx !== -1 && args[dirIdx + 1]) {
1625
+ projectDir = args[dirIdx + 1];
1626
+ }
1627
+
1628
+ // Extract goal text - filter out all flags and their values
1629
+ const flagsWithValues = ['--provider', '-P', '--dir'];
1630
+ const flagsWithoutValues = ['--json'];
1631
+ const goalParts = [];
1632
+ for (let i = 0; i < args.length; i++) {
1633
+ if (flagsWithValues.includes(args[i])) {
1634
+ i++; // skip the flag's value too
1635
+ continue;
1636
+ }
1637
+ if (flagsWithoutValues.includes(args[i])) {
1638
+ continue;
1639
+ }
1640
+ goalParts.push(args[i]);
1641
+ }
1642
+ const goalText = goalParts.join(' ');
1643
+ if (!goalText) {
1644
+ if (jsonMode) {
1645
+ console.log(JSON.stringify({ error: 'missing_goal', usage: 'mem new "<goal>" [--provider c] [--dir /path]' }));
1646
+ } else {
1647
+ console.log(`${c.red}Usage:${c.reset} mem new "<goal>" [--provider c|g|o] [--dir /path]`);
1648
+ }
1649
+ process.exit(1);
1650
+ }
1651
+
1652
+ // Generate task name from goal
1653
+ const taskName = goalText
1654
+ .toLowerCase()
1655
+ .replace(/[^a-z0-9\s]/g, '')
1656
+ .split(/\s+/)
1657
+ .slice(0, 3)
1658
+ .join('-');
1659
+
1660
+ const branch = `task/${taskName}`;
1661
+ const today = new Date().toISOString().split('T')[0];
1662
+
1663
+ try {
1664
+ // Create branch (also initializes central mem if needed)
1665
+ cmdBranch([taskName], memDir);
1666
+
1667
+ // Write goal.md
1668
+ const goalMd = `---
1669
+ task: ${taskName}
1670
+ created: ${today}
1671
+ ---
1672
+
1673
+ # Goal
1674
+
1675
+ ${goalText}
1676
+
1677
+ ## Criteria
1678
+
1679
+ - [ ] Define success criteria
1680
+
1681
+ ## Progress: 0%`;
1682
+ writeMemFile(CENTRAL_MEM, 'goal.md', goalMd);
1683
+
1684
+ // Write state.md with provider
1685
+ const stateMd = `---
1686
+ status: active
1687
+ provider: ${provider}
1688
+ ---
1689
+
1690
+ # State
1691
+
1692
+ ## Next Step
1693
+
1694
+ Begin work
1695
+
1696
+ ## Checkpoints
1697
+
1698
+ - [ ] Started`;
1699
+ writeMemFile(CENTRAL_MEM, 'state.md', stateMd);
1700
+
1701
+ // Write memory.md
1702
+ writeMemFile(CENTRAL_MEM, 'memory.md', '# Learnings\n\n');
1703
+
1704
+ // Commit
1705
+ git(CENTRAL_MEM, 'add', '-A');
1706
+ git(CENTRAL_MEM, 'commit', '-m', `new: ${taskName}`);
1707
+
1708
+ // Update index mapping (projectDir -> branch)
1709
+ const index = loadIndex();
1710
+ index[projectDir] = branch;
1711
+ saveIndex(index);
1712
+
1713
+ if (jsonMode) {
1714
+ console.log(JSON.stringify({ taskName, branch, projectDir, created: today, goal: goalText, provider }));
1715
+ } else {
1716
+ console.log(`${c.green}✓${c.reset} Created task: ${c.bold}${taskName}${c.reset}`);
1717
+ console.log(`${c.green}✓${c.reset} Provider: ${c.cyan}${provider}${c.reset}`);
1718
+ console.log(`${c.green}✓${c.reset} Mapped: ${c.dim}${projectDir} → ${branch}${c.reset}`);
1719
+ }
1720
+ } catch (err) {
1721
+ if (jsonMode) {
1722
+ console.log(JSON.stringify({ error: err.message }));
1723
+ } else {
1724
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
1725
+ }
1726
+ process.exit(1);
1727
+ }
1728
+ }
1729
+
1302
1730
  function cmdSet(args, memDir) {
1303
1731
  if (!memDir) {
1304
1732
  console.log(`${c.yellow}No .mem repo found.${c.reset}`);
@@ -1402,6 +1830,73 @@ function cmdLog(memDir) {
1402
1830
  console.log(log);
1403
1831
  }
1404
1832
 
1833
+ // Live watch memory activity
1834
+ function cmdWatch(memDir) {
1835
+ if (!memDir) {
1836
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1837
+ return;
1838
+ }
1839
+
1840
+ const clearScreen = () => process.stdout.write('\x1b[2J\x1b[H');
1841
+
1842
+ const render = () => {
1843
+ clearScreen();
1844
+ console.log('═══════════════════════════════════════════════════════');
1845
+ console.log(' 📝 Memory Activity');
1846
+ console.log('═══════════════════════════════════════════════════════');
1847
+ console.log('');
1848
+
1849
+ // Show main branch
1850
+ console.log(` ${c.yellow}📌 main${c.reset}`);
1851
+ try {
1852
+ const mainLog = git(memDir, 'log', 'main', '--format=%C(dim)%ar%C(reset) %C(cyan)%s%C(reset)', '-6');
1853
+ mainLog.split('\n').filter(l => l).forEach(line => console.log(` ${line}`));
1854
+ } catch (e) {
1855
+ console.log(` ${c.dim}(no commits)${c.reset}`);
1856
+ }
1857
+
1858
+ // Show task branches
1859
+ try {
1860
+ const branches = git(memDir, 'branch', '--format=%(refname:short)')
1861
+ .split('\n')
1862
+ .filter(b => b.startsWith('task/'))
1863
+ .slice(0, 3);
1864
+
1865
+ for (const branch of branches) {
1866
+ const taskName = branch.replace('task/', '');
1867
+ console.log('');
1868
+ console.log(` ${c.yellow}📌 ${taskName}${c.reset}`);
1869
+ try {
1870
+ const branchLog = git(memDir, 'log', branch, '--format=%C(dim)%ar%C(reset) %C(cyan)%s%C(reset)', '-3');
1871
+ branchLog.split('\n').filter(l => l).forEach(line => console.log(` ${line}`));
1872
+ } catch (e) {
1873
+ console.log(` ${c.dim}(no commits)${c.reset}`);
1874
+ }
1875
+ }
1876
+ } catch (e) {
1877
+ // No task branches
1878
+ }
1879
+
1880
+ console.log('');
1881
+ console.log('═══════════════════════════════════════════════════════');
1882
+ const now = new Date().toLocaleTimeString('en-US', { hour12: false });
1883
+ console.log(` ${c.dim}${now} • ctrl+c to exit${c.reset}`);
1884
+ };
1885
+
1886
+ // Initial render
1887
+ render();
1888
+
1889
+ // Update every 2 seconds
1890
+ const interval = setInterval(render, 2000);
1891
+
1892
+ // Handle exit
1893
+ process.on('SIGINT', () => {
1894
+ clearInterval(interval);
1895
+ clearScreen();
1896
+ process.exit(0);
1897
+ });
1898
+ }
1899
+
1405
1900
  // ==================== SKILL ====================
1406
1901
 
1407
1902
  function isSkillInstalled(provider) {
@@ -1501,171 +1996,6 @@ function handleSkillCommand(args) {
1501
1996
  console.log('');
1502
1997
  }
1503
1998
 
1504
- // ==================== WAKE ====================
1505
-
1506
- // Parse wake pattern to cron expression
1507
- function parseWakeToCron(pattern) {
1508
- pattern = pattern.toLowerCase().trim();
1509
-
1510
- // Handle intervals: every Xm, every Xh
1511
- const intervalMatch = pattern.match(/^every\s+(\d+)\s*(m|min|minutes?|h|hr|hours?)$/);
1512
- if (intervalMatch) {
1513
- const num = parseInt(intervalMatch[1]);
1514
- const unit = intervalMatch[2][0];
1515
- if (unit === 'm') {
1516
- return `*/${num} * * * *`;
1517
- } else if (unit === 'h') {
1518
- return `0 */${num} * * *`;
1519
- }
1520
- }
1521
-
1522
- // Handle daily: Xam daily, Xpm daily, every day at X
1523
- const dailyMatch = pattern.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:daily|every\s*day)?$/);
1524
- if (dailyMatch) {
1525
- let hour = parseInt(dailyMatch[1]);
1526
- const min = dailyMatch[2] ? parseInt(dailyMatch[2]) : 0;
1527
- const ampm = dailyMatch[3];
1528
- if (ampm === 'pm' && hour < 12) hour += 12;
1529
- if (ampm === 'am' && hour === 12) hour = 0;
1530
- return `${min} ${hour} * * *`;
1531
- }
1532
-
1533
- // Handle weekly: monday 9am, every tuesday at 3pm
1534
- const weeklyMatch = pattern.match(/^(?:every\s+)?(mon|tue|wed|thu|fri|sat|sun)[a-z]*\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/);
1535
- if (weeklyMatch) {
1536
- const days = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
1537
- const day = days[weeklyMatch[1]];
1538
- let hour = parseInt(weeklyMatch[2]);
1539
- const min = weeklyMatch[3] ? parseInt(weeklyMatch[3]) : 0;
1540
- const ampm = weeklyMatch[4];
1541
- if (ampm === 'pm' && hour < 12) hour += 12;
1542
- if (ampm === 'am' && hour === 12) hour = 0;
1543
- return `${min} ${hour} * * ${day}`;
1544
- }
1545
-
1546
- // Already cron format? Pass through
1547
- if (pattern.match(/^[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+$/)) {
1548
- return pattern;
1549
- }
1550
-
1551
- return null;
1552
- }
1553
-
1554
- // Set/get/clear wake
1555
- function cmdWake(args, memDir) {
1556
- if (!memDir) {
1557
- console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1558
- return;
1559
- }
1560
-
1561
- const state = readMemFile(memDir, 'state.md') || '';
1562
- const { frontmatter, body } = parseFrontmatter(state);
1563
-
1564
- // No args: show current wake
1565
- if (args.length === 0) {
1566
- if (frontmatter.wake) {
1567
- console.log(`${c.bold}Wake:${c.reset} ${frontmatter.wake}`);
1568
- if (frontmatter.wake_command) {
1569
- console.log(`${c.bold}Command:${c.reset} ${frontmatter.wake_command}`);
1570
- }
1571
- const cron = parseWakeToCron(frontmatter.wake);
1572
- if (cron) {
1573
- console.log(`${c.dim}Cron: ${cron}${c.reset}`);
1574
- }
1575
- } else {
1576
- console.log(`${c.dim}No wake set${c.reset}`);
1577
- console.log(`${c.dim}Usage: mem wake "every 15m" [--run "command"]${c.reset}`);
1578
- }
1579
- return;
1580
- }
1581
-
1582
- // Clear wake
1583
- if (args[0] === 'clear') {
1584
- delete frontmatter.wake;
1585
- delete frontmatter.wake_command;
1586
- writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
1587
- git(memDir, 'add', 'state.md');
1588
- git(memDir, 'commit', '-m', 'wake: clear');
1589
- console.log(`${c.green}✓${c.reset} Wake cleared`);
1590
- return;
1591
- }
1592
-
1593
- // Set wake
1594
- let pattern = '';
1595
- let command = '';
1596
-
1597
- // Parse args
1598
- for (let i = 0; i < args.length; i++) {
1599
- if (args[i] === '--run' || args[i] === '-r') {
1600
- command = args.slice(i + 1).join(' ');
1601
- break;
1602
- }
1603
- pattern += (pattern ? ' ' : '') + args[i];
1604
- }
1605
-
1606
- // Validate pattern
1607
- const cron = parseWakeToCron(pattern);
1608
- if (!cron) {
1609
- console.log(`${c.red}Could not parse wake pattern:${c.reset} ${pattern}`);
1610
- console.log(`\n${c.dim}Examples:${c.reset}`);
1611
- console.log(` mem wake "every 15m"`);
1612
- console.log(` mem wake "every 2h"`);
1613
- console.log(` mem wake "8am daily"`);
1614
- console.log(` mem wake "monday 9am"`);
1615
- console.log(` mem wake "*/30 * * * *" ${c.dim}(raw cron)${c.reset}`);
1616
- return;
1617
- }
1618
-
1619
- frontmatter.wake = pattern;
1620
- if (command) {
1621
- frontmatter.wake_command = command;
1622
- }
1623
-
1624
- writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
1625
- git(memDir, 'add', 'state.md');
1626
- git(memDir, 'commit', '-m', `wake: ${pattern}`);
1627
-
1628
- console.log(`${c.green}✓${c.reset} Wake set: ${c.bold}${pattern}${c.reset}`);
1629
- console.log(`${c.dim}Cron: ${cron}${c.reset}`);
1630
- if (command) {
1631
- console.log(`${c.dim}Command: ${command}${c.reset}`);
1632
- }
1633
- console.log(`\n${c.dim}Export with: ${c.reset}mem cron export`);
1634
- }
1635
-
1636
- // Export to cron format
1637
- function cmdCronExport(memDir) {
1638
- if (!memDir) {
1639
- console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1640
- return;
1641
- }
1642
-
1643
- const state = readMemFile(memDir, 'state.md') || '';
1644
- const { frontmatter } = parseFrontmatter(state);
1645
-
1646
- if (!frontmatter.wake) {
1647
- console.log(`${c.yellow}No wake set.${c.reset} Use ${c.cyan}mem wake "pattern"${c.reset} first.`);
1648
- return;
1649
- }
1650
-
1651
- const cron = parseWakeToCron(frontmatter.wake);
1652
- if (!cron) {
1653
- console.log(`${c.red}Could not parse wake pattern:${c.reset} ${frontmatter.wake}`);
1654
- return;
1655
- }
1656
-
1657
- // Detect PATH from current environment for cron (which has minimal PATH)
1658
- const nodeBin = path.dirname(process.execPath);
1659
- const pathDirs = [nodeBin, '/usr/local/bin', '/usr/bin', '/bin'];
1660
- const pathPrefix = `PATH=${pathDirs.join(':')}`;
1661
-
1662
- const command = frontmatter.wake_command || `cd ${memDir} && mem context`;
1663
- const entry = `${cron} ${pathPrefix} && ${command}`;
1664
-
1665
- // Just output the cron line (can be piped/appended)
1666
- console.log(entry);
1667
- }
1668
-
1669
1999
  // ==================== MCP ====================
1670
2000
 
1671
2001
  function startMCPServer(args) {
@@ -1756,7 +2086,7 @@ ${c.bold}PROGRESS${c.reset}
1756
2086
  criteria <n> Mark criterion #n complete
1757
2087
 
1758
2088
  ${c.bold}QUERY${c.reset}
1759
- context Full hydration for agent wake
2089
+ context Full context for agent
1760
2090
  history Task progression
1761
2091
  query "<search>" Search all memory
1762
2092
 
@@ -1772,12 +2102,8 @@ ${c.bold}PRIMITIVES${c.reset}
1772
2102
  get <key> Get a value
1773
2103
  append <list> <item> Append to list
1774
2104
  log Raw git log
2105
+ watch Live tail of memory activity
1775
2106
 
1776
- ${c.bold}WAKE${c.reset}
1777
- wake "<pattern>" Set wake schedule (every 15m, 8am daily, etc.)
1778
- wake --run "<cmd>" Set wake with custom command
1779
- wake clear Clear wake schedule
1780
- cron export Export wake as crontab entry
1781
2107
 
1782
2108
  ${c.bold}INTEGRATION${c.reset}
1783
2109
  skill View LLM skill
@@ -1799,42 +2125,45 @@ ${c.dim}Docs: https://github.com/ramarlina/memx${c.reset}
1799
2125
  // ==================== MAIN ====================
1800
2126
 
1801
2127
  async function main() {
2128
+ // ============================================================
2129
+ // mem: Git-backed key-value store for AI agents
2130
+ //
2131
+ // Architecture:
2132
+ // - All data stored in central ~/.mem git repo
2133
+ // - Each task is a branch (task/<name>)
2134
+ // - index.json maps project directories to task branches
2135
+ // - agx orchestrator uses mem primitives, never accesses ~/.mem directly
2136
+ // ============================================================
2137
+
1802
2138
  const args = process.argv.slice(2);
1803
2139
  const cmd = args[0];
1804
2140
  const cmdArgs = args.slice(1);
1805
-
1806
- // Find .mem repo (returns object with memDir, taskBranch, etc.)
1807
- const memInfo = findMemDir();
1808
- let memDir = null;
1809
-
1810
- if (memInfo) {
1811
- memDir = memInfo.memDir;
1812
- // If central mem with task mapping, ensure we're on the right branch
1813
- if (!memInfo.isLocal && memInfo.taskBranch) {
1814
- ensureTaskBranch(memDir, memInfo.taskBranch);
1815
- }
1816
- }
1817
-
1818
- // No command and no .mem? Start interactive onboarding
1819
- if (!cmd && !memDir) {
1820
- await interactiveInit();
1821
- return;
1822
- }
1823
-
1824
- // No command but has central mem with no mapping for this dir?
1825
- if (!cmd && memInfo && memInfo.unmapped) {
1826
- console.log(`${c.dim}No task mapped for this directory.${c.reset}`);
1827
- const create = await prompt(`Create a new task? [Y/n]: `);
1828
- if (create.toLowerCase() !== 'n') {
2141
+
2142
+ // Always use central ~/.mem repository
2143
+ // This is the single source of truth for all task data
2144
+ const memDir = CENTRAL_MEM;
2145
+
2146
+ // Initialize central mem if it doesn't exist
2147
+ if (!fs.existsSync(memDir)) {
2148
+ // No command? Start interactive onboarding
2149
+ if (!cmd) {
1829
2150
  await interactiveInit();
2151
+ return;
1830
2152
  }
1831
- return;
2153
+ // For commands like 'branch', let them handle initialization
1832
2154
  }
1833
-
1834
- // No command but has .mem? Show status
1835
- if (!cmd && memDir) {
1836
- cmdStatus(memDir);
1837
- console.log(`${c.dim}Run ${c.reset}mem help${c.dim} for all commands${c.reset}\n`);
2155
+
2156
+ // No command? Show status for current directory's mapped task
2157
+ if (!cmd) {
2158
+ const index = loadIndex();
2159
+ const taskBranch = index[process.cwd()];
2160
+ if (taskBranch) {
2161
+ ensureTaskBranch(memDir, taskBranch);
2162
+ cmdStatus(memDir);
2163
+ } else {
2164
+ console.log(`${c.dim}No task mapped for this directory.${c.reset}`);
2165
+ console.log(`${c.dim}Run ${c.reset}mem branch <name>${c.dim} to create one.${c.reset}`);
2166
+ }
1838
2167
  return;
1839
2168
  }
1840
2169
 
@@ -1848,6 +2177,11 @@ async function main() {
1848
2177
  case 'init':
1849
2178
  await cmdInit(cmdArgs, memDir);
1850
2179
  break;
2180
+ case 'new':
2181
+ // Create a new task - this is the primary entry point
2182
+ // agx calls: mem new "goal" --provider claude --dir /project/path
2183
+ cmdNew(cmdArgs, memDir);
2184
+ break;
1851
2185
  case 'status':
1852
2186
  cmdStatus(memDir);
1853
2187
  break;
@@ -1918,7 +2252,7 @@ async function main() {
1918
2252
  // Tasks
1919
2253
  case 'tasks':
1920
2254
  case 'ls':
1921
- cmdTasks(memDir);
2255
+ cmdTasks(cmdArgs, memDir);
1922
2256
  break;
1923
2257
  case 'switch':
1924
2258
  case 'sw':
@@ -1931,6 +2265,14 @@ async function main() {
1931
2265
  break;
1932
2266
 
1933
2267
  // Primitives
2268
+ case 'branch':
2269
+ case 'br':
2270
+ cmdBranch(cmdArgs, memDir);
2271
+ break;
2272
+ case 'commit':
2273
+ case 'ci':
2274
+ cmdCommit(cmdArgs, memDir);
2275
+ break;
1934
2276
  case 'set':
1935
2277
  cmdSet(cmdArgs, memDir);
1936
2278
  break;
@@ -1943,24 +2285,16 @@ async function main() {
1943
2285
  case 'log':
1944
2286
  cmdLog(memDir);
1945
2287
  break;
1946
-
2288
+ case 'watch':
2289
+ case 'tail':
2290
+ cmdWatch(memDir);
2291
+ break;
2292
+
1947
2293
  // Skill
1948
2294
  case 'skill':
1949
2295
  handleSkillCommand(args);
1950
2296
  break;
1951
-
1952
- // Wake
1953
- case 'wake':
1954
- cmdWake(cmdArgs, memDir);
1955
- break;
1956
- case 'cron':
1957
- if (cmdArgs[0] === 'export') {
1958
- cmdCronExport(memDir);
1959
- } else {
1960
- console.log(`${c.dim}Usage: mem cron export${c.reset}`);
1961
- }
1962
- break;
1963
-
2297
+
1964
2298
  // MCP
1965
2299
  case 'mcp':
1966
2300
  if (cmdArgs[0] === 'config') {
@@ -1976,7 +2310,81 @@ async function main() {
1976
2310
  }
1977
2311
  }
1978
2312
 
1979
- main().catch(err => {
1980
- console.error(`${c.red}Error:${c.reset}`, err.message);
1981
- process.exit(1);
1982
- });
2313
+ // Only run main if executed directly (not when required for tests)
2314
+ if (require.main === module) {
2315
+ main().catch(err => {
2316
+ console.error(`${c.red}Error:${c.reset}`, err.message);
2317
+ process.exit(1);
2318
+ });
2319
+ }
2320
+
2321
+ // Export for testing
2322
+ module.exports = {
2323
+ // Constants
2324
+ CONFIG_DIR,
2325
+ CONFIG_FILE,
2326
+ CENTRAL_MEM,
2327
+ INDEX_FILE,
2328
+ MEM_SKILL,
2329
+ c,
2330
+
2331
+ // Utility functions
2332
+ loadConfig,
2333
+ saveConfig,
2334
+ prompt,
2335
+ loadIndex,
2336
+ saveIndex,
2337
+ findMemDir,
2338
+ ensureTaskBranch,
2339
+ git,
2340
+ getCurrentBranch,
2341
+ readMemFile,
2342
+ writeMemFile,
2343
+ parseFrontmatter,
2344
+ serializeFrontmatter,
2345
+
2346
+ // Command functions
2347
+ interactiveInit,
2348
+ setupRemote,
2349
+ cmdInit,
2350
+ cmdStatus,
2351
+ cmdGoal,
2352
+ cmdNext,
2353
+ cmdCheckpoint,
2354
+ cmdLearn,
2355
+ cmdContext,
2356
+ cmdTasks,
2357
+ cmdSwitch,
2358
+ cmdSync,
2359
+ cmdHistory,
2360
+ cmdDone,
2361
+ cmdStuck,
2362
+ cmdQuery,
2363
+ cmdPlaybook,
2364
+ cmdLearnings,
2365
+ cmdPromote,
2366
+ cmdConstraint,
2367
+ cmdProgress,
2368
+ cmdCriteria,
2369
+ cmdBranch,
2370
+ cmdCommit,
2371
+ cmdSet,
2372
+ cmdGet,
2373
+ cmdAppend,
2374
+ cmdLog,
2375
+ cmdWatch,
2376
+ cmdNew,
2377
+
2378
+ // Skill functions
2379
+ isSkillInstalled,
2380
+ installSkillTo,
2381
+ handleSkillCommand,
2382
+
2383
+ // MCP functions
2384
+ startMCPServer,
2385
+ showMCPConfig,
2386
+
2387
+ // Main
2388
+ showHelp,
2389
+ main
2390
+ };