@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/.claude/settings.local.json +7 -0
- package/README.md +183 -0
- package/index.js +1969 -0
- package/mcp.js +308 -0
- package/package.json +27 -0
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
|
+
});
|