@mndrk/memx 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1969 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+
8
+ // ==================== CONFIG ====================
9
+
10
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.mem');
11
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
12
+
13
+ // ==================== SKILL ====================
14
+
15
+ const MEM_SKILL = `---
16
+ name: mem
17
+ description: Persistent memory for AI agents. Git-backed, branch-per-task, queryable.
18
+ ---
19
+
20
+ # mem - Persistent Agent Memory
21
+
22
+ Use \`mem\` to maintain state, track progress, and accumulate learnings across sessions.
23
+
24
+ ## Architecture
25
+
26
+ - **Git-backed**: All state is versioned and syncable
27
+ - **Branches = Tasks**: Each task/goal is a separate branch
28
+ - **Two scopes**: Task-local memory + global playbook
29
+ - **Wake system**: Store schedule intent, export to cron
30
+
31
+ ## On Wake (Start of Session)
32
+
33
+ \`\`\`bash
34
+ mem context # Load full state: goal, progress, learnings
35
+ \`\`\`
36
+
37
+ ## Core Commands
38
+
39
+ ### Lifecycle
40
+ \`\`\`bash
41
+ mem init <name> "<goal>" # Start new task (creates branch)
42
+ mem status # Current state summary
43
+ mem done # Complete task, reflect, merge to main
44
+ \`\`\`
45
+
46
+ ### Goal & Criteria
47
+ \`\`\`bash
48
+ mem goal [value] # Get/set current goal
49
+ mem criteria add "..." # Add success criterion
50
+ mem criteria <n> # Mark criterion #n complete
51
+ mem progress # Show progress % against criteria
52
+ mem constraint add "..." # Add constraint/boundary
53
+ mem constraints # List constraints
54
+ \`\`\`
55
+
56
+ ### Progress
57
+ \`\`\`bash
58
+ mem next [step] # Get/set next step
59
+ mem checkpoint "<msg>" # Save progress point
60
+ mem stuck [reason|clear] # Mark/clear blocker
61
+ \`\`\`
62
+
63
+ ### Learning
64
+ \`\`\`bash
65
+ mem learn "<insight>" # Add task learning
66
+ mem learn -g "<insight>" # Add global learning
67
+ mem learnings # List learnings with IDs
68
+ mem playbook # View global playbook
69
+ mem promote <n> # Promote learning #n to playbook
70
+ \`\`\`
71
+
72
+ ### Tasks (Isolation)
73
+ \`\`\`bash
74
+ mem tasks # List all tasks (branches)
75
+ mem switch <name> # Switch to different task
76
+ \`\`\`
77
+
78
+ ### Wake & Sync
79
+ \`\`\`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
+ mem sync # Push/pull with remote
84
+ \`\`\`
85
+
86
+ ## File Structure
87
+
88
+ \`\`\`
89
+ .mem/
90
+ goal.md # Objective + criteria + constraints
91
+ state.md # Progress, next step, blockers, wake
92
+ memory.md # Task-specific learnings
93
+ playbook.md # Global learnings (shared)
94
+ \`\`\`
95
+
96
+ ## Typical Session Loop
97
+
98
+ 1. \`mem context\` - Load state on wake
99
+ 2. \`mem next\` - See what to work on
100
+ 3. Do work
101
+ 4. \`mem checkpoint "..."\` - Save progress
102
+ 5. \`mem learn "..."\` - Capture insights
103
+ 6. \`mem next "..."\` - Set next step for future self
104
+
105
+ ## When to use mem
106
+
107
+ - Tracking long-running goals across sessions
108
+ - Accumulating learnings that persist
109
+ - Coordinating between multiple agents
110
+ - Maintaining continuity when context resets
111
+ `;
112
+
113
+ // ==================== COLORS ====================
114
+
115
+ const c = {
116
+ reset: '\x1b[0m',
117
+ bold: '\x1b[1m',
118
+ dim: '\x1b[2m',
119
+ green: '\x1b[32m',
120
+ yellow: '\x1b[33m',
121
+ blue: '\x1b[34m',
122
+ cyan: '\x1b[36m',
123
+ red: '\x1b[31m',
124
+ magenta: '\x1b[35m'
125
+ };
126
+
127
+ // ==================== UTILS ====================
128
+
129
+ function loadConfig() {
130
+ try {
131
+ if (fs.existsSync(CONFIG_FILE)) {
132
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
133
+ }
134
+ } catch {}
135
+ return { repos: {} };
136
+ }
137
+
138
+ function saveConfig(config) {
139
+ if (!fs.existsSync(CONFIG_DIR)) {
140
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
141
+ }
142
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
143
+ }
144
+
145
+ function prompt(question) {
146
+ const rl = readline.createInterface({
147
+ input: process.stdin,
148
+ output: process.stdout
149
+ });
150
+ return new Promise(resolve => {
151
+ rl.question(question, answer => {
152
+ rl.close();
153
+ resolve(answer.trim());
154
+ });
155
+ });
156
+ }
157
+
158
+ // Central mem location
159
+ const CENTRAL_MEM = path.join(process.env.HOME || process.env.USERPROFILE, '.mem');
160
+ const INDEX_FILE = path.join(CENTRAL_MEM, 'index.json');
161
+
162
+ // Load/save index
163
+ function loadIndex() {
164
+ try {
165
+ if (fs.existsSync(INDEX_FILE)) {
166
+ return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
167
+ }
168
+ } catch {}
169
+ return {};
170
+ }
171
+
172
+ function saveIndex(index) {
173
+ if (!fs.existsSync(CENTRAL_MEM)) {
174
+ fs.mkdirSync(CENTRAL_MEM, { recursive: true });
175
+ }
176
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
177
+ }
178
+
179
+ // Find .mem directory (check local first, then central with index)
180
+ function findMemDir(startDir = process.cwd()) {
181
+ // First check local .mem (backwards compat)
182
+ let dir = startDir;
183
+ while (dir !== path.dirname(dir)) {
184
+ const memDir = path.join(dir, '.mem');
185
+ if (fs.existsSync(memDir) && fs.existsSync(path.join(memDir, '.git'))) {
186
+ return { memDir, isLocal: true, taskBranch: null };
187
+ }
188
+ dir = path.dirname(dir);
189
+ }
190
+
191
+ // Then check central ~/.mem with index
192
+ if (fs.existsSync(CENTRAL_MEM) && fs.existsSync(path.join(CENTRAL_MEM, '.git'))) {
193
+ const index = loadIndex();
194
+ const taskBranch = index[startDir];
195
+ if (taskBranch) {
196
+ return { memDir: CENTRAL_MEM, isLocal: false, taskBranch };
197
+ }
198
+ // Central exists but no mapping for this dir
199
+ return { memDir: CENTRAL_MEM, isLocal: false, taskBranch: null, unmapped: true };
200
+ }
201
+
202
+ return null;
203
+ }
204
+
205
+ // Get current task branch for central mem
206
+ function ensureTaskBranch(memDir, taskBranch) {
207
+ if (!taskBranch) return;
208
+
209
+ const currentBranch = git(memDir, 'rev-parse', '--abbrev-ref', 'HEAD');
210
+ if (currentBranch !== taskBranch) {
211
+ git(memDir, 'checkout', taskBranch);
212
+ }
213
+ }
214
+
215
+ // Run git command in .mem directory
216
+ function git(memDir, ...args) {
217
+ try {
218
+ // Use spawn-style args for proper escaping
219
+ const result = require('child_process').spawnSync('git', args, {
220
+ cwd: memDir,
221
+ encoding: 'utf8',
222
+ stdio: ['pipe', 'pipe', 'pipe']
223
+ });
224
+ if (result.status !== 0) {
225
+ throw new Error(result.stderr?.trim() || 'Git command failed');
226
+ }
227
+ return (result.stdout || '').trim();
228
+ } catch (err) {
229
+ throw err;
230
+ }
231
+ }
232
+
233
+ // Get current branch
234
+ function getCurrentBranch(memDir) {
235
+ return git(memDir, 'rev-parse', '--abbrev-ref', 'HEAD');
236
+ }
237
+
238
+ // Read a file from .mem
239
+ function readMemFile(memDir, filename) {
240
+ const filepath = path.join(memDir, filename);
241
+ if (fs.existsSync(filepath)) {
242
+ return fs.readFileSync(filepath, 'utf8');
243
+ }
244
+ return null;
245
+ }
246
+
247
+ // Write a file to .mem
248
+ function writeMemFile(memDir, filename, content) {
249
+ const filepath = path.join(memDir, filename);
250
+ fs.writeFileSync(filepath, content);
251
+ }
252
+
253
+ // Parse frontmatter from markdown
254
+ function parseFrontmatter(content) {
255
+ if (!content) return { frontmatter: {}, body: '' };
256
+
257
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
258
+ if (match) {
259
+ const frontmatter = {};
260
+ match[1].split('\n').forEach(line => {
261
+ const [key, ...rest] = line.split(':');
262
+ if (key && rest.length) {
263
+ frontmatter[key.trim()] = rest.join(':').trim();
264
+ }
265
+ });
266
+ return { frontmatter, body: match[2].trim() };
267
+ }
268
+ return { frontmatter: {}, body: content };
269
+ }
270
+
271
+ // Serialize frontmatter + body
272
+ function serializeFrontmatter(frontmatter, body) {
273
+ const fm = Object.entries(frontmatter)
274
+ .map(([k, v]) => `${k}: ${v}`)
275
+ .join('\n');
276
+ return `---\n${fm}\n---\n\n${body}`;
277
+ }
278
+
279
+ // ==================== COMMANDS ====================
280
+
281
+ // Interactive onboarding (centralized by default)
282
+ async function interactiveInit() {
283
+ console.log(`\n${c.bold}${c.cyan}mem${c.reset} ${c.dim}— persistent memory for AI agents${c.reset}\n`);
284
+
285
+ // Get goal
286
+ const goalText = await prompt(`${c.bold}What are you working on?${c.reset}\n> `);
287
+ if (!goalText) {
288
+ console.log(`${c.dim}Cancelled${c.reset}`);
289
+ return null;
290
+ }
291
+
292
+ // Generate task name from goal
293
+ const name = goalText
294
+ .toLowerCase()
295
+ .replace(/[^a-z0-9\s]/g, '')
296
+ .split(/\s+/)
297
+ .slice(0, 3)
298
+ .join('-');
299
+
300
+ console.log(`\n${c.bold}How will you know it's done?${c.reset} ${c.dim}(add criteria, empty line to finish)${c.reset}\n`);
301
+
302
+ const criteria = [];
303
+ let i = 1;
304
+ while (true) {
305
+ const criterion = await prompt(`${c.dim}${i}.${c.reset} `);
306
+ if (!criterion) break;
307
+ criteria.push(criterion);
308
+ i++;
309
+ }
310
+
311
+ // Use central ~/.mem
312
+ const targetDir = CENTRAL_MEM;
313
+ const isNewRepo = !fs.existsSync(path.join(targetDir, '.git'));
314
+
315
+ if (!fs.existsSync(targetDir)) {
316
+ fs.mkdirSync(targetDir, { recursive: true });
317
+ }
318
+
319
+ try {
320
+ if (isNewRepo) {
321
+ require('child_process').execSync('git init', { cwd: targetDir, stdio: 'ignore' });
322
+ writeMemFile(targetDir, 'playbook.md', `# Playbook\n\nGlobal learnings that transfer across tasks.\n`);
323
+ writeMemFile(targetDir, '.gitignore', '');
324
+ git(targetDir, 'add', '-A');
325
+ git(targetDir, 'commit', '-m', 'init: memory repo');
326
+ } else {
327
+ try { git(targetDir, 'checkout', 'main'); } catch {}
328
+ }
329
+
330
+ // Create task branch
331
+ const branch = `task/${name}`;
332
+ git(targetDir, 'checkout', '-b', branch);
333
+
334
+ // Build criteria markdown
335
+ const criteriaText = criteria.length
336
+ ? criteria.map(c => `- [ ] ${c}`).join('\n')
337
+ : '- [ ] Define your first criterion';
338
+
339
+ writeMemFile(targetDir, 'goal.md', serializeFrontmatter(
340
+ { task: name, created: new Date().toISOString().split('T')[0] },
341
+ `# Goal\n\n${goalText}\n\n## Definition of Done\n\n${criteriaText}\n\n## Progress: 0%`
342
+ ));
343
+ writeMemFile(targetDir, 'state.md', serializeFrontmatter(
344
+ { status: 'active' },
345
+ `# State\n\n## Next Step\n\nDefine approach\n\n## Checkpoints\n\n- [ ] Started`
346
+ ));
347
+ writeMemFile(targetDir, 'memory.md', `# Learnings\n\n`);
348
+
349
+ git(targetDir, 'add', '-A');
350
+ git(targetDir, 'commit', '-m', `init: ${name}`);
351
+
352
+ // Update index with cwd mapping
353
+ const index = loadIndex();
354
+ index[process.cwd()] = branch;
355
+ saveIndex(index);
356
+
357
+ console.log(`\n${c.green}✓${c.reset} Created task: ${c.bold}${name}${c.reset}`);
358
+ console.log(`${c.green}✓${c.reset} Location: ${c.dim}~/.mem${c.reset}`);
359
+ console.log(`${c.green}✓${c.reset} Mapped: ${c.dim}${process.cwd()} → ${branch}${c.reset}`);
360
+ if (criteria.length) {
361
+ console.log(`${c.green}✓${c.reset} ${criteria.length} success criteria defined`);
362
+ }
363
+
364
+ console.log(`\n${c.dim}Run ${c.reset}mem status${c.dim} to see your progress${c.reset}\n`);
365
+
366
+ return { memDir: targetDir, taskBranch: branch, name };
367
+
368
+ } catch (err) {
369
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
370
+ return null;
371
+ }
372
+ }
373
+
374
+ // Setup git remote
375
+ async function setupRemote(memDir, taskName) {
376
+ // Check if gh CLI is available
377
+ let hasGh = false;
378
+ try {
379
+ require('child_process').execSync('which gh', { stdio: 'ignore' });
380
+ hasGh = true;
381
+ } catch {}
382
+
383
+ const existingRepo = await prompt(`\n${c.dim}GitHub repo URL (leave empty to create new):${c.reset} `);
384
+
385
+ if (existingRepo) {
386
+ // Connect to existing repo
387
+ try {
388
+ git(memDir, 'remote', 'add', 'origin', existingRepo);
389
+ git(memDir, 'push', '-u', 'origin', 'main');
390
+ git(memDir, 'push', '-u', 'origin', `task/${taskName}`);
391
+ console.log(`${c.green}✓${c.reset} Connected to ${c.dim}${existingRepo}${c.reset}`);
392
+ } catch (err) {
393
+ console.log(`${c.yellow}Could not connect:${c.reset} ${err.message}`);
394
+ console.log(`${c.dim}You can set it up later with: cd .mem && git remote add origin <url>${c.reset}`);
395
+ }
396
+ return;
397
+ }
398
+
399
+ if (!hasGh) {
400
+ console.log(`${c.yellow}GitHub CLI (gh) not found.${c.reset}`);
401
+ console.log(`${c.dim}Install it to auto-create repos: brew install gh${c.reset}`);
402
+ console.log(`${c.dim}Or set up manually: cd .mem && git remote add origin <url>${c.reset}`);
403
+ return;
404
+ }
405
+
406
+ // Create new repo with gh
407
+ const repoName = await prompt(`${c.dim}Repo name${c.reset} [${taskName}-mem]: `) || `${taskName}-mem`;
408
+ const isPrivate = await prompt(`${c.dim}Private repo?${c.reset} [Y/n]: `);
409
+ const visibility = (isPrivate.toLowerCase() === 'n') ? '--public' : '--private';
410
+
411
+ try {
412
+ const { execSync } = require('child_process');
413
+ execSync(`gh repo create ${repoName} ${visibility} --source=. --push`, {
414
+ cwd: memDir,
415
+ stdio: ['pipe', 'pipe', 'pipe']
416
+ });
417
+
418
+ // Also push the task branch
419
+ git(memDir, 'push', '-u', 'origin', `task/${taskName}`);
420
+
421
+ console.log(`${c.green}✓${c.reset} Created repo: ${c.cyan}${repoName}${c.reset}`);
422
+ console.log(`${c.dim}Run ${c.reset}mem sync${c.dim} anytime to push/pull${c.reset}`);
423
+ } catch (err) {
424
+ console.log(`${c.yellow}Could not create repo:${c.reset} ${err.message}`);
425
+ console.log(`${c.dim}You can set it up later manually${c.reset}`);
426
+ }
427
+ }
428
+
429
+ // Initialize a new memory repo
430
+ async function cmdInit(args, memDir) {
431
+ const name = args[0];
432
+ const goal = args.slice(1).join(' ');
433
+
434
+ // Interactive mode if no args and no existing .mem
435
+ if (!name && !memDir) {
436
+ return await interactiveInit();
437
+ }
438
+
439
+ if (!name) {
440
+ console.log(`${c.red}Usage:${c.reset} mem init <name> "<goal>"`);
441
+ return;
442
+ }
443
+
444
+ // If already in a mem repo, create a new task branch
445
+ if (memDir) {
446
+ const branch = `task/${name}`;
447
+ try {
448
+ git(memDir, 'checkout', '-b', branch);
449
+ console.log(`${c.green}✓${c.reset} Created task branch: ${c.cyan}${branch}${c.reset}`);
450
+
451
+ if (goal) {
452
+ writeMemFile(memDir, 'goal.md', serializeFrontmatter(
453
+ { task: name, created: new Date().toISOString().split('T')[0] },
454
+ `# Goal\n\n${goal}\n\n## Definition of Done\n\n- [ ] Define success criteria\n\n## Progress: 0%`
455
+ ));
456
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(
457
+ { status: 'active' },
458
+ `# State\n\n## Next Step\n\nDefine approach\n\n## Checkpoints\n\n- [ ] Started`
459
+ ));
460
+ writeMemFile(memDir, 'memory.md', `# Learnings\n\n`);
461
+
462
+ git(memDir, 'add', '-A');
463
+ git(memDir, 'commit', '-m', `init: ${name}`);
464
+
465
+ console.log(`${c.green}✓${c.reset} Initialized task: ${goal}`);
466
+ }
467
+ } catch (err) {
468
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
469
+ }
470
+ return;
471
+ }
472
+
473
+ // Create new .mem repo
474
+ const targetDir = path.join(process.cwd(), '.mem');
475
+
476
+ if (fs.existsSync(targetDir)) {
477
+ console.log(`${c.yellow}.mem already exists in this directory${c.reset}`);
478
+ return;
479
+ }
480
+
481
+ fs.mkdirSync(targetDir, { recursive: true });
482
+
483
+ try {
484
+ execSync('git init', { cwd: targetDir, stdio: 'ignore' });
485
+
486
+ // Create initial files on main
487
+ writeMemFile(targetDir, 'playbook.md', `# Playbook\n\nGlobal learnings that transfer across tasks.\n`);
488
+ writeMemFile(targetDir, '.gitignore', '');
489
+
490
+ git(targetDir, 'add', '-A');
491
+ git(targetDir, 'commit', '-m', 'init: memory repo');
492
+
493
+ console.log(`${c.green}✓${c.reset} Created memory repo: ${c.dim}${targetDir}${c.reset}`);
494
+
495
+ // Create task branch if name provided
496
+ if (name && name !== 'main') {
497
+ const branch = `task/${name}`;
498
+ git(targetDir, 'checkout', '-b', branch);
499
+
500
+ writeMemFile(targetDir, 'goal.md', serializeFrontmatter(
501
+ { task: name, created: new Date().toISOString().split('T')[0] },
502
+ `# Goal\n\n${goal || 'Define your goal here'}\n\n## Definition of Done\n\n- [ ] Criterion 1\n- [ ] Criterion 2\n- [ ] Criterion 3\n\n## Progress: 0%`
503
+ ));
504
+ writeMemFile(targetDir, 'state.md', serializeFrontmatter(
505
+ { status: 'active' },
506
+ `# State\n\n## Next Step\n\nDefine approach\n\n## Checkpoints\n\n- [ ] Started`
507
+ ));
508
+ writeMemFile(targetDir, 'memory.md', `# Learnings\n\n`);
509
+
510
+ git(targetDir, 'add', '-A');
511
+ git(targetDir, 'commit', '-m', `init: ${name}`);
512
+
513
+ console.log(`${c.green}✓${c.reset} Created task: ${c.cyan}${name}${c.reset}`);
514
+ if (goal) {
515
+ console.log(`${c.dim}Goal: ${goal}${c.reset}`);
516
+ }
517
+ }
518
+ } catch (err) {
519
+ console.log(`${c.red}Error:${c.reset} ${err.message}`);
520
+ // Cleanup on failure
521
+ fs.rmSync(targetDir, { recursive: true, force: true });
522
+ }
523
+ }
524
+
525
+ // Show current status
526
+ function cmdStatus(memDir) {
527
+ if (!memDir) {
528
+ console.log(`${c.yellow}No .mem repo found.${c.reset} Run ${c.cyan}mem init${c.reset} first.`);
529
+ return;
530
+ }
531
+
532
+ const branch = getCurrentBranch(memDir);
533
+ const goal = readMemFile(memDir, 'goal.md');
534
+ const state = readMemFile(memDir, 'state.md');
535
+
536
+ console.log(`\n${c.bold}Memory Status${c.reset}\n`);
537
+ console.log(`${c.dim}Branch:${c.reset} ${c.cyan}${branch}${c.reset}`);
538
+ console.log(`${c.dim}Repo:${c.reset} ${memDir}`);
539
+
540
+ if (goal) {
541
+ const { frontmatter, body } = parseFrontmatter(goal);
542
+ const goalLine = body.split('\n').find(l => l && !l.startsWith('#'));
543
+ if (goalLine) {
544
+ console.log(`\n${c.bold}Goal:${c.reset} ${goalLine}`);
545
+ }
546
+ if (frontmatter.status) {
547
+ console.log(`${c.dim}Status:${c.reset} ${frontmatter.status}`);
548
+ }
549
+ }
550
+
551
+ if (state) {
552
+ const { body } = parseFrontmatter(state);
553
+ const nextMatch = body.match(/## Next Step\n\n([^\n#]+)/);
554
+ if (nextMatch) {
555
+ console.log(`\n${c.bold}Next:${c.reset} ${nextMatch[1]}`);
556
+ }
557
+
558
+ // Show recent checkpoints
559
+ const checkpointsMatch = body.match(/## Checkpoints\n\n([\s\S]*?)(?=\n##|$)/);
560
+ if (checkpointsMatch) {
561
+ const checkpoints = checkpointsMatch[1].trim().split('\n').slice(-3);
562
+ if (checkpoints.length) {
563
+ console.log(`\n${c.bold}Recent Checkpoints:${c.reset}`);
564
+ checkpoints.forEach(cp => console.log(` ${cp}`));
565
+ }
566
+ }
567
+ }
568
+
569
+ console.log('');
570
+ }
571
+
572
+ // Get/set goal
573
+ function cmdGoal(args, memDir) {
574
+ if (!memDir) {
575
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
576
+ return;
577
+ }
578
+
579
+ if (args.length === 0) {
580
+ // Get goal
581
+ const goal = readMemFile(memDir, 'goal.md');
582
+ if (goal) {
583
+ console.log(goal);
584
+ } else {
585
+ console.log(`${c.dim}No goal set${c.reset}`);
586
+ }
587
+ return;
588
+ }
589
+
590
+ // Set goal
591
+ const newGoal = args.join(' ');
592
+ const existing = readMemFile(memDir, 'goal.md');
593
+ const { frontmatter } = parseFrontmatter(existing || '');
594
+
595
+ writeMemFile(memDir, 'goal.md', serializeFrontmatter(
596
+ { ...frontmatter, updated: new Date().toISOString().split('T')[0] },
597
+ `# Goal\n\n${newGoal}`
598
+ ));
599
+
600
+ git(memDir, 'add', 'goal.md');
601
+ git(memDir, 'commit', '-m', `goal: ${newGoal.slice(0, 50)}`);
602
+
603
+ console.log(`${c.green}✓${c.reset} Goal updated`);
604
+ }
605
+
606
+ // Get/set next step
607
+ function cmdNext(args, memDir) {
608
+ if (!memDir) {
609
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
610
+ return;
611
+ }
612
+
613
+ const state = readMemFile(memDir, 'state.md') || '';
614
+ const { frontmatter, body } = parseFrontmatter(state);
615
+
616
+ if (args.length === 0) {
617
+ // Get next step
618
+ const nextMatch = body.match(/## Next Step\n\n([^\n#]+)/);
619
+ if (nextMatch) {
620
+ console.log(nextMatch[1]);
621
+ } else {
622
+ console.log(`${c.dim}No next step set${c.reset}`);
623
+ }
624
+ return;
625
+ }
626
+
627
+ // Set next step
628
+ const nextStep = args.join(' ');
629
+ let newBody = body;
630
+
631
+ if (body.includes('## Next Step')) {
632
+ newBody = body.replace(/## Next Step\n\n[^\n#]*/, `## Next Step\n\n${nextStep}`);
633
+ } else {
634
+ newBody = `## Next Step\n\n${nextStep}\n\n${body}`;
635
+ }
636
+
637
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, newBody));
638
+ git(memDir, 'add', 'state.md');
639
+ git(memDir, 'commit', '-m', `next: ${nextStep.slice(0, 50)}`);
640
+
641
+ console.log(`${c.green}✓${c.reset} Next step: ${nextStep}`);
642
+ }
643
+
644
+ // Add checkpoint
645
+ function cmdCheckpoint(args, memDir) {
646
+ if (!memDir) {
647
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
648
+ return;
649
+ }
650
+
651
+ const msg = args.join(' ');
652
+ if (!msg) {
653
+ console.log(`${c.red}Usage:${c.reset} mem checkpoint "<message>"`);
654
+ return;
655
+ }
656
+
657
+ const state = readMemFile(memDir, 'state.md') || '';
658
+ const { frontmatter, body } = parseFrontmatter(state);
659
+
660
+ const timestamp = new Date().toISOString().split('T')[0];
661
+ const checkpoint = `- [x] ${timestamp}: ${msg}`;
662
+
663
+ let newBody = body;
664
+ if (body.includes('## Checkpoints')) {
665
+ newBody = body.replace(/## Checkpoints\n\n/, `## Checkpoints\n\n${checkpoint}\n`);
666
+ } else {
667
+ newBody = `${body}\n\n## Checkpoints\n\n${checkpoint}`;
668
+ }
669
+
670
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, newBody));
671
+ git(memDir, 'add', 'state.md');
672
+ git(memDir, 'commit', '-m', `checkpoint: ${msg.slice(0, 50)}`);
673
+
674
+ console.log(`${c.green}✓${c.reset} Checkpoint: ${msg}`);
675
+ }
676
+
677
+ // Add learning
678
+ function cmdLearn(args, memDir) {
679
+ if (!memDir) {
680
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
681
+ return;
682
+ }
683
+
684
+ // Check for -g flag (global)
685
+ const isGlobal = args[0] === '-g';
686
+ if (isGlobal) args = args.slice(1);
687
+
688
+ const insight = args.join(' ');
689
+ if (!insight) {
690
+ console.log(`${c.red}Usage:${c.reset} mem learn [-g] "<insight>"`);
691
+ return;
692
+ }
693
+
694
+ const filename = isGlobal ? 'playbook.md' : 'memory.md';
695
+ const content = readMemFile(memDir, filename) || `# ${isGlobal ? 'Playbook' : 'Learnings'}\n\n`;
696
+
697
+ const timestamp = new Date().toISOString().split('T')[0];
698
+ const newContent = content + `- ${timestamp}: ${insight}\n`;
699
+
700
+ writeMemFile(memDir, filename, newContent);
701
+ git(memDir, 'add', filename);
702
+ git(memDir, 'commit', '-m', `learn: ${insight.slice(0, 50)}`);
703
+
704
+ console.log(`${c.green}✓${c.reset} ${isGlobal ? 'Global' : 'Task'} learning added`);
705
+ }
706
+
707
+ // Show context (full hydration)
708
+ function cmdContext(memDir) {
709
+ if (!memDir) {
710
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
711
+ return;
712
+ }
713
+
714
+ const branch = getCurrentBranch(memDir);
715
+ const goal = readMemFile(memDir, 'goal.md');
716
+ const state = readMemFile(memDir, 'state.md');
717
+ const memory = readMemFile(memDir, 'memory.md');
718
+ const playbook = readMemFile(memDir, 'playbook.md');
719
+
720
+ console.log(`# Context\n`);
721
+ console.log(`Branch: ${branch}\n`);
722
+
723
+ if (goal) {
724
+ console.log(`## Goal\n`);
725
+ const { body } = parseFrontmatter(goal);
726
+ const goalLine = body.split('\n').find(l => l && !l.startsWith('#'));
727
+ console.log(goalLine || 'Not set');
728
+ console.log('');
729
+ }
730
+
731
+ if (state) {
732
+ console.log(`## State\n`);
733
+ const { body } = parseFrontmatter(state);
734
+ console.log(body);
735
+ console.log('');
736
+ }
737
+
738
+ if (memory && memory.trim() !== '# Learnings\n\n' && memory.trim() !== '# Learnings') {
739
+ console.log(`## Task Learnings\n`);
740
+ const lines = memory.split('\n').filter(l => l.startsWith('- '));
741
+ lines.forEach(l => console.log(l));
742
+ console.log('');
743
+ }
744
+
745
+ if (playbook && playbook.trim() !== '# Playbook\n\nGlobal learnings that transfer across tasks.\n') {
746
+ console.log(`## Playbook (Global)\n`);
747
+ const lines = playbook.split('\n').filter(l => l.startsWith('- '));
748
+ lines.slice(-10).forEach(l => console.log(l)); // Last 10 global learnings
749
+ console.log('');
750
+ }
751
+ }
752
+
753
+ // List tasks
754
+ function cmdTasks(memDir) {
755
+ if (!memDir) {
756
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
757
+ return;
758
+ }
759
+
760
+ const current = getCurrentBranch(memDir);
761
+ const branches = git(memDir, 'branch', '--list').split('\n');
762
+
763
+ console.log(`\n${c.bold}Tasks${c.reset}\n`);
764
+
765
+ branches.forEach(b => {
766
+ const name = b.replace('*', '').trim();
767
+ const isCurrent = b.includes('*');
768
+ const marker = isCurrent ? `${c.green}→${c.reset}` : ' ';
769
+ const display = name.replace('task/', '');
770
+ console.log(`${marker} ${isCurrent ? c.cyan : c.dim}${display}${c.reset}`);
771
+ });
772
+
773
+ console.log('');
774
+ }
775
+
776
+ // Switch task
777
+ function cmdSwitch(args, memDir) {
778
+ if (!memDir) {
779
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
780
+ return;
781
+ }
782
+
783
+ const name = args[0];
784
+ if (!name) {
785
+ console.log(`${c.red}Usage:${c.reset} mem switch <name>`);
786
+ return;
787
+ }
788
+
789
+ const branch = name.startsWith('task/') ? name : `task/${name}`;
790
+
791
+ try {
792
+ git(memDir, 'checkout', branch);
793
+ console.log(`${c.green}✓${c.reset} Switched to: ${c.cyan}${branch}${c.reset}`);
794
+ } catch (err) {
795
+ // Try without task/ prefix (for main, etc)
796
+ try {
797
+ git(memDir, 'checkout', name);
798
+ console.log(`${c.green}✓${c.reset} Switched to: ${c.cyan}${name}${c.reset}`);
799
+ } catch {
800
+ console.log(`${c.red}Branch not found:${c.reset} ${name}`);
801
+ }
802
+ }
803
+ }
804
+
805
+ // Sync with remote
806
+ function cmdSync(memDir) {
807
+ if (!memDir) {
808
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
809
+ return;
810
+ }
811
+
812
+ try {
813
+ // Check if remote exists
814
+ const remotes = git(memDir, 'remote');
815
+ if (!remotes) {
816
+ console.log(`${c.yellow}No remote configured.${c.reset}`);
817
+ console.log(`${c.dim}Add one with: cd ${memDir} && git remote add origin <url>${c.reset}`);
818
+ return;
819
+ }
820
+
821
+ console.log(`${c.cyan}Syncing...${c.reset}`);
822
+
823
+ const branch = getCurrentBranch(memDir);
824
+
825
+ try {
826
+ git(memDir, 'pull', '--rebase', 'origin', branch);
827
+ console.log(`${c.green}✓${c.reset} Pulled latest`);
828
+ } catch {
829
+ // Branch might not exist on remote yet
830
+ }
831
+
832
+ git(memDir, 'push', '-u', 'origin', branch);
833
+ console.log(`${c.green}✓${c.reset} Pushed to origin/${branch}`);
834
+
835
+ } catch (err) {
836
+ console.log(`${c.red}Sync failed:${c.reset} ${err.message}`);
837
+ }
838
+ }
839
+
840
+ // Show history
841
+ function cmdHistory(memDir) {
842
+ if (!memDir) {
843
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
844
+ return;
845
+ }
846
+
847
+ const log = git(memDir, 'log', '--oneline', '-20');
848
+ console.log(`\n${c.bold}History${c.reset}\n`);
849
+ console.log(log);
850
+ console.log('');
851
+ }
852
+
853
+ // Complete task
854
+ async function cmdDone(memDir) {
855
+ if (!memDir) {
856
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
857
+ return;
858
+ }
859
+
860
+ const branch = getCurrentBranch(memDir);
861
+
862
+ if (branch === 'main') {
863
+ console.log(`${c.yellow}Already on main branch.${c.reset}`);
864
+ return;
865
+ }
866
+
867
+ console.log(`\n${c.bold}Completing task: ${branch}${c.reset}\n`);
868
+
869
+ // Prompt for reflection
870
+ console.log(`${c.cyan}Reflection${c.reset} - What learnings should be promoted to the global playbook?\n`);
871
+
872
+ const memory = readMemFile(memDir, 'memory.md') || '';
873
+ const learnings = memory.split('\n').filter(l => l.startsWith('- '));
874
+
875
+ if (learnings.length) {
876
+ console.log(`${c.dim}Task learnings:${c.reset}`);
877
+ learnings.forEach((l, i) => console.log(` ${i + 1}. ${l.slice(2)}`));
878
+ console.log('');
879
+ }
880
+
881
+ const toPromote = await prompt('Enter numbers to promote (comma-separated) or "none": ');
882
+
883
+ if (toPromote && toPromote !== 'none') {
884
+ const indices = toPromote.split(',').map(n => parseInt(n.trim()) - 1);
885
+ const playbook = readMemFile(memDir, 'playbook.md') || '# Playbook\n\n';
886
+
887
+ let additions = '';
888
+ indices.forEach(i => {
889
+ if (learnings[i]) {
890
+ additions += learnings[i] + '\n';
891
+ }
892
+ });
893
+
894
+ if (additions) {
895
+ writeMemFile(memDir, 'playbook.md', playbook + additions);
896
+ git(memDir, 'add', 'playbook.md');
897
+ git(memDir, 'commit', '-m', 'promote learnings to playbook');
898
+ console.log(`${c.green}✓${c.reset} Promoted learnings to playbook`);
899
+ }
900
+ }
901
+
902
+ // Merge to main
903
+ try {
904
+ git(memDir, 'checkout', 'main');
905
+ git(memDir, 'merge', branch, '-m', `done: ${branch}`);
906
+ console.log(`${c.green}✓${c.reset} Merged ${branch} to main`);
907
+
908
+ // Optionally delete branch
909
+ const deleteBranch = await prompt('Delete the task branch? [y/N]: ');
910
+ if (deleteBranch.toLowerCase() === 'y') {
911
+ git(memDir, 'branch', '-d', branch);
912
+ console.log(`${c.green}✓${c.reset} Deleted branch: ${branch}`);
913
+ }
914
+ } catch (err) {
915
+ console.log(`${c.red}Merge failed:${c.reset} ${err.message}`);
916
+ git(memDir, 'checkout', branch);
917
+ }
918
+
919
+ console.log('');
920
+ }
921
+
922
+ // Mark stuck
923
+ function cmdStuck(args, memDir) {
924
+ if (!memDir) {
925
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
926
+ return;
927
+ }
928
+
929
+ const state = readMemFile(memDir, 'state.md') || '';
930
+ const { frontmatter, body } = parseFrontmatter(state);
931
+
932
+ if (args.length === 0) {
933
+ // Show blocker or clear
934
+ if (frontmatter.blocker) {
935
+ console.log(`${c.red}Blocker:${c.reset} ${frontmatter.blocker}`);
936
+ } else {
937
+ console.log(`${c.green}No blockers${c.reset}`);
938
+ }
939
+ return;
940
+ }
941
+
942
+ const reason = args.join(' ');
943
+
944
+ if (reason === 'clear') {
945
+ delete frontmatter.blocker;
946
+ frontmatter.status = 'active';
947
+ } else {
948
+ frontmatter.blocker = reason;
949
+ frontmatter.status = 'blocked';
950
+ }
951
+
952
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
953
+ git(memDir, 'add', 'state.md');
954
+ git(memDir, 'commit', '-m', reason === 'clear' ? 'unblocked' : `stuck: ${reason.slice(0, 50)}`);
955
+
956
+ if (reason === 'clear') {
957
+ console.log(`${c.green}✓${c.reset} Blocker cleared`);
958
+ } else {
959
+ console.log(`${c.yellow}⚠${c.reset} Marked as stuck: ${reason}`);
960
+ }
961
+ }
962
+
963
+ // Query (simple grep for now)
964
+ function cmdQuery(args, memDir) {
965
+ if (!memDir) {
966
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
967
+ return;
968
+ }
969
+
970
+ const search = args.join(' ');
971
+ if (!search) {
972
+ console.log(`${c.red}Usage:${c.reset} mem query "<search>"`);
973
+ return;
974
+ }
975
+
976
+ try {
977
+ const results = execSync(`grep -r -i -n "${search}" --include="*.md"`, {
978
+ cwd: memDir,
979
+ encoding: 'utf8'
980
+ });
981
+ console.log(results);
982
+ } catch {
983
+ console.log(`${c.dim}No matches found${c.reset}`);
984
+ }
985
+ }
986
+
987
+ // Show playbook
988
+ function cmdPlaybook(memDir) {
989
+ if (!memDir) {
990
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
991
+ return;
992
+ }
993
+
994
+ const playbook = readMemFile(memDir, 'playbook.md');
995
+ if (playbook) {
996
+ console.log(playbook);
997
+ } else {
998
+ console.log(`${c.dim}No playbook yet${c.reset}`);
999
+ }
1000
+ }
1001
+
1002
+ // List learnings with IDs
1003
+ function cmdLearnings(args, memDir) {
1004
+ if (!memDir) {
1005
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1006
+ return;
1007
+ }
1008
+
1009
+ const isGlobal = args[0] === '-g';
1010
+ const filename = isGlobal ? 'playbook.md' : 'memory.md';
1011
+ const content = readMemFile(memDir, filename) || '';
1012
+
1013
+ const lines = content.split('\n').filter(l => l.startsWith('- '));
1014
+
1015
+ if (lines.length === 0) {
1016
+ console.log(`${c.dim}No learnings yet${c.reset}`);
1017
+ return;
1018
+ }
1019
+
1020
+ console.log(`\n${c.bold}${isGlobal ? 'Playbook' : 'Task Learnings'}${c.reset}\n`);
1021
+
1022
+ lines.forEach((line, i) => {
1023
+ // Parse: "- YYYY-MM-DD: text" or just "- text"
1024
+ const match = line.match(/^- (?:(\d{4}-\d{2}-\d{2}): )?(.+)$/);
1025
+ if (match) {
1026
+ const date = match[1] ? `${c.dim}${match[1]}${c.reset} ` : '';
1027
+ console.log(` ${c.cyan}${i + 1}.${c.reset} ${date}${match[2]}`);
1028
+ }
1029
+ });
1030
+
1031
+ if (!isGlobal) {
1032
+ console.log(`\n${c.dim}Promote with: mem promote <number>${c.reset}`);
1033
+ }
1034
+ console.log('');
1035
+ }
1036
+
1037
+ // Promote a learning to playbook
1038
+ function cmdPromote(args, memDir) {
1039
+ if (!memDir) {
1040
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1041
+ return;
1042
+ }
1043
+
1044
+ const num = parseInt(args[0]);
1045
+ if (!num) {
1046
+ console.log(`${c.red}Usage:${c.reset} mem promote <number>`);
1047
+ console.log(`${c.dim}Run 'mem learnings' to see numbered list${c.reset}`);
1048
+ return;
1049
+ }
1050
+
1051
+ const memory = readMemFile(memDir, 'memory.md') || '';
1052
+ const lines = memory.split('\n').filter(l => l.startsWith('- '));
1053
+
1054
+ if (num < 1 || num > lines.length) {
1055
+ console.log(`${c.red}Invalid number.${c.reset} You have ${lines.length} learnings.`);
1056
+ return;
1057
+ }
1058
+
1059
+ const learning = lines[num - 1];
1060
+
1061
+ // Add to playbook
1062
+ const playbook = readMemFile(memDir, 'playbook.md') || '# Playbook\n\n';
1063
+ const newPlaybook = playbook + learning + '\n';
1064
+ writeMemFile(memDir, 'playbook.md', newPlaybook);
1065
+
1066
+ git(memDir, 'add', 'playbook.md');
1067
+ git(memDir, 'commit', '-m', `promote: ${learning.slice(2, 50)}`);
1068
+
1069
+ // Extract just the text for display
1070
+ const text = learning.match(/^- (?:\d{4}-\d{2}-\d{2}: )?(.+)$/)?.[1] || learning;
1071
+ console.log(`${c.green}✓${c.reset} Promoted to playbook: "${text}"`);
1072
+ }
1073
+
1074
+ // Manage constraints
1075
+ function cmdConstraint(args, memDir) {
1076
+ if (!memDir) {
1077
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1078
+ return;
1079
+ }
1080
+
1081
+ const action = args[0];
1082
+ const text = args.slice(1).join(' ');
1083
+
1084
+ let goal = readMemFile(memDir, 'goal.md') || '';
1085
+
1086
+ // Ensure Constraints section exists
1087
+ if (!goal.includes('## Constraints')) {
1088
+ goal = goal.trimEnd() + '\n\n## Constraints\n\n';
1089
+ }
1090
+
1091
+ // List constraints
1092
+ if (!action || action === 'list') {
1093
+ const match = goal.match(/## Constraints\n\n([\s\S]*?)(?=\n## |$)/);
1094
+ if (match) {
1095
+ const lines = match[1].trim().split('\n').filter(l => l.startsWith('- '));
1096
+ if (lines.length) {
1097
+ console.log(`\n${c.bold}Constraints${c.reset}\n`);
1098
+ lines.forEach((line, i) => {
1099
+ console.log(` ${c.cyan}${i + 1}.${c.reset} ${line.slice(2)}`);
1100
+ });
1101
+ console.log('');
1102
+ return;
1103
+ }
1104
+ }
1105
+ console.log(`${c.dim}No constraints set${c.reset}`);
1106
+ console.log(`${c.dim}Add with: mem constraint add "Don't push without review"${c.reset}`);
1107
+ return;
1108
+ }
1109
+
1110
+ // Add constraint
1111
+ if (action === 'add' && text) {
1112
+ goal = goal.replace(
1113
+ /## Constraints\n\n/,
1114
+ `## Constraints\n\n- ${text}\n`
1115
+ );
1116
+ writeMemFile(memDir, 'goal.md', goal);
1117
+ git(memDir, 'add', 'goal.md');
1118
+ git(memDir, 'commit', '-m', `constraint: ${text.slice(0, 40)}`);
1119
+ console.log(`${c.green}✓${c.reset} Constraint added: ${text}`);
1120
+ return;
1121
+ }
1122
+
1123
+ // Remove constraint
1124
+ if ((action === 'remove' || action === 'rm') && text) {
1125
+ const num = parseInt(text);
1126
+ const match = goal.match(/## Constraints\n\n([\s\S]*?)(?=\n## |$)/);
1127
+ if (match) {
1128
+ const lines = match[1].split('\n');
1129
+ let constraintIndex = 0;
1130
+
1131
+ for (let i = 0; i < lines.length; i++) {
1132
+ if (lines[i].startsWith('- ')) {
1133
+ constraintIndex++;
1134
+ if (constraintIndex === num) {
1135
+ const removed = lines[i].slice(2);
1136
+ lines.splice(i, 1);
1137
+ goal = goal.replace(match[0], `## Constraints\n\n${lines.join('\n')}`);
1138
+ writeMemFile(memDir, 'goal.md', goal);
1139
+ git(memDir, 'add', 'goal.md');
1140
+ git(memDir, 'commit', '-m', `remove constraint: ${removed.slice(0, 30)}`);
1141
+ console.log(`${c.green}✓${c.reset} Removed: ${removed}`);
1142
+ return;
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+ console.log(`${c.red}Constraint #${num} not found${c.reset}`);
1148
+ return;
1149
+ }
1150
+
1151
+ // Usage
1152
+ console.log(`${c.bold}mem constraint${c.reset} - Manage task constraints\n`);
1153
+ console.log(`${c.dim}Commands:${c.reset}`);
1154
+ console.log(` ${c.cyan}mem constraint${c.reset} List constraints`);
1155
+ console.log(` ${c.cyan}mem constraint add "..."${c.reset} Add constraint`);
1156
+ console.log(` ${c.cyan}mem constraint remove <n>${c.reset} Remove by number`);
1157
+ console.log('');
1158
+ }
1159
+
1160
+ // Show/update progress
1161
+ function cmdProgress(args, memDir) {
1162
+ if (!memDir) {
1163
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1164
+ return;
1165
+ }
1166
+
1167
+ const goal = readMemFile(memDir, 'goal.md');
1168
+ if (!goal) {
1169
+ console.log(`${c.dim}No goal.md found${c.reset}`);
1170
+ return;
1171
+ }
1172
+
1173
+ // Count checkboxes in Definition of Done section
1174
+ const doneSection = goal.match(/## Definition of Done\n\n([\s\S]*?)(?=\n## |$)/);
1175
+
1176
+ if (!doneSection) {
1177
+ console.log(`${c.yellow}No "Definition of Done" section found in goal.md${c.reset}`);
1178
+ console.log(`${c.dim}Add a section like:\n\n## Definition of Done\n\n- [ ] Criterion 1\n- [x] Criterion 2${c.reset}`);
1179
+ return;
1180
+ }
1181
+
1182
+ const checkboxes = doneSection[1].match(/- \[[ x]\]/g) || [];
1183
+ const checked = (doneSection[1].match(/- \[x\]/g) || []).length;
1184
+ const total = checkboxes.length;
1185
+
1186
+ if (total === 0) {
1187
+ console.log(`${c.yellow}No criteria defined yet${c.reset}`);
1188
+ return;
1189
+ }
1190
+
1191
+ const percent = Math.round((checked / total) * 100);
1192
+
1193
+ // Visual progress bar
1194
+ const barWidth = 20;
1195
+ const filled = Math.round((percent / 100) * barWidth);
1196
+ const empty = barWidth - filled;
1197
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
1198
+
1199
+ console.log(`\n${c.bold}Progress${c.reset}\n`);
1200
+ console.log(`${c.cyan}${bar}${c.reset} ${percent}%`);
1201
+ console.log(`${c.dim}${checked}/${total} criteria complete${c.reset}\n`);
1202
+
1203
+ // Show criteria
1204
+ const lines = doneSection[1].trim().split('\n');
1205
+ lines.forEach(line => {
1206
+ if (line.startsWith('- [x]')) {
1207
+ console.log(`${c.green}✓${c.reset} ${line.slice(6)}`);
1208
+ } else if (line.startsWith('- [ ]')) {
1209
+ console.log(`${c.dim}○${c.reset} ${line.slice(6)}`);
1210
+ }
1211
+ });
1212
+
1213
+ console.log('');
1214
+
1215
+ // Update progress in goal.md if it has a Progress line
1216
+ if (goal.includes('## Progress:')) {
1217
+ const updatedGoal = goal.replace(/## Progress: \d+%/, `## Progress: ${percent}%`);
1218
+ if (updatedGoal !== goal) {
1219
+ writeMemFile(memDir, 'goal.md', updatedGoal);
1220
+ git(memDir, 'add', 'goal.md');
1221
+ git(memDir, 'commit', '-m', `progress: ${percent}%`);
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ // Mark a criterion as done
1227
+ function cmdCriteria(args, memDir) {
1228
+ if (!memDir) {
1229
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1230
+ return;
1231
+ }
1232
+
1233
+ const action = args[0]; // 'add', 'check', or number
1234
+ const text = args.slice(1).join(' ');
1235
+
1236
+ let goal = readMemFile(memDir, 'goal.md');
1237
+ if (!goal) {
1238
+ console.log(`${c.dim}No goal.md found${c.reset}`);
1239
+ return;
1240
+ }
1241
+
1242
+ if (action === 'add' && text) {
1243
+ // Add new criterion
1244
+ goal = goal.replace(
1245
+ /## Definition of Done\n\n/,
1246
+ `## Definition of Done\n\n- [ ] ${text}\n`
1247
+ );
1248
+ writeMemFile(memDir, 'goal.md', goal);
1249
+ git(memDir, 'add', 'goal.md');
1250
+ git(memDir, 'commit', '-m', `criteria: add "${text.slice(0, 30)}"`);
1251
+ console.log(`${c.green}✓${c.reset} Added criterion: ${text}`);
1252
+ return;
1253
+ }
1254
+
1255
+ if (action === 'check' || !isNaN(parseInt(action))) {
1256
+ // Check off a criterion by number
1257
+ const num = action === 'check' ? parseInt(text) : parseInt(action);
1258
+
1259
+ const lines = goal.split('\n');
1260
+ let criteriaIndex = 0;
1261
+
1262
+ for (let i = 0; i < lines.length; i++) {
1263
+ if (lines[i].startsWith('- [ ]')) {
1264
+ criteriaIndex++;
1265
+ if (criteriaIndex === num) {
1266
+ lines[i] = lines[i].replace('- [ ]', '- [x]');
1267
+ break;
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ goal = lines.join('\n');
1273
+ writeMemFile(memDir, 'goal.md', goal);
1274
+ git(memDir, 'add', 'goal.md');
1275
+ git(memDir, 'commit', '-m', `criteria: complete #${num}`);
1276
+ console.log(`${c.green}✓${c.reset} Marked criterion #${num} complete`);
1277
+
1278
+ // Show updated progress
1279
+ cmdProgress([], memDir);
1280
+ return;
1281
+ }
1282
+
1283
+ // Show usage
1284
+ console.log(`${c.bold}mem criteria${c.reset} - Manage success criteria\n`);
1285
+ console.log(`${c.dim}Commands:${c.reset}`);
1286
+ console.log(` ${c.cyan}mem criteria add "<text>"${c.reset} Add new criterion`);
1287
+ console.log(` ${c.cyan}mem criteria check <n>${c.reset} Mark criterion #n complete`);
1288
+ console.log(` ${c.cyan}mem criteria <n>${c.reset} Same as check`);
1289
+ console.log('');
1290
+ }
1291
+
1292
+ // ==================== PRIMITIVES ====================
1293
+
1294
+ function cmdSet(args, memDir) {
1295
+ if (!memDir) {
1296
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1297
+ return;
1298
+ }
1299
+
1300
+ const key = args[0];
1301
+ const value = args.slice(1).join(' ');
1302
+
1303
+ if (!key || !value) {
1304
+ console.log(`${c.red}Usage:${c.reset} mem set <key> <value>`);
1305
+ return;
1306
+ }
1307
+
1308
+ // Store in a simple key-value format in state.md frontmatter
1309
+ const state = readMemFile(memDir, 'state.md') || '';
1310
+ const { frontmatter, body } = parseFrontmatter(state);
1311
+
1312
+ frontmatter[key] = value;
1313
+
1314
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
1315
+ git(memDir, 'add', 'state.md');
1316
+ git(memDir, 'commit', '-m', `set: ${key}=${value.slice(0, 30)}`);
1317
+
1318
+ console.log(`${c.green}✓${c.reset} ${key} = ${value}`);
1319
+ }
1320
+
1321
+ function cmdGet(args, memDir) {
1322
+ if (!memDir) {
1323
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1324
+ return;
1325
+ }
1326
+
1327
+ const key = args[0];
1328
+
1329
+ if (!key) {
1330
+ console.log(`${c.red}Usage:${c.reset} mem get <key>`);
1331
+ return;
1332
+ }
1333
+
1334
+ const state = readMemFile(memDir, 'state.md') || '';
1335
+ const { frontmatter } = parseFrontmatter(state);
1336
+
1337
+ if (frontmatter[key] !== undefined) {
1338
+ console.log(frontmatter[key]);
1339
+ } else {
1340
+ console.log(`${c.dim}Not set${c.reset}`);
1341
+ }
1342
+ }
1343
+
1344
+ function cmdAppend(args, memDir) {
1345
+ if (!memDir) {
1346
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1347
+ return;
1348
+ }
1349
+
1350
+ const list = args[0];
1351
+ const item = args.slice(1).join(' ');
1352
+
1353
+ if (!list || !item) {
1354
+ console.log(`${c.red}Usage:${c.reset} mem append <list> <item>`);
1355
+ return;
1356
+ }
1357
+
1358
+ // Map list names to files
1359
+ const fileMap = {
1360
+ learnings: 'memory.md',
1361
+ playbook: 'playbook.md',
1362
+ checkpoints: 'state.md'
1363
+ };
1364
+
1365
+ const filename = fileMap[list];
1366
+ if (!filename) {
1367
+ console.log(`${c.red}Unknown list:${c.reset} ${list}`);
1368
+ console.log(`${c.dim}Valid lists: learnings, playbook, checkpoints${c.reset}`);
1369
+ return;
1370
+ }
1371
+
1372
+ const timestamp = new Date().toISOString().split('T')[0];
1373
+
1374
+ if (list === 'checkpoints') {
1375
+ // Special handling for checkpoints in state.md
1376
+ cmdCheckpoint([item], memDir);
1377
+ } else {
1378
+ const content = readMemFile(memDir, filename) || '';
1379
+ const newContent = content + `- ${timestamp}: ${item}\n`;
1380
+ writeMemFile(memDir, filename, newContent);
1381
+ git(memDir, 'add', filename);
1382
+ git(memDir, 'commit', '-m', `append ${list}: ${item.slice(0, 40)}`);
1383
+ console.log(`${c.green}✓${c.reset} Appended to ${list}`);
1384
+ }
1385
+ }
1386
+
1387
+ function cmdLog(memDir) {
1388
+ if (!memDir) {
1389
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1390
+ return;
1391
+ }
1392
+
1393
+ const log = git(memDir, 'log', '--oneline', '-30');
1394
+ console.log(log);
1395
+ }
1396
+
1397
+ // ==================== SKILL ====================
1398
+
1399
+ function isSkillInstalled(provider) {
1400
+ const skillDir = path.join(
1401
+ process.env.HOME || process.env.USERPROFILE,
1402
+ provider === 'claude' ? '.claude' : '.gemini',
1403
+ 'skills',
1404
+ 'mem'
1405
+ );
1406
+ return fs.existsSync(path.join(skillDir, 'SKILL.md'));
1407
+ }
1408
+
1409
+ function installSkillTo(provider) {
1410
+ const baseDir = path.join(
1411
+ process.env.HOME || process.env.USERPROFILE,
1412
+ provider === 'claude' ? '.claude' : '.gemini',
1413
+ 'skills',
1414
+ 'mem'
1415
+ );
1416
+
1417
+ if (!fs.existsSync(baseDir)) {
1418
+ fs.mkdirSync(baseDir, { recursive: true });
1419
+ }
1420
+
1421
+ fs.writeFileSync(path.join(baseDir, 'SKILL.md'), MEM_SKILL);
1422
+ return baseDir;
1423
+ }
1424
+
1425
+ function handleSkillCommand(args) {
1426
+ const subCmd = args[1];
1427
+
1428
+ if (!subCmd || subCmd === 'view' || subCmd === 'show') {
1429
+ console.log(`\n${c.bold}${c.cyan}/mem${c.reset} - ${c.dim}LLM instructions for using mem${c.reset}\n`);
1430
+
1431
+ const claudeInstalled = isSkillInstalled('claude');
1432
+ const geminiInstalled = isSkillInstalled('gemini');
1433
+
1434
+ if (claudeInstalled || geminiInstalled) {
1435
+ console.log(`${c.green}Installed:${c.reset}`);
1436
+ if (claudeInstalled) console.log(` ${c.dim}~/.claude/skills/mem/SKILL.md${c.reset}`);
1437
+ if (geminiInstalled) console.log(` ${c.dim}~/.gemini/skills/mem/SKILL.md${c.reset}`);
1438
+ console.log('');
1439
+ }
1440
+
1441
+ console.log(c.dim + '─'.repeat(60) + c.reset);
1442
+ console.log(MEM_SKILL);
1443
+ console.log(c.dim + '─'.repeat(60) + c.reset);
1444
+
1445
+ if (!claudeInstalled && !geminiInstalled) {
1446
+ console.log(`\n${c.dim}Install with: ${c.reset}mem skill install`);
1447
+ }
1448
+ console.log('');
1449
+ return;
1450
+ }
1451
+
1452
+ if (subCmd === 'install' || subCmd === 'add') {
1453
+ const target = args[2];
1454
+
1455
+ console.log(`\n${c.bold}Install mem skill${c.reset}\n`);
1456
+
1457
+ if (!target || target === 'all') {
1458
+ let installed = 0;
1459
+
1460
+ // Always try claude
1461
+ try {
1462
+ const dest = installSkillTo('claude');
1463
+ console.log(`${c.green}✓${c.reset} Installed to ${c.dim}${dest}${c.reset}`);
1464
+ installed++;
1465
+ } catch {}
1466
+
1467
+ // Always try gemini
1468
+ try {
1469
+ const dest = installSkillTo('gemini');
1470
+ console.log(`${c.green}✓${c.reset} Installed to ${c.dim}${dest}${c.reset}`);
1471
+ installed++;
1472
+ } catch {}
1473
+
1474
+ if (installed > 0) {
1475
+ console.log(`\n${c.dim}LLMs can now use /mem to learn how to use persistent memory.${c.reset}\n`);
1476
+ }
1477
+ } else if (target === 'claude' || target === 'gemini') {
1478
+ const dest = installSkillTo(target);
1479
+ console.log(`${c.green}✓${c.reset} Installed to ${c.dim}${dest}${c.reset}`);
1480
+ console.log(`\n${c.dim}LLMs can now use /mem to learn how to use persistent memory.${c.reset}\n`);
1481
+ } else {
1482
+ console.log(`${c.yellow}Unknown target:${c.reset} ${target}`);
1483
+ console.log(`${c.dim}Usage: mem skill install [claude|gemini|all]${c.reset}\n`);
1484
+ }
1485
+ return;
1486
+ }
1487
+
1488
+ console.log(`${c.bold}mem skill${c.reset} - Manage the mem skill for LLMs\n`);
1489
+ console.log(`${c.dim}Commands:${c.reset}`);
1490
+ console.log(` ${c.cyan}mem skill${c.reset} View the skill content`);
1491
+ console.log(` ${c.cyan}mem skill install${c.reset} Install to all providers`);
1492
+ console.log(` ${c.cyan}mem skill install claude${c.reset} Install to Claude only`);
1493
+ console.log('');
1494
+ }
1495
+
1496
+ // ==================== WAKE ====================
1497
+
1498
+ // Parse wake pattern to cron expression
1499
+ function parseWakeToCron(pattern) {
1500
+ pattern = pattern.toLowerCase().trim();
1501
+
1502
+ // Handle intervals: every Xm, every Xh
1503
+ const intervalMatch = pattern.match(/^every\s+(\d+)\s*(m|min|minutes?|h|hr|hours?)$/);
1504
+ if (intervalMatch) {
1505
+ const num = parseInt(intervalMatch[1]);
1506
+ const unit = intervalMatch[2][0];
1507
+ if (unit === 'm') {
1508
+ return `*/${num} * * * *`;
1509
+ } else if (unit === 'h') {
1510
+ return `0 */${num} * * *`;
1511
+ }
1512
+ }
1513
+
1514
+ // Handle daily: Xam daily, Xpm daily, every day at X
1515
+ const dailyMatch = pattern.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:daily|every\s*day)?$/);
1516
+ if (dailyMatch) {
1517
+ let hour = parseInt(dailyMatch[1]);
1518
+ const min = dailyMatch[2] ? parseInt(dailyMatch[2]) : 0;
1519
+ const ampm = dailyMatch[3];
1520
+ if (ampm === 'pm' && hour < 12) hour += 12;
1521
+ if (ampm === 'am' && hour === 12) hour = 0;
1522
+ return `${min} ${hour} * * *`;
1523
+ }
1524
+
1525
+ // Handle weekly: monday 9am, every tuesday at 3pm
1526
+ 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)?$/);
1527
+ if (weeklyMatch) {
1528
+ const days = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
1529
+ const day = days[weeklyMatch[1]];
1530
+ let hour = parseInt(weeklyMatch[2]);
1531
+ const min = weeklyMatch[3] ? parseInt(weeklyMatch[3]) : 0;
1532
+ const ampm = weeklyMatch[4];
1533
+ if (ampm === 'pm' && hour < 12) hour += 12;
1534
+ if (ampm === 'am' && hour === 12) hour = 0;
1535
+ return `${min} ${hour} * * ${day}`;
1536
+ }
1537
+
1538
+ // Already cron format? Pass through
1539
+ if (pattern.match(/^[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+\s+[\d\*\/\-\,]+$/)) {
1540
+ return pattern;
1541
+ }
1542
+
1543
+ return null;
1544
+ }
1545
+
1546
+ // Set/get/clear wake
1547
+ function cmdWake(args, memDir) {
1548
+ if (!memDir) {
1549
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1550
+ return;
1551
+ }
1552
+
1553
+ const state = readMemFile(memDir, 'state.md') || '';
1554
+ const { frontmatter, body } = parseFrontmatter(state);
1555
+
1556
+ // No args: show current wake
1557
+ if (args.length === 0) {
1558
+ if (frontmatter.wake) {
1559
+ console.log(`${c.bold}Wake:${c.reset} ${frontmatter.wake}`);
1560
+ if (frontmatter.wake_command) {
1561
+ console.log(`${c.bold}Command:${c.reset} ${frontmatter.wake_command}`);
1562
+ }
1563
+ const cron = parseWakeToCron(frontmatter.wake);
1564
+ if (cron) {
1565
+ console.log(`${c.dim}Cron: ${cron}${c.reset}`);
1566
+ }
1567
+ } else {
1568
+ console.log(`${c.dim}No wake set${c.reset}`);
1569
+ console.log(`${c.dim}Usage: mem wake "every 15m" [--run "command"]${c.reset}`);
1570
+ }
1571
+ return;
1572
+ }
1573
+
1574
+ // Clear wake
1575
+ if (args[0] === 'clear') {
1576
+ delete frontmatter.wake;
1577
+ delete frontmatter.wake_command;
1578
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
1579
+ git(memDir, 'add', 'state.md');
1580
+ git(memDir, 'commit', '-m', 'wake: clear');
1581
+ console.log(`${c.green}✓${c.reset} Wake cleared`);
1582
+ return;
1583
+ }
1584
+
1585
+ // Set wake
1586
+ let pattern = '';
1587
+ let command = '';
1588
+
1589
+ // Parse args
1590
+ for (let i = 0; i < args.length; i++) {
1591
+ if (args[i] === '--run' || args[i] === '-r') {
1592
+ command = args.slice(i + 1).join(' ');
1593
+ break;
1594
+ }
1595
+ pattern += (pattern ? ' ' : '') + args[i];
1596
+ }
1597
+
1598
+ // Validate pattern
1599
+ const cron = parseWakeToCron(pattern);
1600
+ if (!cron) {
1601
+ console.log(`${c.red}Could not parse wake pattern:${c.reset} ${pattern}`);
1602
+ console.log(`\n${c.dim}Examples:${c.reset}`);
1603
+ console.log(` mem wake "every 15m"`);
1604
+ console.log(` mem wake "every 2h"`);
1605
+ console.log(` mem wake "8am daily"`);
1606
+ console.log(` mem wake "monday 9am"`);
1607
+ console.log(` mem wake "*/30 * * * *" ${c.dim}(raw cron)${c.reset}`);
1608
+ return;
1609
+ }
1610
+
1611
+ frontmatter.wake = pattern;
1612
+ if (command) {
1613
+ frontmatter.wake_command = command;
1614
+ }
1615
+
1616
+ writeMemFile(memDir, 'state.md', serializeFrontmatter(frontmatter, body));
1617
+ git(memDir, 'add', 'state.md');
1618
+ git(memDir, 'commit', '-m', `wake: ${pattern}`);
1619
+
1620
+ console.log(`${c.green}✓${c.reset} Wake set: ${c.bold}${pattern}${c.reset}`);
1621
+ console.log(`${c.dim}Cron: ${cron}${c.reset}`);
1622
+ if (command) {
1623
+ console.log(`${c.dim}Command: ${command}${c.reset}`);
1624
+ }
1625
+ console.log(`\n${c.dim}Export with: ${c.reset}mem cron export`);
1626
+ }
1627
+
1628
+ // Export to cron format
1629
+ function cmdCronExport(memDir) {
1630
+ if (!memDir) {
1631
+ console.log(`${c.yellow}No .mem repo found.${c.reset}`);
1632
+ return;
1633
+ }
1634
+
1635
+ const state = readMemFile(memDir, 'state.md') || '';
1636
+ const { frontmatter } = parseFrontmatter(state);
1637
+
1638
+ if (!frontmatter.wake) {
1639
+ console.log(`${c.yellow}No wake set.${c.reset} Use ${c.cyan}mem wake "pattern"${c.reset} first.`);
1640
+ return;
1641
+ }
1642
+
1643
+ const cron = parseWakeToCron(frontmatter.wake);
1644
+ if (!cron) {
1645
+ console.log(`${c.red}Could not parse wake pattern:${c.reset} ${frontmatter.wake}`);
1646
+ return;
1647
+ }
1648
+
1649
+ const command = frontmatter.wake_command || `cd ${memDir} && mem context`;
1650
+ const entry = `${cron} ${command}`;
1651
+
1652
+ // Just output the cron line (can be piped/appended)
1653
+ console.log(entry);
1654
+ }
1655
+
1656
+ // ==================== MCP ====================
1657
+
1658
+ function startMCPServer(args) {
1659
+ const mcpPath = path.join(__dirname, 'mcp.js');
1660
+
1661
+ // Pass through args
1662
+ const mcpArgs = args.slice(1);
1663
+
1664
+ // Run MCP server
1665
+ const { spawn } = require('child_process');
1666
+ const child = spawn('node', [mcpPath, ...mcpArgs], {
1667
+ stdio: 'inherit'
1668
+ });
1669
+
1670
+ child.on('error', (err) => {
1671
+ console.error(`${c.red}Failed to start MCP server:${c.reset}`, err.message);
1672
+ process.exit(1);
1673
+ });
1674
+
1675
+ child.on('exit', (code) => {
1676
+ process.exit(code || 0);
1677
+ });
1678
+ }
1679
+
1680
+ function showMCPConfig() {
1681
+ const mcpPath = path.join(__dirname, 'mcp.js');
1682
+
1683
+ console.log(`\n${c.bold}MCP Server Configuration${c.reset}\n`);
1684
+ console.log(`Add to your Claude Desktop config (${c.dim}~/Library/Application Support/Claude/claude_desktop_config.json${c.reset}):\n`);
1685
+
1686
+ const config = {
1687
+ "mcpServers": {
1688
+ "mem": {
1689
+ "command": "node",
1690
+ "args": [mcpPath]
1691
+ }
1692
+ }
1693
+ };
1694
+
1695
+ console.log(JSON.stringify(config, null, 2));
1696
+ console.log(`\n${c.dim}Or for a specific project:${c.reset}\n`);
1697
+
1698
+ const configWithDir = {
1699
+ "mcpServers": {
1700
+ "mem": {
1701
+ "command": "node",
1702
+ "args": [mcpPath, "--dir", "/path/to/your/project"]
1703
+ }
1704
+ }
1705
+ };
1706
+
1707
+ console.log(JSON.stringify(configWithDir, null, 2));
1708
+ console.log('');
1709
+ }
1710
+
1711
+ // ==================== HELP ====================
1712
+
1713
+ function showHelp() {
1714
+ console.log(`
1715
+ ${c.bold}mem${c.reset} - Persistent memory for AI agents
1716
+
1717
+ ${c.bold}USAGE${c.reset}
1718
+ mem <command> [args]
1719
+
1720
+ ${c.bold}LIFECYCLE${c.reset}
1721
+ init <name> "<goal>" Start new task (creates .mem repo or branch)
1722
+ status Current state summary
1723
+ done Complete task, reflect, merge learnings
1724
+
1725
+ ${c.bold}PROGRESS${c.reset}
1726
+ goal [value] Get/set current goal
1727
+ next [step] Get/set next step
1728
+ checkpoint "<msg>" Save progress point
1729
+ stuck [reason|clear] Mark/clear blocker
1730
+
1731
+ ${c.bold}LEARNING${c.reset}
1732
+ learn [-g] "<insight>" Add learning (-g for global)
1733
+ learnings [-g] List learnings with IDs
1734
+ playbook View global playbook
1735
+ promote <n> Promote learning #n to playbook
1736
+ constraint add "..." Add constraint/boundary
1737
+ constraint remove <n> Remove constraint
1738
+ constraints List constraints
1739
+
1740
+ ${c.bold}PROGRESS${c.reset}
1741
+ progress Show progress % against Definition of Done
1742
+ criteria add "<text>" Add success criterion
1743
+ criteria <n> Mark criterion #n complete
1744
+
1745
+ ${c.bold}QUERY${c.reset}
1746
+ context Full hydration for agent wake
1747
+ history Task progression
1748
+ query "<search>" Search all memory
1749
+
1750
+ ${c.bold}TASKS${c.reset}
1751
+ tasks List all tasks (branches)
1752
+ switch <name> Switch to task
1753
+
1754
+ ${c.bold}SYNC${c.reset}
1755
+ sync Push/pull with remote
1756
+
1757
+ ${c.bold}PRIMITIVES${c.reset}
1758
+ set <key> <value> Set a value
1759
+ get <key> Get a value
1760
+ append <list> <item> Append to list
1761
+ log Raw git log
1762
+
1763
+ ${c.bold}WAKE${c.reset}
1764
+ wake "<pattern>" Set wake schedule (every 15m, 8am daily, etc.)
1765
+ wake --run "<cmd>" Set wake with custom command
1766
+ wake clear Clear wake schedule
1767
+ cron export Export wake as crontab entry
1768
+
1769
+ ${c.bold}INTEGRATION${c.reset}
1770
+ skill View LLM skill
1771
+ skill install Install skill to Claude/Gemini
1772
+ mcp Start MCP server (stdio)
1773
+ mcp config Show MCP config for Claude Desktop
1774
+
1775
+ ${c.bold}EXAMPLES${c.reset}
1776
+ mem init build-landing "Create landing page for repr.dev"
1777
+ mem checkpoint "Hero section complete"
1778
+ mem learn "Tailwind is faster than custom CSS"
1779
+ mem context # Get full state for new session
1780
+ mem done # Complete and merge learnings
1781
+
1782
+ ${c.dim}Docs: https://github.com/ramarlina/memx${c.reset}
1783
+ `);
1784
+ }
1785
+
1786
+ // ==================== MAIN ====================
1787
+
1788
+ async function main() {
1789
+ const args = process.argv.slice(2);
1790
+ const cmd = args[0];
1791
+ const cmdArgs = args.slice(1);
1792
+
1793
+ // Find .mem repo (returns object with memDir, taskBranch, etc.)
1794
+ const memInfo = findMemDir();
1795
+ let memDir = null;
1796
+
1797
+ if (memInfo) {
1798
+ memDir = memInfo.memDir;
1799
+ // If central mem with task mapping, ensure we're on the right branch
1800
+ if (!memInfo.isLocal && memInfo.taskBranch) {
1801
+ ensureTaskBranch(memDir, memInfo.taskBranch);
1802
+ }
1803
+ }
1804
+
1805
+ // No command and no .mem? Start interactive onboarding
1806
+ if (!cmd && !memDir) {
1807
+ await interactiveInit();
1808
+ return;
1809
+ }
1810
+
1811
+ // No command but has central mem with no mapping for this dir?
1812
+ if (!cmd && memInfo && memInfo.unmapped) {
1813
+ console.log(`${c.dim}No task mapped for this directory.${c.reset}`);
1814
+ const create = await prompt(`Create a new task? [Y/n]: `);
1815
+ if (create.toLowerCase() !== 'n') {
1816
+ await interactiveInit();
1817
+ }
1818
+ return;
1819
+ }
1820
+
1821
+ // No command but has .mem? Show status
1822
+ if (!cmd && memDir) {
1823
+ cmdStatus(memDir);
1824
+ console.log(`${c.dim}Run ${c.reset}mem help${c.dim} for all commands${c.reset}\n`);
1825
+ return;
1826
+ }
1827
+
1828
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
1829
+ showHelp();
1830
+ return;
1831
+ }
1832
+
1833
+ switch (cmd) {
1834
+ // Lifecycle
1835
+ case 'init':
1836
+ await cmdInit(cmdArgs, memDir);
1837
+ break;
1838
+ case 'status':
1839
+ cmdStatus(memDir);
1840
+ break;
1841
+ case 'done':
1842
+ await cmdDone(memDir);
1843
+ break;
1844
+
1845
+ // Progress
1846
+ case 'goal':
1847
+ cmdGoal(cmdArgs, memDir);
1848
+ break;
1849
+ case 'next':
1850
+ cmdNext(cmdArgs, memDir);
1851
+ break;
1852
+ case 'checkpoint':
1853
+ case 'cp':
1854
+ cmdCheckpoint(cmdArgs, memDir);
1855
+ break;
1856
+ case 'stuck':
1857
+ cmdStuck(cmdArgs, memDir);
1858
+ break;
1859
+
1860
+ // Learning
1861
+ case 'learn':
1862
+ cmdLearn(cmdArgs, memDir);
1863
+ break;
1864
+ case 'learnings':
1865
+ case 'ls-learn':
1866
+ cmdLearnings(cmdArgs, memDir);
1867
+ break;
1868
+ case 'playbook':
1869
+ case 'pb':
1870
+ cmdPlaybook(memDir);
1871
+ break;
1872
+ case 'promote':
1873
+ cmdPromote(cmdArgs, memDir);
1874
+ break;
1875
+ case 'constraint':
1876
+ case 'constraints':
1877
+ cmdConstraint(cmdArgs, memDir);
1878
+ break;
1879
+
1880
+ // Progress
1881
+ case 'progress':
1882
+ case 'prog':
1883
+ case '%':
1884
+ cmdProgress(cmdArgs, memDir);
1885
+ break;
1886
+ case 'criteria':
1887
+ case 'crit':
1888
+ cmdCriteria(cmdArgs, memDir);
1889
+ break;
1890
+
1891
+ // Query
1892
+ case 'context':
1893
+ case 'ctx':
1894
+ cmdContext(memDir);
1895
+ break;
1896
+ case 'history':
1897
+ case 'hist':
1898
+ cmdHistory(memDir);
1899
+ break;
1900
+ case 'query':
1901
+ case 'q':
1902
+ cmdQuery(cmdArgs, memDir);
1903
+ break;
1904
+
1905
+ // Tasks
1906
+ case 'tasks':
1907
+ case 'ls':
1908
+ cmdTasks(memDir);
1909
+ break;
1910
+ case 'switch':
1911
+ case 'sw':
1912
+ cmdSwitch(cmdArgs, memDir);
1913
+ break;
1914
+
1915
+ // Sync
1916
+ case 'sync':
1917
+ cmdSync(memDir);
1918
+ break;
1919
+
1920
+ // Primitives
1921
+ case 'set':
1922
+ cmdSet(cmdArgs, memDir);
1923
+ break;
1924
+ case 'get':
1925
+ cmdGet(cmdArgs, memDir);
1926
+ break;
1927
+ case 'append':
1928
+ cmdAppend(cmdArgs, memDir);
1929
+ break;
1930
+ case 'log':
1931
+ cmdLog(memDir);
1932
+ break;
1933
+
1934
+ // Skill
1935
+ case 'skill':
1936
+ handleSkillCommand(args);
1937
+ break;
1938
+
1939
+ // Wake
1940
+ case 'wake':
1941
+ cmdWake(cmdArgs, memDir);
1942
+ break;
1943
+ case 'cron':
1944
+ if (cmdArgs[0] === 'export') {
1945
+ cmdCronExport(memDir);
1946
+ } else {
1947
+ console.log(`${c.dim}Usage: mem cron export${c.reset}`);
1948
+ }
1949
+ break;
1950
+
1951
+ // MCP
1952
+ case 'mcp':
1953
+ if (cmdArgs[0] === 'config') {
1954
+ showMCPConfig();
1955
+ } else {
1956
+ startMCPServer(args);
1957
+ }
1958
+ break;
1959
+
1960
+ default:
1961
+ console.log(`${c.red}Unknown command:${c.reset} ${cmd}`);
1962
+ console.log(`${c.dim}Run ${c.reset}mem help${c.dim} for usage${c.reset}`);
1963
+ }
1964
+ }
1965
+
1966
+ main().catch(err => {
1967
+ console.error(`${c.red}Error:${c.reset}`, err.message);
1968
+ process.exit(1);
1969
+ });